浅谈Vue组件在实际项目中的应用

avatar
UX @京东
原文链接: jdc.jd.com

Vue.js 是一套构建用户界面的渐进式框架,目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。——摘自 Vue 官网。

虽然目前 Vue 已经很火了,但不可否认的是,仍有很多人刚刚开始学习使用 Vue 来构建前端项目,从生疏的初学者到熟练运用 Vue 的过程中,不可避免地会走一些弯路。为了实现某个功能,也许尝试过很多方法,最终蓦然回首,才发现当初犯下的错误是那么幼稚。然而,这些错误也正是通往成功道路上的奠基石,因此,总结这个过程很有必要。本文基于参与过的项目——绩效管理与人才库项目,总结了在实际项目中使用 Vue 组件逐渐完善的过程,旨在抛砖引玉,共同学习。

合理的使用 Vue 子组件

初次使用 Vue 组件是在绩效管理项目中,由于当时对于父、子组件只是停留在基础概念上,在实际项目中没有合理构建父、子组件结构。首先看一下需要完成的页面效果:

4

图中最外层是二级部门,从外到里,一直嵌套到五级部门(为了更好的展示层级效果,这里把每级部门中的人员列表省略了)。当时分析每一级部门都是类似的,于是把每一层级部门作为一个子组件来构建,正如下图所示:左图为搭建的 Vue 组件结构图,右图为代码结构:

90

相信看到这里,各位看官已经发现了问题所在:二级部门子组件嵌套了三级部门的子组件,同时三级部门子组件嵌套了四级部门子组件,以此类推。这样导致子组件嵌套层数过多,父子组件之间、兄弟组件之间通信繁琐,并且也失去了组件的意义——即组件是可以扩展 HTML 元素,用来封装可重用的代码。

接下来,我们分析这样搭建组件带来的困难:也就是父子组件之间、兄弟组件之间通信繁琐的问题。因为很快我就遇到了这样一个需求,如下图所示:

693756-20170718174046177-110591891-690x457

每一级部门都会有人员信息,此时如果点击第五级部门中的“绩效评价”按钮,需要出现“评价与评级”的弹窗(右图),然而弹窗组件是放在最外层 Vue 实例中的,与二级部门子组件形成并列关系,其 Vue 组件结构如下图所示:

92

这样一来,五级部门子组件中点击按钮要想触发弹窗组件,需要层层监听被触发的函数,直至传到根实例 Vue 中,再分配到弹窗组件中(注,这里采用的是父子组件通信方式 $emit ,因为尽可能的不要让子组件修改父组件中的数据,所以没有采用其他通信方式,例如直接修改父组件数据或者父组件的数据公共化等方法)。

根据 Vue 知识,父组件向子组件中使用 props 传递属性值,子组件向父组件中传递数据使用 $emit(eventName) 触发事件,父组件使用 $on(eventName) 监听事件。这样说可能有些糊涂,请看图解:

TimLine图片20170715144322

从图中可以看出,要想从五级部门和父组件中进行通信,需要层层上传数据,每一层通信函数都要在 HTML 和 JavaScript 中触发、监听, 何其繁琐!尤其务必注意的是,命名的规范问题,由于 HTML  属性会忽略大小写,父子间通信定义的函数名称可以使用中划线命名,但不能使用驼峰法定义,否则无法正常对通信函数触发和监听!笔者初期就因为在 JavaScript 中习惯使用驼峰法命名函数,结果在 HTML 中使用了驼峰法定义监听函数,导致无法监听到该事件,花费了很多时间来排除错误。 然而塞翁失马焉知非福,在解决父子组件通信的过程中,逐渐加深了对父子之间函数通信的理解。

优化方法

上面介绍了这么多在初次构建 Vue 组件时走过的“弯路”,那么如何去优化呢?我们再来看最开始要完成的页面:

2

上图为每个层级部门展开后的页面,从图中可以看出每个层级部门的页面很类似,要知道组件最主要的是用来封装可“重用”的代码!很明显标红色区域或者标蓝色区域都可以重复使用,为了最大限度的使用组件,避免重复,这里我们使用红色区域为子组件,HTML 结构为:

