Tech
导读
组件化渗透在开发的方方面面,本文主要从“为什么”、“是什么”、“怎样做”三方面来系统讲述前端组件化的相关知识。通过阅读本文,读者可以对组件化形成一个更加深入的认识和理解。在实现前端组件化的过程,大家可以更加关注组件化的目的,需求及其特点,了解前提才能结合项目来思考如何更好的拆分和实现组件化。
01
思考:为什么要实施前端组件化?
在项目开发中,页面和功能大都拆分为多文件来实现,多文件管理逐渐暴露出以下问题:
1.相似的业务代码无法复用:X同事实现了一遍A页面,Y同事要实现一个和A页面类似的B页面,发现X同事的代码无法有效复用,只好重新再写一遍。
2.多人重复实现同一功能:X同事完成了A功能,Y同事开发时要做同样的功能,但是并不知道X同事已经实现了,又重新写了一遍。
随着项目的不断迭代,以上问题便会导致:
1.代码体积不断增加,冗余越来越大;
2.业务逻辑复杂度不断增加,逻辑的可扩展性、可维护性、健壮性越来越差;
而产生以上问题的原因主要体现在:
1.相似代码重复开发;
2.复用功能代码的方式是简单粗暴的复制粘贴;
3.团队内协作开发导致代码耦合度高,后期维护难;
随着项目的迭代,从长期维护的稳定性和可操作性方面来看,大多数人都想过无数次重构和优化,却总是不敢轻易“动”。所以在前端项目工程化的前提下,引入了前端组件化,从功能模块的复用及多人协作层面进行解耦。
02
组件化:前端解耦的有效利器
2.1 什么是前端组件化?
前端的组件化,其实是对项目进行自上而下的拆分,把通用的、可复用的功能中的模型(Model)、视图(View)和视图模型(ViewModel)以黑盒的形式封装到一个组件中,然后暴露一些开箱即用的函数和属性配置供外部组件调用,实现与业务逻辑的解耦,来达到代码间的高内聚、低耦合,实现功能模块的可配置、可复用、可扩展。除此之外,还可以再由这些组件组合更复杂的组件、页面。
上面提到了要对项目进行拆分,那经常听到的模块化与组件化都涉及了对项目的模块拆分,指的是一回事吗?
组件化≠模块化。模块化是从文件层面上,对代码或资源进行拆分;而组件化是从设计层面上,对用户界面进行拆分。前端组件化更偏向UI层面,更多把逻辑放到页面中,使得UI元素复用性更高。
换句话说,页面上所有的东西都可以是组件,可以把页面看作大型业务组件,它又能拆分为多个中型业务组件,然后可以再拆分成多个复合组件,复合组件再拆成多个基础组件,直到拆成Dom元素为止。实际项目开发中,只需要应用这些组件,像搭积木一样完成页面的搭建就可以了。
图1 页面与组件间的关系
2.2 组件化思维
组件化思维的精髓是独立、完整、自由组合。以此为目标,尽可能把设计和开发中的元素独立化,使其具备完整的功能,通过自由组合来构成整个页面/产品。
2.3 组件分类
一般常见的组件可以划分为这四种:基础组件、业务组件、区块、页面。
图2 组件分类与关系图
2.4 组件化的特点
网上对于组件化并没有一个明确的定义,但是在提到组件化的时候都会提到高内聚、低耦合。也就是希望每个组件对内做到各个元素紧密结合,互相依赖;对外和其他组件的联系最少且接口简单,可复用可组合。组件化的意义在于提效,希望可以交付可用的、直观的、可组合的业务形态。
如果项目实现了组件化,那么写代码就具有了更高的灵活性,可扩展性和可维护性。
图3 组件化特点
2.5 组件化开发后的新面貌
组件化以后,一个页面可能是这个样子的:
图4 页面与组件的构成
项目可能是这个样子的:项目内部对应多个页面,页面内部对应多个组件,组件内部又可能对应了多个不同的库。
图5 项目、页面与组件的关系分布
图6 Elsa-组件库列表页面 业务组件的开发离不开基础组件,Elsa也提供了可视化拼装表单功能,提供表单页面的基础组件元素,通过拖拽轻松完成表单页面的搭建。
图7 Elsa-拼装组件页面 并且针对不同的业务场景和页面模式,该插件还提供了多版项目模板。开发人员可以直接使用现有项目模板进行开发,极大地降低了项目研发和维护的成本。
图8 Elsa-创建应用页面 关于Elsa中目前沉淀出的业务组件是从哪里来?又是如何从项目拆分、设计的?目前Elsa列表中提供的业务组件是从产品中心中抽取封装发布的,在重构中提供了极大的便利,团队别的同事可以很方便的下载使用。所以下文组件拆分就以产品中心为例说明。 3.3 组件拆分 面对项目中一个功能几百行上千行,都堆积在一个js文件中,尤其在刚接手别人项目得时候遇到这种情形会极其崩溃。这个现象就像盖房子时,地基不稳,还在上面一直摞砖,这样的房子试问有人敢住吗?那同样的,这样的代码运行起来随时都可能会出现意想不到的问题,可能下一秒出现问题,可能增加了某个功能后出现问题......OMG,项目要上线,BUG改不完,问题还没有定位到,怕是要完蛋了...... 目前业内对于组件拆分也没有统一的标准。因为每个人对组件化的理解不同,拆分边界、程度都要结合具体的业务场景来思考。拆分思路也可以借鉴Vue官网的一个图来说明:
图9 vue中组件系统的划分 中台技术部的前端团队在拆分组件时,除按照布局和人员分工拆分之外,主要从这两个角度来考虑的:
重构项目前先做了项目整体业务逻辑和代码架构的梳理,制定出组件化方案。对比以下几个页面: 页面A:
图10 产品中心页面A 页面B:
图11 产品中心页面B 页面C:
图12 产品中心页面C 现象描述
拆分思路 将每个页面都划分成过滤条件组件、表格组件和分页组件来实现组件间的独立、再组合完成复用。 封装设计
业务发展前期,可能这样抽取的组件通用性很强了。当日积月累业务中增加了新的需求,同一项目中的不同页面中类似的模块又有别的差异无法用现有的组件逻辑来满足,需要不断新增参数。此时组件内部出现了大量的判断逻辑。尽管组件的通用性再好,可以应对各种页面的逻辑,但此时组件本身已经变得更难以维护。这个时候就应该思考:抽离组件应该做业务层和视图层的分离,视图层负责页面的样式和交互,业务层处理业务逻辑(接口调用、数据结构调整等)。这种情形在常见的新增、编辑页面,就可以避免组件中的耦合判断,共用一个视图,并在各自的业务层实现不同的业务逻辑。 table组件主要部分:
<el-table ref="multipleTable" v-loading="loading" :stripe="ifStripe" :border="border" :height="`calc(100% - ${pageHeight}px)`" :max-height="maxHeight" :style="{ width: '100%' }" :data="dataSource" v-bind="options" v-on="tableEvents" @selection-change="handleSelectionChange" > <!-- 复选框 --> <el-table-column v-if="options && options.selection && (!options.isShow || options.isShow())" type="selection" width="55" :align="alignDirestion" ></el-table-column> <el-table-column v-if="operates && operates.length > 0" :fixed="operatesDir" label="操作" width="200" v-bind="options && options.props" :align="alignDirestion" > <template slot-scope="scope"> <div class="operate-group"> <template v-for="(btn, key) in operatesOut"> <span v-if="!btn.isShow || (btn.isShow && btn.isShow(scope.row, scope.$index))" :key="key"> <template v-if="btn.render"> <render :column="scope.row" :row="scope.row" :render="btn.render" :index="scope.$index"></render> </template> <template v-if="!btn.render"> <!-- v-show="!btn.disabled || (btn.show && btn.show(scope.row, scope.$index))" --> <el-button :size="btn.size || 'small'" :type="btn.type || `text`" :icon="btn.icon" :plain="btn.plain" :style="btn.setStyle && btn.setStyle(scope.row, scope.$index)" v-bind="btn.props" :disabled="btn.disabled && btn.disabled(scope.row, scope.$index)" @click.native.prevent="btn.method(scope.row, scope.$index)" > {{ typeof btn.label === 'string' ? btn.label : btn.label(scope.row) }}{{ operates.length >= 2 ? ' ' : '' }} </el-button> </template> </span> </template> <template> <span v-if="operatesInner && operatesInner.length > 0"> <el-dropdown class="dropdown-btn"> <span class="el-dropdown-link"> 更多 <i class="el-icon-arrow-down el-icon--right"></i> </span> <el-dropdown-menu> <!-- @click="arrange(scope.row)" command="arrange" @click="getLog(scope.row)" command="getLog" --> <el-dropdown-item :disabled="btn.disabled && btn.disabled(scope.row, scope.$index)" v-for="(btn, key) in operatesInner" :key="key" @click.native.prevent="btn.method(scope.row, scope.$index)" > {{ btn.label }} </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </span> </template> </div> </template> </el-table-column> <!-- 表格数据 --> <template v-for="(column, index) in columns"> <slot v-if="column.slot" :name="column.slot"></slot> <!-- :align="column.align" 表头字体样式无需自定义直接居中即可 --> <template v-else> <el-table-column v-if="!column.isShow || (column.isShow && column.isShow())" :key="index" v-bind="column.props" :prop="column.prop" :label="column.label" :width="column.width" :fixed="column.fixed" show-overflow-tooltip :align="alignDirestion" > <!-- 每一个值 --> <template slot-scope="scope"> <template v-if="!column.render"> <template v-if="column.formatter"> <span v-if="!column.time" @click="column.click && column.click(scope.row, scope.$index)"> {{ column.formatter(scope.row, column, scope.$index) }} </span> <span v-if="column.time"> {{ column.formatter(scope.row, column, scope.$index) | dateFormat }} </span> </template> <template v-else-if="column.newjump"> <router-link class="newjump" type="primary" :underline="false" v-bind="{ target: '_blank', ...column.target }" :to="column.newjump(scope.row, column, scope.$index)" > {{ scope.row[column.prop] || column.content }} </router-link> </template> <template v-else> <span :style="column.click ? 'color: #409EFF; cursor: pointer;' : null" @click="column.click && column.click(scope.row, scope.$index)" > {{ scope.row[column.prop] || column.content ? scope.row[column.prop] || column.content : '-' }} {{ `${scope.row[column.prop] && column.unit ? column.unit : ''}` }} </span> </template> </template> <template v-else> <render :column="column" :row="scope.row" :render="column.render" :index="index"></render> </template> </template> </el-table-column> </template> </template> <!-- slot插槽按钮 --> <el-table-column v-if="options && options.slotcontent" label="操作" :align="alignDirestion"> <template slot-scope="scope"> <slot :data="scope"></slot> </template> </el-table-column> </el- 页面调用:
<table-list alignDirestion="left" :operates="operates(this)" operatesDir="right" :pageHeight="48" :export-but="exportBut(this)" class="table-list" :columns="columns" :data-source="tableData" :isDoLayout="true" ></table-list> 3.4 组件化面临的挑战
随着前端领域的发展和开发探索,组件化思想已经在前端开发中广泛使用,比如流行的Vue、React框架等,都是基于组件化思想的产物。组件化并非一蹴而就,而是一个持续的过程。在沉淀业务组件的同时还需考虑组件包的大小,不能因为组件包的体积大而导致页面加载过慢,以及组件发布前的测试等。挑战也是一直存在的,但可以通过一些方法和规范去解决挑战,让组件化设计更好的服务于系统。所以,理解组件化可以帮助开发者更好的使用框架进行工作内容的拆分和维护,才能在实际开发中结合具体的业务场景,设计出合理的组件,实现真正的前端组件化。
推荐阅读