Monday, June 13, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


为什么Hook没有ErrorBoundary?

Posted: 12 Jun 2022 07:50 PM PDT

大家好,我卡颂。

在很多全面使用Hooks开发的团队,唯一使用ClassComponent的场景就是使用ClassComponent创建ErrorBoundary

可以说,如果Hooks存在如下两个生命周期函数的替代品,就能全面抛弃ClassComponent了:

  • getDerivedStateFromError
  • componentDidCatch

那为什么还没有对标的Hook呢?

今天我们从上述两个生命周期函数的实现原理,以及要移植到Hook上需要付出的成本来谈论这个问题。

欢迎加入人类高质量前端框架群,带飞

ErrorBoundary实现原理

ErrorBoundary可以捕获子孙组件中React工作流程内的错误。

React工作流程指:

  • render阶段,即组件renderDiff算法发生的阶段
  • commit阶段,即渲染DOMcomponentDidMount/Update执行的阶段

这也是为什么事件回调中发生的错误无法被ErrorBoundary捕获 —— 事件回调并不属于React工作流程

如何捕获错误

render阶段的整体执行流程如下:

do {   try {     // render阶段具体的执行流程     workLoop();     break;   } catch (thrownValue) {     handleError(root, thrownValue);   } } while (true);

可以发现,如果render阶段发生错误,会被捕获并执行handleError方法。

类似的,commit阶段的整体执行流程如下:

try {   // ...具体执行流程 } catch (error) {   captureCommitPhaseError(current, nearestMountedAncestor, error); }

如果commit阶段发生错误,会被捕获并执行captureCommitPhaseError方法。

getDerivedStateFromError原理

捕获后的错误如何处理呢?

我们知道,ClassComponentthis.setState第一个参数,除了可以接收新的状态,也能接收改变状态的函数作为参数:

// 可以这样 this.setState(this.state.num + 1)  // 也可以这样 this.setState(num => num + 1)

getDerivedStateFromError的实现,就借助了this.setState改变状态的函数这一特性。

当捕获错误后,即:

  • 对于render阶段handleError执行后
  • 对于commit阶段captureCommitPhaseError执行后

会在ErrorBoundary对应组件中触发类似如下更新:

this.setState(   getDerivedStateFromError.bind(null, error) )

这就是为什么getDerivedStateFromError要求开发者返回新的state —— 本质来说,他就是触发一次新的更新。

componentDidCatch原理

再来看另一个ErrorBoundary相关的生命周期函数 —— componentDidCatch

ClassComponentthis.setState的第二个参数,可以接收回调函数作为参数:

this.setState(newState, () => {   // ...回调 })

当触发的更新渲染到页面后,回调会触发。

这就是componentDidCatch的实现原理。

当捕获错误后,会在ErrorBoundary对应组件中触发类似如下更新:

this.setState(this.state, componentDidCatch.bind(this, error))

处理"未捕获"的错误

可以发现,React运行流程中的错误,都已经被React自身捕获了,再交由ErrorBoundary处理。

如果没有定义ErrorBoundary,这些被捕获的错误需要重新抛出,营造错误未被捕获的感觉

那这一步在哪里执行呢?

this.setState类似,ReactDOM.render(element, container[, callback])第三个参数也能接收回调函数

如果开发者没有定义ErrorBoundary,那么React最终会在ReactDOM.render的回调中抛出错误。

可以发现,在ClassComponentErrorBoundary的实现完全依赖了ClassComponent已有的特性。

Hooks本身并不存在类似this.setState的回调特性,所以实现起来会比较复杂。

实现Hooks中的ErrorBoundary

除了上述谈到的阻碍,FunctionComponentClassComponent在源码层面的运行流程也有细节上的差异,要照搬实现也有一定难度。

如果一定要实现,在最大程度复用现有基础设施的指导方针下,useErrorBoundaryErrorBoundaryHooks中的实现)的使用方式应该类似如下:

function ErrorBoundary({children}: {children: ReactNode}) {   const [errorMsg, updateError] = useState<Error | null>(null);    useErrorBoundary((e: Error) => {     // 捕获到错误,触发更新     updateError(e);   })    return (     <div>       {errorMsg ? '报错:' + errorMsg.toString() : children}     </div>   ) }

其中useErrorBoundary的触发方式类似useEffect

useErrorBoundary((e: Error) => {   // ... })  // 类似 useEffect(() => {   // ... })

笔者仿照ClassComponentErrorBoundary的实现原理与useEffect的实现原理,实现了原生Hooks —— useErrorBoundary

感兴趣的朋友可以在useErrorBoundary在线示例体验效果。

总结

ErrorBoundaryClassComponent中的实现使用了this.setState的回调函数特性,这使得Hooks中要完全实现同样功能,需要额外开发成本。

笔者猜测,这是没有提供对应原生Hooks的原因之一。

十年老友记 | @Peter 谭老师:编程路上,梦想占比更重

Posted: 06 Jun 2022 03:43 AM PDT

俗话说不怕二傻子,就怕二傻子有梦想瞎折腾。
——Peter 谭老师

十年前的今天,SegmentFault 思否正式创立,如一颗嫩绿的幼芽开始成长,期间承载过和煦的日光、沐浴过柔和的春风,也挑战过滂沱的暴雨、体会过凛冽的冬雪。所幸,今日呈现在我们面前的 SegmentFault 思否,已经长成了一棵足以抵御一些风雨的大树,这样的成长离不开各位管理员的修剪,更离不开社区每一位用户的栽种。

正如 SegmentFault 思否创始人之一祁宁所言:

"SegmentFault 思否是一个属于大家的社区,因此,在这个特殊的时刻,我们想跟社区的成员一起为它喝彩。"

这十年中,有成千上万的开发者加入了 SegmentFault 社区,我们雀跃于看到每张不同的新面孔,也感动于社区里的那些老朋友们多年如一日的陪伴,见到他们就像见到一位相识多年的老友。或许老友们会在某一段时间里突然消失,但消失并不代表着再也不见,而是重逢后的那句:好久不见。


今天,我们有幸邀请到老朋友 @Peter谭老师 参与我们的十年老友记系列访谈。

小编有话说:

对比我访谈的其他几位老师,谭老师在很多方面都比较特别。他说自己从来不穿格子衬衫,还说自己最大的焦虑来源于内卷,这些答案是我在这个系列的访谈中第一次听到。

我想,谭老师身上围绕着的焦虑并不少,但无论是"内卷焦虑"还是"年龄焦虑",都不会轻易地影响到他,因为他在心里对自己本身和自己所处的行业有着非常清晰的认知,这些认知督促他提升自我,同时也构建起了他的自信。

1.还记得和 SegmentFault 思否的初识吗?是在什么样的机缘巧合下踏入这个社区的?

记得是在一个朋友的电脑发现的,他说:思否这个社区风格很喜欢,里面内容不错,特别是问答模块,让我以后有前端问题就来搜搜。我当天就回去注册了思否,于是就开启了这段旅程。

2.这些年有没有见证思否的改变?其中对你而言触动最大的是哪一部分?与思否一起成长的路上,你觉得自己改变了什么?

这些年见证了思否的成长和改变,最主要的是跟思否一起成长。思否的名字由来:独立思考,敢于否定,后来也是成为了我人生的一个座右铭。

3.为什么会选择做一名程序员?因为梦想和因为现实这两者的占比哪个更重?

初中时候家里买了电脑,让我学习编程,不过那个时候,思否这样的论坛还没兴起,学习资料少之又少,加上电脑配置很差,JAVA只能用一个控制台输出一些日志,很是无聊,于是短期放弃了,等大学后又开始从事这个行业。是出自骨子里的热爱吧,初中时候想做一个伟大的项目/程序给很多人用,梦想占比更重要。

4.如果有一天因为种种因素你决定放弃编程,你想去做什么?

我可能会去开一个饭店

5.程序员的形象在很多人的心目中离不开格子衬衫、黑框眼镜、双肩背包等物品,你想对这种刻板印象说什么?

我就不是,我从来不穿格子衬衫、黑框眼镜,偶尔背包,我见过很多非常会打理自己的程序员,想说的是:会打理的人做什么都会打理,不会打理的人不管是不是程序员都不会打理自己

6.编程对你而言只是工作任务吗?如果不是的话,它对你有何种特殊的意义呢?

不是。在公司,可能要负责整个公司的前端项目架构和质量,那这个工作任务就很难划分一个度,更多的是出自内心的责任去自驱把工作做得更好。如果是跟朋友做项目,更多的可能是为了实现自己的梦想

7.工作中有没有让你焦虑的事?这种焦虑源于何处?面对焦虑你一般会怎么做?

内卷。国内工作环境太恶劣,这是没有办法的。特别是互联网的内卷,关键很多程序员的内卷毫无意义,可是你得适当的招架,这是一个苦恼/焦虑的事。俗话说不怕二傻子,就怕二傻子有梦想瞎折腾。面对这种事,会适当回击,用实力说话,另外我个人认为我的工作之余成长非常迅速,具备跨多个行业, web3 remote job 的能力,我是用这种模式去面对国内的内卷(就是不做低层次的内卷,越是高层次的内卷越少)。

8.年龄对程序员这个职业有一定的影响,你认同这个观点吗?有没有想过自己未来的职业规划?

认同。国内的核心技术很少,所以很容易被替代。自己的职业未来很清晰,投身全球化,为这五个字努力准备着。

9.请留下你对 SegmentFault 思否社区十岁生日的祝福

种一棵树最好的时间是十年前,其次是现在。这句话给SegmentFault思否社区非常贴切实际,思否社区是国内少有保持口碑并且十年的时间屹立不倒的社区,这次携手Ones之后,相信定会在国内绽放出更多的光芒。思否,十周年,你是最棒的!一起见证你们的下一个十年!


在与谭老师对话的过程中我发现,他确实是一个对自己职业的各个方面都有着掌控力的人,这种掌控力绝不是在一朝一夕间就能锻炼出的,这是多年经验的累积,也是路途坎坷的见证。

十年老友记 | @Airy:编程是毕生爱好,是不可或缺的氧气

Posted: 06 Jun 2022 03:50 AM PDT

编程对我而言是毕生爱好,也是实现梦想的必备技能,是成就感的来源,是生命之火持续燃烧必不可少的氧气。 —— Airy

十年前的今天,SegmentFault 思否正式创立,如一颗嫩绿的幼芽开始成长,期间承载过和煦的日光、沐浴过柔和的春风,也挑战过滂沱的暴雨、体会过凛冽的冬雪。所幸,今日呈现在我们面前的 SegmentFault 思否,已经长成了一棵足以抵御一些风雨的大树,这样的成长离不开各位管理员的修剪,更离不开社区每一位用户的栽种。

正如 SegmentFault 思否创始人之一祁宁所言:

"SegmentFault 思否是一个属于大家的社区,因此,在这个特殊的时刻,我们想跟社区的成员一起为它喝彩。"

这十年中,有成千上万的开发者加入了 SegmentFault 社区,我们雀跃于看到每张不同的新面孔,也感动于社区里的那些老朋友们多年如一日的陪伴,见到他们就像见到一位相识多年的老友。或许老友们会在某一段时间里突然消失,但消失并不代表着再也不见,而是重逢后的那句:好久不见。


今天,我们有幸邀请到老朋友 @Airy 参与我们的十年老友记系列访谈。

小编有话说:

1991 年 8 月 25 日,22 岁的 LinusTorvalds 在芬兰赫尔辛基大学学习计算机期间,在网络上发布了 Linux 内核的源代码。这件事对 Airy 影响极深,受到 LinusTorvalds 此举动的鼓舞后,他决定开始编写代码,踏入编程这个行业。

编程对他而言不只是实现梦想的工具,而是犹如生命之火一样的存在,是他生命中最重要的因素之一。

1.还记得和 SegmentFault 思否的初识吗?是在什么样的机缘巧合下踏入这个社区的?

我跟 Joyqi 和 Sunny 是超过十年的旧相识。Sunny 是一个特别靠谱、特别有想法的年轻人,Joyqi 是我技术上的大神偶像。得知他们创立了 sf 之后大家都很开心,靠谱的人做靠谱的事情总是让人对未来充满期待。

2.这些年有没有见证思否的改变?其中对你而言触动最大的是哪一部分?与思否一起成长的路上,你觉得自己改变了什么?

印象最深的是线下黑客马拉松和 "课程" 模块上线,大家可以有更多的机会用各种不同的方式互相学习,共同进步。

3.为什么会选择做一名程序员?因为梦想和因为现实这两者的占比哪个更重?

Linus Torvalds 创立 Linux 操作系统的经历让我深受震撼,同时也鼓舞着我,让我下定决心要写代码。梦想对我来说更重要,如果还有我

4.如果有一天因为种种因素你决定放弃编程,你想去做什么?

做产品设计,做出人人都喜欢用的产品。做编程老师,教孩子用代码做出好玩的东西。

5.程序员的形象在很多人的心目中离不开格子衬衫、黑框眼镜、双肩背包等物品,你想对这种刻板印象说什么?

没有什么需要证明的,也没有谁的印象是标准。君子和而不同,自己给自己的印象更重要。。

6.编程对你而言只是工作任务吗?如果不是的话,它对你有何种特殊的意义呢?

编程对我而言是毕生爱好,也是实现梦想的必备技能,是成就感的来源,是生命之火持续燃烧必不可少的氧气。

7.工作中有没有让你焦虑的事?这种焦虑源于何处?面对焦虑你一般会怎么做?

焦虑的事情在慢慢变少,更多的是大环境变化导致的。一般跑跑步,学习一门新的技术,可以极大的缓解焦虑。

8.年龄对程序员这个职业有一定的影响,你认同这个观点吗?有没有想过自己未来的职业规划?

不可否认年龄对程序员有一定的影响,就像其他职业一样。未来的规划跟梦想其实是一件事情,需要长期持续不断的努力。

9.请留下你对 SegmentFault 思否社区十岁生日的祝福

十年磨一剑,霜刃展锋芒。祝思否下个十年再创辉煌!


星星之火,可以燎原。
希望 Airy 的生命之火将永不燃熄。

让拖拽更加人性化?如何自定义dragover样式

Posted: 12 Jun 2022 07:31 PM PDT

欢迎关注微信公众号: 前端侦探

在 web 开发中,经常会碰到需要拖拽的场景。为了更好的体验,拖拽区域需要有一定的变化提示,告诉用户:"现在可以放在这里了~",例如这样的

dragover效果

之前在这篇文章讲述了如何自定义 drag 样式,这次接着探索一下如何自定义 dragover 样式。

一、dragenter 和 dragleave

要实现这样的效果,少不了和dragenterdragleave打交道。

当拖动的元素进入有效的放置目标时, 将会触发dragenter 事件

当拖动的元素离开有效的放置目标时,将会触发dragleave 事件

拖拽目标和放置目标

假设现在有这样一个结构,这里 img是拖拽目标,div.content是放置目标。

<img> <div class="content"></div>

然后在document监听一下

document.addEventListener('dragleave', function(ev) {     console.log('dragleave', ev.target) }) document.addEventListener('dragenter', function(ev) {     console.log('dragenter', ev.target) })

那么,将img拖入div.content的过程中,肯定会触发dragenterdragleave这两个事件,如下

dragenter和dragleave

如果页面比较简单,要自定义拖拽过程就比较容易了

document.addEventListener('dragleave', function(ev) {     ev.target.toggleAttribute('over',false); }) document.addEventListener('dragenter', function(ev) {     ev.target.toggleAttribute('over',true); })

通过添加over属性自定义样式

.content[over]{   outline: 4px solid slateblue; }

效果如下

dragover效果

是不是非常容易呢?

实际使用起来其实还存在很多局限性,下面一一介绍

二、当放置目标有子元素时

大部分情况下,放置目标并不是空的,还有其他子元素,如果采用上面的方式就会有问题了,假设布局是这样的,为了区分,可以给需要放置的元素添加一个属性,比如allowdrop,表示允许放置

<img> <div class="content" allowdrop>     <div>不允许放置</div> </div>

这里通过属性区分一下

document.addEventListener('dragleave', function(ev) {   if (ev.target.getAttribute('allowdrop')!==null) {     ev.target.toggleAttribute('over',false);   } }) document.addEventListener('dragenter', function(ev) {   if (ev.target.getAttribute('allowdrop')!==null) {     ev.target.toggleAttribute('over',true);   } })

效果如下

有子元素的情况下

可以看到,当拖拽目标经过子元素时,外面的样式已经丢失了。原因其实很简单,在经过子元素时,放置目标也触发了dragleave事件!

那有没有办法不触发呢?这里有两种方式:

首先可以取消dragleave的监听,因为在执行dragleave时,元素本身是不知道即将进入哪一个区域,很容易"误伤"。取而代之的是每次dragenter时,先移除上一次放置目标的属性,然后再添加新的,有点类似选项卡的操作,具体实现如下:

var lastDrop = null; document.addEventListener('dragenter', function(ev) {   if (lastDrop) {     lastDrop.toggleAttribute('over',false);   }   const dropbox = ev.target.closest('[allowdrop]'); // 获取最近的放置目标   if (dropbox) {     dropbox.toggleAttribute('over',true);     lastDrop = dropbox;   } })

还有另一种方式:借助 CSS 就非常容易了

这里有一个非常简单粗暴的方式,直接将子元素禁用鼠标响应,如下

.content[allowdrop][over] *{   pointer-events: none; }

这样,在滑过任何子元素都不会有响应了,完美😁

有子元素的情况,完美

三、多层嵌套放置目标

上面这种方式其实可以解决大多数问题了,毕竟大部分场景都是扁平的。不过有时候也会碰到多层结构,比如那种可视化编辑工具,尤其是目前比较火的低代码平台,就会涉及到多层结构,假设 HTML 是这样的

<img> <div class="content" allowdrop>   <div class="content" allowdrop></div>     <div class="content">不允许拖拽</div>   <div class="content" allowdrop></div> </div>

如果按照 CSS 的处理方式(JS 方式没有问题),由于所有子元素都被禁用,里面的结构自然也无法响应了

多层嵌套结构

那如何让里面的放置目标可以响应呢?其实只需要改一下上面的 CSS 即可,如下

.content[allowdrop][over]>*:not([allowdrop]){   pointer-events: none; }

这里使用了>选择器,表示只选择子元素,不包含后代元素,然后排除掉放置目标,这样就能实现多层嵌套了,效果如下

多层嵌套结构,完美

是不是出乎意料的简单呢?

四、其他交互细节

不知道大家发现没,上面的例子在拖拽开始,鼠标就一直处于这种"可放置"状态,不管是在放置目标外部还是内部,如下

鼠标指针状态

这是因为设置了dragover属性,所以整个document都变成了可放置目标,都允许触发drop事件

document.addEventListener('dragover', function(ev){   ev.preventDefault() })

如果希望交互更加细腻,体验更好,那么在鼠标指示上也可以进一步的优化,可以在进入放置目标后才变成这种状态,实现如下

document.addEventListener('dragover', function(ev){   const dropbox = ev.target.closest('[allowdrop]');   if (dropbox) {     ev.preventDefault()   } })

效果如下(注意观察鼠标的变化🔽)

拖拽过程中的鼠标变化

除此之外,还应该在drop结束后移除掉over属性

document.addEventListener('drop', function(ev){   const dropbox = ev.target.closest('[allowdrop]');   if (dropbox) {     dropbox.toggleAttribute('over',false);   } })

这样就实现了一个完全通用的自定义 dragover效果,区区数十行,划重点,完整代码如下:

document.addEventListener('dragover', function(ev){   const dropbox = ev.target.closest('[allowdrop]');   if (dropbox) {     ev.preventDefault()   } })  document.addEventListener('drop', function(ev){   ev.target.toggleAttribute('over',false); })  document.addEventListener('dragleave', function(ev) {   if (ev.target.getAttribute('allowdrop')!==null) {     ev.target.toggleAttribute('over',false);   } }) document.addEventListener('dragenter', function(ev) {   if (ev.target.getAttribute('allowdrop')!==null) {     ev.target.toggleAttribute('over',true);   } })  // 或者以下方式,无需dragleave,无需额外 CSS var lastDrop = null; document.addEventListener('dragenter', function(ev) {   if (lastDrop) {     lastDrop.toggleAttribute('over',false);   }   const dropbox = ev.target.closest('[allowdrop]'); // 获取最近的放置目标   if (dropbox) {     dropbox.toggleAttribute('over',true);     lastDrop = dropbox;   } })

当然还少不了 CSS 的配合,同样重要

[allowdrop]:empty::after{   content: '拖放此处'; } [allowdrop][over]:empty::after{   content: '松开放置'; } [allowdrop][over]{   /*自定义样式*/ } [allowdrop][over]>*:not([allowdrop]){   pointer-events: none; }
这里有个 CSS 小技巧,上面例子在拖放过程中的文字提示变化其实是通过伪元素实时变化的~

你也可以查看在线链接:自定义 dragover (codepen.io)或者自定义 dragover (juejin.cn)

另外,如果需要完全自定义拖拽,可以参考这个项目:https://github.com/XboxYan/draggable-polyfill,非常轻量,100 来行代码,不影响业务逻辑,非常适合学习和时使用,欢迎 star~

五、总结和说明

以上就是自定义 dragover 效果的完整实现了,不算复杂,但也有一些小技巧,特别是借助了 CSS 的能力。其实在这一版实现之前,我还尝试过很多别的实现,但都不如这种方式简洁明了,下面总结一下:

  1. 为了更好的体验,可以在拖拽过程中给与用户适当的变化提示
  2. 主要实现方法在于 dragenter 和 dragleave
  3. 当放置目标存在子元素时,也会触发 dragleave 事件,干扰原有逻辑
  4. 可以移除 dragleave 去除子元素的干扰,dragenter 需要先移除再添加 over
  5. 通过 CSS pointer-events 可以去除子元素的干扰
  6. 如果有多层可放置结构,可以通过 :not 过滤可放置目标
  7. 通过鼠标指针也可以改善交互体验
  8. 在 DOM 操作中千万不要忘记了 CSS,这点很重要

当然,拖拽在页面中的交互细节还有很多,比如拖拽排序过程中的挤压动画效果,后面有空再研究吧,争取出一个通用的解决方案。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发❤❤❤

欢迎关注微信公众号: 前端侦探

巧妙使用Vue.extend继承组件实现el-table双击可编辑(不使用v-if和v-else)

Posted: 12 Jun 2022 07:18 PM PDT

问题描述

有一个简单的表格,产品要求实现双击可编辑

看了一下网上的帖子,大多数都是搞两部分dom,一块是输入框,用于编辑状态填写另一块是普通标签,用于在不编辑显示状态下呈现单元格文字内容。再加上一个flag标识搭配v-if和v-else去控制编辑状态、还是显示状态。大致代码如下:

  <el-table-column     align="center"     label="姓名"   >     <template slot-scope="scope">       <!--isClick就是标识状态,状态处于编辑时候,显示输入框,状态属于呈现状态就显示文本内容-->       <el-input v-if="scope.row.isClick" v-model="scope.row.name"  @blur="blurFn(scope.row)"></el-input>       <span @click="clickCell(scope.row)" v-else>{{scope.row.name}}</span>     </template>   </el-table-column>

这种方式有其适用场景,但是得每个el-table-column列中都加上el-input和span以及v-if和v-else。我们尝试一下动态添加el-input,就是点击那个单元格,给那个单元格添加el-input让其处于可编辑状态,然后适时移除即可。这样的话,很多列的时候,就不用加很多个v-if和v-else啦。我们先看一下效果图

效果图

代码思路

  • 第1步:给el-table绑定双击事件 @cell-dblclick='dblclick',再双击事件的回调函数中,可以得知点击的是哪一行、那一列、那个单元格dom,以及点击事件。dblclick(row, column, cell, event) {...},这个是饿了么官方提供的,没啥好说的
  • 第2步:重点来喽

    • 第2.1步:单元格双击事件以后,我们首先创建一个el-input标签,然后把点击的这个单元格的值,作为参数props让这个el-input接收,这样的话el-input就会显示这个单元格的值了,就可以编辑了。问题一:如何创建一个el-input标签? ,客官稍等,下方会解答
    • 第2.2步:把创建好的el-input标签替换掉原来的单元格span标签,这样的话,就可以看到单元格变成了可输入的输入框了。问题二:如何把新创建的el-input标签,替换原有的span标签 ,客官稍等,下方会解答
    • 第2.3步,当用户编辑完了点击别处时候,即输入框失去焦点的时候,再把el-input输入框标签移除掉,恢复默认的span标签(当然失去焦点的时候,就要发请求修改数据了)问题三:如何移除el-input标签,并恢复原有的span标签,客官稍等,下方会解答
  • 这样的话,每次双击搞一个input标签用于修改,每次改完了失去焦点,就恢复默认单元格展示状态了,功能就实现了

代码思路中的三个问题解答

问题一:如何创建一个el-input标签?

我们知道,如果是创建原生的input标签并指定一个值,比较简单,直接:

let input = document.createElement('input') // 创建一个input标签 input.value = '孙悟空' // 给input标签赋值 document.body.appendChild(input) // 把input标签追加到文档body中

不过el-input标签不能通过上述方式创建,因为document.createElement()方法虽然可以创建出来el-input标签,但是dom并不认识这个el-input标签,所以页面没有变化。毕竟饿了么的el-input也是把input标签做一个二次封装的

所以,这里我们可以使用Vue.extend()方法去继承一个组件并暴露出去,而继承的这个组件中又有一个input标签,所以那个需要使用,那里就可以引入并new出来一个el-input了。关于Vue.extend()的定义啥的,这里不赘述,详情看官方文档。笔者之前也写过一篇Vue.extend文章,传送门:https://segmentfault.com/a/11...

首先搞一个.vue文件,用于继承

// input.vue文件 <template>   <div class="cell">     <el-input       ref="elInputRef"       size="mini"       v-model.trim="cellValue"     ></el-input>   </div> </template>  props: {     cellValue: {       type: String | Number,       default: "",     }, }

然后定义一个data.js文件,继承input.vue文件,并暴露

// data.js import Vue from "vue"; import definedInput from "./input.vue"; // vue继承这个input组件,就相当于一个构造函数了 const inputC = Vue.extend(definedInput); // 暴露出去,哪里需要哪里引入 export default {     inputC, }

页面中引入并使用

// page.vue import extendComponents from "./threeC/data"; // 1. 引入  new extendComponents.inputC({ // 2. 实例化     propsData: {       // 使用propsData对象传递参数,子组件在props中可以接收到       cellValue: cellValue, // 传递单元格的值     },   }).$mount(cell.children[0]);// 3. 挂载
propsData对象用于给继承的组件传递参数,也可以传递一个函数,从而继承组件通过这个函数通知外部使用组件,详情见后续完整代码

问题二三:el-input标签和span标签的来回替换恢复

使用$mount方法去做来回替换,$mount可以把一个子dom元素追加到父dom元素内部,相当于appendChild

然后这里需要有一个替换的时机,就是实例化的组件中的el-input失去焦点的时候,去通知外部使用的组件,所以可以在外部使用是,在propsData中传递一个函数到继承的组件,如:

// 外部组件传递 new extendComponents.inputC({     propsData: {       cellValue: cellValue, // 传递单元格的值       saveRowData: this.saveRowData, // 传递回调函数用于通知,继承组件中可以触发之     }, }).$mount(cell.children[0]);   saveRowData(params){     console.log('收到继承组件消息通知啦参数为:',params) }
// 内部组件失去焦点时候通知 <el-input   ref="elInputRef"   size="mini"   v-model.trim="cellValue"   @blur="blurFn" ></el-input>  props: {     cellValue: {       type: String | Number,       default: "",     },     saveRowData: Function, // 外部,传递进来一个函数,当这个el-input失去焦点的时候,通过此函数通知外部 }  blurFn() {   // 失去焦点,再抛出去,通知外部   this.saveRowData({     cellValue: this.cellValue,     // 其他参数   }); },

所以当内层失去焦点的时候,就可以通知外层去做一个替换了,就是把单元格dom重新做一个$mount挂载,就把el-input替换成了span了,为了进一步理解,这里的span我们也可以使用继承的方式,是new实例化使用,详情见下方完整代码

完整代码

目录结构

threeC -- data.js -- input.vue -- span.vue three.vue

用于继承的el-input组件

input.vue

<template>   <div class="cell">     <el-input       ref="elInputRef"       size="mini"       v-model.trim="cellValue"       @blur="blurFn"     ></el-input>   </div> </template>  <script> export default {   props: {     cellValue: {       type: String | Number,       default: "",     },     saveRowData: Function, // 外部,传递进来一个函数,当这个el-input失去焦点的时候,通过此函数通知外部     cellDom: Node, // 单元格dom     row: Object, // 单元格所在行数据     property: String, // 单元格的key   },   mounted() {     // 用户双击后,让其处于获取焦点的状态     this.$refs.elInputRef.focus();   },   methods: {     blurFn() {       // 失去焦点,再抛出去,通知外部       this.saveRowData({         cellValue: this.cellValue,         cellDom: this.cellDom,         row: this.row,         property: this.property,       });     },   }, }; </script>  <style> .cell {   width: 100%;   height: 100%;   display: flex;   justify-content: center;   align-items: center;   box-sizing: border-box;   padding: 0 8px; } </style>

用于继承的span组件

span.vue

<template>   <span class="cell">{{ cellValue }}</span> </template>  <script> export default {   props: {     cellValue: {       type: String | Number,       default: "",     },   }, }; </script>

统一继承并暴露data.js文件

import Vue from "vue"; import definedInput from "./input.vue"; import definedSpan from "./span.vue";  const inputC = Vue.extend(definedInput); const spanC = Vue.extend(definedSpan);  export default {     inputC,     spanC, }

使用继承的three.vue组件

<template>   <div id="app">     <el-table       @cell-dblclick="dblclick"       :cell-class-name="cellClassName"       height="480"       :data="tableData"       border     >       <el-table-column align="center" type="index" label="序号" width="50">       </el-table-column>       <el-table-column align="center" prop="name" label="姓名" width="100">       </el-table-column>       <el-table-column align="center" prop="age" label="年龄" width="100">       </el-table-column>       <el-table-column align="center" prop="home" label="家乡">       </el-table-column>     </el-table>   </div> </template>  <script> // 引入继承组件对象,可取其身上的inputC构造函数、或spanC构造函数生成组件dom import extendComponents from "./threeC/data"; export default {   data() {     return {       tableData: [         {           name: "孙悟空",           age: 500,           home: "花果山水帘洞",         },         {           name: "猪八戒",           age: 88,           home: "高老庄",         },         {           name: "沙和尚",           age: 1000,           home: "通天河",         },       ],       /**        * 存一份旧的值,用于校验是否发生变化,是否修改        * */       oldCellValue: null,     };   },   methods: {     cellClassName({ row, column, rowIndex, columnIndex }) {       row.index = rowIndex; // 自定义指定一个索引,下方能够用到     },     dblclick(row, column, cell, event) {       // 1. 序号列单元格不允许编辑,别的列单元格可以编辑       if (column.label == "序号") {         this.$message({           type: "warning",           message: "序号列不允许编辑",         });         return;       }       // 2. 存一份旧的单元格的值       this.oldCellValue = row[column.property];       // 3. 然后把单元格的值,作为参数传递给实例化的input组件       let cellValue = row[column.property];       // 4. 实例化组件以后,带着参数,再挂载到对应位置       new extendComponents.inputC({         propsData: {           // 使用propsData对象传递参数,子组件在props中可以接收到           cellValue: cellValue, // 传递单元格的值           saveRowData: this.saveRowData, // 传递回调函数用于保存行数据,组件中可以触发之           cellDom: cell, // 传递这个dom元素           row: row, // 传递双击的行的数据           property: column.property, // 传递双击的是哪个字段         },       }).$mount(cell.children[0]); // 5. $mount方法,用于将某个dom挂载到某个dom上     },     /**      * 失去焦点的时候有以下操作      *    1. 校验新值是否等于原有值,若等于,说明用户未修改,就不发请求。若不等于就发请求,然后更新tableData数据      *    2. 然后使用$mount方法,挂载一个新的span标签dom在页面上,即恢复原样,而span标签也是实例化的哦      * */     saveRowData(params) {       console.log("继承的子组件传递过来的数据", params);       // 1. 看看用户是否修改了       if (params.cellValue == this.oldCellValue) {         console.log("未修改数据,不用发请求");       } else {         params.row[params.property] = params.cellValue;         // 这里模拟一下发了请求,得到最新表体数据以后,更新tableData         setTimeout(() => {           //        给那个数组的     第几项            修改为什么值           this.$set(this.tableData, params.row.index, params.row);         }, 300);       }       // 2. 恢复dom节点成为原来的样子,有下面两种方式        /**        * 方式一:使用官方推荐的$mount去挂载到某个节点上,上方也是        * */       new extendComponents.spanC({         propsData: {           cellValue: params.cellValue,         },       }).$mount(params.cellDom.children[0]);        /**        * 方式二:使用原生js去清空原节点内容,同时再添加子元素        * */       // let span = document.createElement("span"); // 创建一个span标签       // span.innerHTML = params.cellValue; // 指定span标签的内容的值       // span.classList.add("cell"); // 给span标签添加class为cell       // params.cellDom.innerHTML = ""; // 清空刚操作的input标签的内容       // params.cellDom.appendChild(span); // 再把span标签给追加上去,恢复原样     },   }, }; </script>  <style lang="less" scoped> #app {   width: 100%;   height: 100vh;   box-sizing: border-box;   padding: 50px; } </style>

总结

使用Vue.extend()方法,可以继承一些组件,甚至继承一些复杂的组件,在实际业务场景中会有巧妙的使用。具体业务场景具体分析。

此外,上述代码中是el-input的继承,其实,我们也可以做el-select的继承,思路和上方类似,这样就可以在表格中双击单元格,选择并更改对应的下拉框更改el-table的单元值了,比如如果有性别这一列,那是下拉框的形式的。道友们可以按照这个思路发散哦...

好记性不如烂笔头,记录一下吧 ^_^

No comments:

Post a Comment