<!--2级部门--> <div class="department"> <people-part v-bind:info="departmentInfo" v-bind:num="0"></people-part> <!--3级部门--> <template v-for="(list,index) in departmentInfo.childList"> <people-part v-bind:info="list" v-bind:num="1"></people-part> <!--4级部门--> <template v-for="(list2,index2) in list.childList"> <people-part v-bind:info="list2" v-bind:num="1"></people-part> <!--5级部门--> <template v-for="(list3,index3) in list2.childList"> <people-part v-bind:info="list3" v-bind:num="1"></people-part> </template> </template> </template> </div>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!--2级部门--> <div class="department">     <people-part v-bind:info="departmentInfo" v-bind:num= "0"></people- part >     <!--3级部门-- >     <template v- for ="(list,index) in departmentInfo.childList">             <people- part v-bind:info ="list" v-bind: num="1" ></people -part>             <!-- 4级部门 -->               <template v -for="(list2,index2) in list.childList">                     <people - part v-bind: info= "list2"  v-bind :num= "1"></ people-part>                     <! -- 5级部门-->                     <template v-for="(list3,index3) in list2.childList" >                              <people -part v- bind:info ="list3"  v -bind:num ="1"> </people-part >                     </ template >             </template >     </template>   </div>

这样,我们只需要写一个子组件:

<script type="text/x-template" id="people-part-template"> <div> <!--子组件内容--> </div> </script>
1 2 3 4 5 <script type="text/x-template" id ="people-part-template">     <div>         <!--子组件内容-- >     </div> </script>

Vue 组件结构如下:

TimLine图片20170715153611

可以看出,部门内部的组件和弹窗均放在了根实例 Vue 下面,每级部门层级搭建好后,只需调用“部门内部子组件”。这样一来,部门内部子组件和弹窗组件之间的通信都化简了很多,部门内部子组件也可以方便的使用根实例 Vue 中的数据!

使用 slot 优化组件层级

在实际项目中,应尽量避免使用多层组件嵌套,但有的时候组件确实需要嵌套多层才能实现所需的功能,那么还能继续优化吗?我们再来看一个使用子组件的例子,在人才库项目中,多个页面出现了弹窗,有的同一个页面有多个弹窗,弹窗的样式如下图所示:

693756-20170715155526087-1334117881

分析这些弹窗的特点,发现有共同的区域,比如说有公共的头部,公共的边框,还有类似的按钮。但同时又存在差异的地方,弹出主体不同,大小不同,有的弹窗下部按钮数量不同。为了减少重复的开发,可以考虑将弹窗中通用的样式封装成一个子组件,弹窗剩下主体部分再具体开发。

按照上述思路,首先,编辑弹窗外层子组件:

<script type="text/x-template" id="dialog-box-template"> <div class="dialog-wrap" v-if="showDialog"> <div class="dialog-is-distribute" :style="dialogStyle"> <div class="title"><b :class="showIcon"></b>{{data.title}}<i class="close" v-on:click="closedialog()"></i></div> <dialog-add v-if="status==0"></dialog-add> <dialog-delete v-else-if="status==1"></dialog-delete> <dialog-tab v-else-if="status==2"></dialog-tab> <dialog-change v-else-if="status==3"></dialog-change> </div> </div> </script>
1 2 3 4 5 6 7 8 9 10 11 <script type="text/x-template" id ="dialog-box-template">     <div class="dialog-wrap" v-if="showDialog">         <div class= "dialog-is-distribute" :style="dialogStyle" >             <div class = "title"><b : class= "showIcon"></b>{{data.title}}<i class="close" v-on:click="closedialog()"></i > </div>             <dialog- add   v-if="status==0">< / dialog-add>             <dialog- delete v-else-if="status==1">< / dialog-delete>             <dialog- tab   v-else-if="status==2"> </ dialog-tab>             <dialog- change   v-else-if="status==3"> </ dialog-change>         </div>     </div> </script>

HTML 中定义组件代码为:

<dialog-box :show="isDialogShow" :type="dialogType" :data="dialogData" @close="closeDialog"></dialog-box>
1 <dialog-box :show ="isDialogShow" :type= "dialogType" :data="dialogData"   @ close="closeDialog"></ dialog -box>

上述代码定义了子组件为 <dialog-box> ,定义了 title 部分和外边框,内部不同部分嵌套不同的子组件来渲染,如 <dialog-add> 子组件、<dialog-delete> 等子组件渲染不同的主体部分,其结构见下图:

图9:Vue组件结构图

最后使用内置组件 <component> ,渲染一个“元组件”为动态组件,依据 is 的值,来决定哪个组件被渲染,从而进一步优化上述代码,其判断逻辑放在 JavaScript 中:

<script type="text/x-template" id="dialog-box-template"> <transition name="fade"> <div class="dialog-wrap" v-if="showDialog"> <div class="dialog-is-distribute" :style="dialogStyle"> <div class="title"><b :class="showIcon"></b>{{data.title}}<i class="close" v-on:click="closedialog()"></i></div> <component :style="contentStyle" :is="type" :data="data.params" @close="closedialog" @action="action"></component> </div> </div> </transition> </script>
1 2 3 4 5 6 7 8 9 10 <script type="text/x-template" id ="dialog-box-template">     <transition name="fade" >         <div class= "dialog-wrap" v-if="showDialog">             <div class = "dialog-is-distribute" :style="dialogStyle" >                 <div class ="title"><b : class ="showIcon"></b>{{data.title}}<i class="close" v-on:click="closedialog()"></i ></div>                 <component :style="contentStyle"  : is ="type" :data= "data.params" @close="closedialog" @ action="action"></ component >               </div>         </div>     </transition> </script>

好了,现在实现了使用公共的弹窗部分,可以根据需求,通过开发不同的子组件来构建弹窗的主体等差异化部分。此时,我们来分析一下上述做法的缺点:

  1. 父子组件嵌套过多,增加了父子组件间通信的复杂度,弹窗公共部分 <dialog-box> 作为子组件,还嵌套着主体部分子组件才能实现弹窗主体部分差异化;
  2. 很多主体子组件部分的逻辑函数,例如点击“确定”、“取消”等按钮需要关闭弹窗,相同的功能,却要每个弹窗主体部分子组件中都要写一遍,并且还需要触发弹窗公共部分 <dialog-box> 的函数,然后才能触发到父组件中关闭弹窗的命令; 693756-20170718154537146-892772573
  3. 从父组件往弹窗主体部分传递数据复杂,例如在父组件中,点击按钮后,触发 Ajax 请求,要想把处理后得到的数据,传递到主体部分的子组件中,首先需要父组件先将数据传递到弹窗外层组件中,然后才能使用 props(如下图定义的 props 参数 data )传递到内部主体子组件中。

55

综上所述,弹窗外层与主体间由于嵌套子组件,导致代码重复、父子组件通信复杂度增加。那么,说了这么多,有优化的方法吗?

优化方法

这时,Vue 中的 slot 分发机制登场了!什么是 slot 分发机制呢?官方定义:“为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个处理称为内容分发,Vue.js 实现了一个内容分发 API,使用特殊的 <slot> 元素作为原始内容的插槽 ”,说简单一些,其实 slot 相当于子组件中的占位符,如果父组件中有内容,则覆盖该占位符,否则显示该占位符内容。

693756-20170718161058708-1692428378

于是,根据上面的思想,我们可以重新定义弹窗组件,这次弹窗主体作为一个子组件,内部使用 slot 分别占位标题部分、主体 body 部分、底部按钮 btn 部分,如果父组件中没有定义该 slot ,则显示默认的子组件,否则渲染父组件定义的 slot 部分,其示意图如下:

2

自定义弹窗主体元素代码为:

<dialog-pox v-show="isDialogShow==1" v-on:closedia="closeDialog()" :configure="nature"> <h2 slot="title">删除项</h2> <div class="eval-content dialog-delete" slot="body"> <p class="delete-content">确认删除{{dataInfo.name}}吗?</p> </div> </dialog-pox> <dialog-pox v-show="isDialogShow==2" v-on:closedia="closeDialog()" :configure="nature"> <h2 slot="title">添加项</h2> <h1 slot="body">我是{{operation.doner}}-{{operation.deleter}}</h1> <div slot="btn"></div> </dialog-pox>
1 2 3 4 5 6 7 8 9 10 11 12 <dialog-pox v- show="isDialogShow==1" v- on :closedia="closeDialog()" : configure ="nature">         <h2 slot= "title" >删除项</h2>         <div class= "eval-content dialog-delete" slot="body">             <p class ="delete-content">确认删除{{dataInfo . name}}吗?</p >         </div> </dialog-pox>   <dialog-pox v- show="isDialogShow==2" v- on: closedia="closeDialog()" : configure= "nature">         <h2 slot= "title" >添加项</h2>         <h1 slot= "body" >我是{{operation.doner }} -{{operation. deleter}} </h1>         <div slot= "btn" ></div> </dialog-pox>

在页面中定义子组件代码:

<script type="text/x-template" id="dialog-pox-template"> <div class="dialog-wrap"> <div class="dialog-is-distribute" :style="{width:configure.width}"> <div class="title"><b></b> <slot name="title">标题</slot> <i class="close" @click="close()"></i></div> <slot name="body"></slot> <slot name="btn"> <div class="btn-part"> <button class="btn-add" @click="close()">确认</button> <button class="btn-cancel" @click="close()">取消</button> </div> </slot> </div> </div> </script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script type="text/x-template" id ="dialog-pox-template">     <div class="dialog-wrap" >         <div class= "dialog-is-distribute" :style="{width:configure.width}" >             <div class= "title"><b></b>             <slot name="title">标题</slot>             <i class ="close" @click="close()" > </i></div>             <slot name="body"></slot>             <slot name= "btn">                 <div class ="btn-part">                     <button class="btn-add" @click= "close()" >确认</button>                     <button class="btn-cancel" @click= "close()" >取消</button>                 </div>             </slot>         </div>     </div> </script>

引入 CSS 样式后,效果如下图所示:

693756-20170718163814599-1632372400

弹窗组件中定义了三部分,如果要修改弹窗主体,则在<dialog-pox> 的 slot=body 中更换弹窗主体内容,如果不需要按钮部分,则在自定义元素中增加 <div slot=”btn”></div> ,替换子组件中 slot=btns 部分。此外,根据参数 configure 可以控制弹窗的宽度,颜色背景等属性。(还可以直接在<dialog-pox class=”newclass”>增加新的 className ,来生成自定义样式的弹窗)。

002

使用 slot 开发的优点有:

  1. 减少父子组件嵌套层数,只定义了弹窗主体子组件,其余部分在页面的 HTML 中定义;
  2. 弹窗主体可以直接使用父组件中 Ajax 返回的数据,例如 {{dataInfo.name}} 中的 dataInfo 就是父组件中的数据;
  3. 避免了重复定义函数,同样是关闭弹窗操作,只需执行 closeDialog() 函数即可,不必从子组件中层层触发父组件中函数;

总结:

综上所述,在实际项目应用中,为了实现一个效果,有可能走过很多弯路,最后回头去发现之前犯下的错误很是简单,甚至结论可以一语带过, 但是在这个过程中,也学习到了很多知识,甚至之所以有了这个过程,才会对知识的理解更加透彻,新的知识学起来有可能很快,真正用到项目中,却总是出现不可预期的错误,深刻体会到“纸上得来终觉浅,绝知此事要躬行” 。总之要不断的完善,总结,也许过不了多久,再次回顾发现目前的代码还能有优化的地方,这也正是我们成长必须要走的路程,愿与君共勉!