关于组件化 组件化思想并不是前端独有的,但却是前端技术的延伸 任何软件开发过程,或多或少都有那么一些组件化的需求。
1.组件化的特点
每个组件对应一个目录,组件所需的各种资源都在这个目录下就近维护;(最具软件工程价值) 页面上的每个独立的可视/可交互区域视为一个组件; 由于组件具有独立性,可以自由组合; 页面是组件的容器,负责组合组件形成功能完整的界面; 当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换 分子是由原子组成的,分子分成原子,原子也可以重新组合成新的分子 一个界面是由独立的分子组件搭建而成,分子组件由原子元件构成,这些原子可通过不同的组合方式,组成新分子组件,继而重组构成新的界面
组件设计的原则 组件化方案下,我们需要具有组件化设计思维,它是一种【整理术】帮助我们高效开发整合
标准性 任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件
独立性 描述了组件的细粒度,遵循单一职责原则,保持组件的纯粹性,属性配置等API对外开放,组件内部状态对外封闭,尽可能的少与业务耦合
复用与易用 UI差异,消化在组件内部(注意并不是写一堆if/else),输入输出友好,易用 。追求短小精悍,Single Point Of Truth法则,就是尽量不要重复代码,出自《The Art of Unix Programming》;
避免暴露组件内部实现,避免直接操作DOM,避免使用ref,使用父组件的 state 控制子组件的状态而不是直接通过 ref 操作子组件 。
入口处检查参数的有效性,出口处检查返回的正确性。
- 无环依赖原则(ADP) 4.1 环形依赖 组件间耦合度高,集成测试难 一处修改,处处影响,交付周期长 因为组件之间存在循环依赖,变成了“先有鸡还是先有蛋”的问题。
4.2 避免环形依赖 沿着逆向的依赖关系即可寻找到所有受影响的组件,创建一个共同依赖的新组件。
- 稳定抽象原则(SAP)
组件的抽象程度与其稳定程度成正比 一个稳定的组件应该是抽象的(逻辑无关的) 一个不稳定的组件应该是具体的(逻辑相关的) 为降低组件之间的耦合度,我们要针对抽象组件编程,而不是针对业务实现编程。
- 避免冗余状态
如果一个数据可以由另一个 state 变换得到,那么这个数据就不是一个 state,只需要写一个变换的处理函数,在 Vue 中可以使用计算属性 如果一个数据是固定的,不会变化的常量,那么这个数据就如同 HTML 固定的站点标题一样,写死或作为全局配置属性等,不属于 state 如果兄弟组件拥有相同的 state,那么这个state 应该放到更高的层级,使用 props 传递到两个组件中
合理的依赖关系 父组件不依赖子组件,删除某个子组件不会造成功能异常
扁平化参数 除了数据,避免复杂的对象,尽量只接收原始类型的值
良好的接口设计
把组件内部可以完成的工作做到极致,虽然提倡拥抱变化,但接口不是越多越好 如果常量变为 props 能应对更多的场景,那么就可以作为 props,原有的常量可作为默认值。 如果需要为了某一调用者编写大量特定需求的代码,那么可以考虑通过扩展等方式构建一个新的组件。 保证组件的属性和事件足够的给大多数的组件使用。 API尽量和已知概念保持一致
组件分类 组件最大的不稳定性来自于展现层,一个组件只做一件事,基于功能做好职责划分。
- 基础组件(ui组件库) antdelement-ui
- 容器型组件(业务角度,非代码角度) 一个容器性质的组件,一般当作一个业务子模块的入口,比如一个路由指向的组件 。
容器组件内的子组件通常具有业务或数据依赖关系 集中/统一的状态管理,向其他展示型/容器型组件提供数据(充当数据源)和行为逻辑处理(接收回调) 如果使用了全局状态管理,那么容器内部的业务组件可以自行调用全局状态处理业务 业务模块内子组件的通信等统筹处理,充当子级组件通信的状态中转站 模版基本都是子级组件的集合,很少包含DOM标签 辅助代码分离
<template> <div class=“purchase-box”> <!– 面包屑导航 –> <bread-crumbs /> <div class=“scroll-content”>
<!-- 搜索区域 -->
<Search v-show="toggleFilter" :form="form"/>
<!--展开收起区域-->
<Toggle :toggleFilter="toggleFilter"/>
<!-- 列表区域-->
<List :data="listData"/>
</div> </template>
- 展示型(stateless)组件 主要表现为组件是怎样渲染的,就像一个简单的模版渲染过程。
只通过props接受数据和回调函数,不充当数据源 可能包含展示和容器组件 并且一般会有Dom标签和css样式 通常用props.children(react) 或者slot(vue)来包含其他组件 对第三方没有依赖(对于一个应用级的组件来说可以有) 可以有状态,在其生命周期内可以操纵并改变其内部状态,职责单一,将不属于自己的行为通过回调传递出去,让父级去处理(搜索组件的搜索事件/表单的添加事件)
<template> <div class=“purchase-box”>
<el-table
:data="data"
:class="{'is-empty': !data || data.length ==0 }"
>
<el-table-column
v-for = "(item, index) in listItemConfig"
:key="item + index"
:prop="item.prop"
:label="item.label"
:width="item.width ? item.width : ''"
:min-width="item.minWidth ? item.minWidth : ''"
:max-width="item.maxWidth ? item.maxWidth : ''">
</el-table-column>
<!-- 操作 -->
<el-table-column label="操作" align="right" width="60">
<template slot-scope="scope">
<slot :data="scope.row" name="listOption"></slot>
</template>
</el-table-column>
<!-- 列表为空 -->
<template slot="empty">
<common-empty />
</template>
</el-table>
</div> </template> <script> export default {
props: {
listItemConfig:{ //列表项配置
type:Array,
default: () => {
return [{
prop:'sku_name',
label:'商品名称',
minWidth:200
},{
prop:'sku_code',
label:'SKU',
minWidth:120
},{
prop:'product_barcode',
label:'条形码',
minWidth:120
}]
}
}}
} </script>
- 业务组件 通常是根据最小业务状态抽象而出,有些业务组件也具有一定的复用性,但大多数是一次性组件
5.通用组件 可以在一个或多个APP内通用的组件
- UI组件 特点:复用性强,只通过 props、events 和 slots 等组件接口与外部通信
<template> <div class=“empty”>
<img src="/images/empty.png" alt>
<p>暂无数据</p>
</div> </template>
- 逻辑组件 不包含UI层的某个功能的逻辑集合
8.高阶组件(HOC) 高阶组件可以看做是函数式编程中的组合 可以把高阶组件看做是一个函数,他接收一个组件作为参数,并返回一个功能增强的组件
高阶组件可以抽象组件公共功能的方法而不污染你本身的组件 比如 debounce 与 throttle
9.组件协同 10.容器/展示组件 引入容器组件的概念只是一种更好的组织方式。
容器组件专门负责和store通信,把数据通过props传递给展示组件,展示组件如果数据需要更新,需要传递回调给容器组件,在容器组 件中执行具体操作(业务逻辑)来获取更新结果 展示型组件不再直接和store耦合,而是通过props接口来定义所需的数据和方法,复用性与正确性更能保证 展示型组件直接和store通信的话,那么一个展示型组件就会收到限制,因为你在store里面的字段已经限制他的使用次数和使用的位置 各司其职,不易出错,即使出错,也能快速定位问题
10.1 引入容器组件的时机 优先考虑展示组件,当你意识到有一些中间组件不使用它继承的props而是转而传递给他们的子级,每次子级组件需要更多数据时,你都需要重新调整这些中间组件,那么,这时候就要考虑引入容器组件
容器组件和展示组件的区别并没有被严格定义,它们的区别不在技术上而是目的性上。
容器组件倾向于有状态,展示组件倾向于无状态,这不是硬性规定,它们都是可以有状态的,不要把分离容器组件和展示组件当做教条,如果你不确定该组件是容器组件还是展示组件,就暂时不要分离,写成展示组件,也许是为时尚早。别着急! 这是一个持续的重构过程,不用试图一次就把它做好,习惯这种模式就会培养起一种直觉,知道何时引入容器 就像你知道何时封装一个函数那样儿进行组件职能划分的利弊
11.进行组件职能划分的利弊 11.1 优点
更好的关注分离,用这种方式写组件,你可以更好的理解你的app和你的ui,甚至会逐渐形成你自己的开发套路 复用性高,一个组件只做一件事,解除了组件的耦合带来更高复用性,它是app的调色版,设计师可以随意调整它的ui而不用改变app的逻辑 这会强制你提取“布局组件”,达到更高的易用性 提高健壮性 可测试性,组件做的事情更少了,测试也会变得容易,容器组件不用关心UI的展示,只关心数据和更新,展示组件只是呈现传入的props,写单元测试的时候也非常容易mock数据层
11.2 缺点
因为容器组件/展示组件的拆分,初期会增加一些学习成本 由于需要封装一个容器,包装一些数据和接口给展示组件,会增加一些工作量 在展示组件内对props的声明会带来少量的工作
组件设计的边界
页面层级不宜嵌套超过三层,切勿过度设计 超过三层之后可见组件的数据传递的过程就会变得越复杂
组件可否(有必要)再分?
划分粒度的根据实际情况权衡,太小会提升维护成本,太大又不够灵活和高复用性 每一个组件都应该有其独特的划分目的的,有的是为了复用实现,有的是为了封装复杂度清晰业务实现 组件划分的依据通常是业务逻辑、功能,要考虑各组件之间的关系是否明确,及可复用度 如果它只是几行代码,那么最终可能会创建更多的代码来分离它,有必要吗?我这么做的好处是否超过了成本? 如果你当前的逻辑不太可能出现在其他地方,那么将它嵌入其中更好,如果需要,你可以随时抽离,毕竟组件化没有终点 性能会受到影响吗? 如果状态频繁更改,并且当前在一个较大的,关系比较紧密的组件里,为了避免性能受到影响最好抽离出来 是否打破了一个逻辑上有意义的实体,倘若抽离的话,这个代码被复用的概率有多大
3.这个组件的依赖是否可再缩减? 缩减组件依赖可以提高组件的可复用度
这个组件是否对其它组件造成侵入? 封装性不足或自身越界操作,就可能对自身之外造成了侵入,一个组件不应对其它兄弟组件造成直接影响 。
这个组件可否复用于其它类似场景中? 需要考虑需要适用的不同场景,在组件接口设计时进行必要的兼容
这个组件当别人用时,会怎么想? 接口设计符合规范和大众习惯,尽量让别人用起来简单易上手,易上手是指更符合直觉。
假如业务需要不需要这个功能,是否方便清除? 各组件之前以组合的关系互相配合,也是对功能需求的模块化抽象,当需求变化时可以将实现以模块粒度进行调整 。
落实到具体业务中如何做
- 划分依据 明确组件划分依据,目前是两种
根据业务划分 根据技术划分 我更多的是根据业务去设计我应用中的组件树,可能会画个草图或xmind,它可以帮我统观全局 明确各个组件的边界,内部state的设计,props的设计以及与其他组件的关系(需要回调出去的事件) 明确各个组件的定位与职能划分,设计好父子组件、兄弟组件的通信机制 搭架子 架子有了,开始填空
2.切割模版(页面结构模块化) 这是最容易想到的方法,当一个组件渲染了很多元素,就需要尝试分离这些组件的渲染逻辑 以掘金页面为例
大体上看,可以分为Part1,Part2,Part3
2.1 初步开发 <template> <div id=“app”>
<div class="panel">
<div class="part1 left">
<!--内容-->
</div>
<div class="part1 right">
<!--内容-->
</div>
<div class="part1 right">
<!--内容-->
</div>
</div> </template>
2.2 存在问题[/h3 代码量大,难以维护,难以测试,有些许重复量
[h3]2.3 划繁为简
<template> <div id=“app”>
<part1 />
<part2 />
<part3 />
</div> </template>
好处:同之前的方式相比,这个微妙的改进是革命性的 , 解决了测试困难,维护困难的问题; 问题:没有解决代码重复的问题,这种按模块划分,复用性低
2.4 组件抽象 它们有相似的外层,part2和part3更有相似的titlebar,除了业务内容,完全就是一模一样
<template> <div class=“part”>
<header>
<span>{{ title }}</span>
</header>
<slot name="content" />
</div> </template>
将part内可以抽象的数据都做成了props,利用slot去做模版 那么我们在开发相应Part1,Part2时:
<template> <div id=“app”>
<part title="亦舒">
<div slot="content">----</div>
</part>
<part title="兴隆臻园户型">
<div slot="content">-----</div>
</part>
</div> </template>
更具代表性的示例图:
UI差异在哪里定义? 在业务逻辑层处理,首先要明确一点,这些差异并不是组件本身造成的,是你自己的业务逻辑造成的,所以容器组件(父组件)应该为此买单
数据差异在哪里定义? 结合组件本身和业务上下文将差异合理的消除在内部
比如part3中,其他的part只有一个类似更多>>的link,但是它却有多个(一居,二居…)
这里推荐将这种差异体现在组件内部,设计方法也很多:
比如可以将link数组化为links; 比如可以将更多>>看作是一个default的link,而多余的部分则是用户自定义的特殊link,这两者合并组成了links。用户自定义的默认是没有的,需要引用组件时进行传入。
- 组件命名规则? 组件设计初期,就应该拥有不耦合业务的名字,一个通用的或者说未来可能通用的,要有相对合理的命名,比如 Search,List,尽量不要出现与业务耦合过深的业务名词,通用组件与业务无关,只与自身抽象的组件有关。
在设计组件初期,就应该有这种思想,库通常都想让广大开发者用,在设计组件时,可以降低标准到先做到你的整个APP中通用
组件划分细粒度的考量 组件设计规则明明白白写着我们要遵循单一职责原则,这也带来了上文聊过的过度抽象(组件化)的问题,组件抽离的过程就是无限向无状态(展示型)组件无限靠近的过程。
对于组件设计,充分的准备固然,但在现实世界中,切实的结果才是最重要的,组件设计也不要过度设计更不要停滞不前,该做的时候就去做,发现不好就去改 有空闲时间就去思考早期不够理想的代码,它可以作为我们向前发展的基础 技术在变迁,但组件化的核心并没有改变,目标仍然是在API设计尽可能接近原生的情况下完成复用、解耦、封装、抽象的目标,最终服务于开发,提高效率降低错误率 组件化是对实现的分层,是更有效地代码组合方式 组件化是对资源的重组和优化,从而使项目资源管理更合理,方便拔插、方便集成、方便删除、方便删除后重新加入 这种化繁为简的思想在后端开发中的体现是微服务,而在前端开发中的体现就是组件化 组件化有利于单元测试与自测效率对重构较友好 新人加入可以直接分配组件进行开发、测试,而非需要熟悉整个项目,可以从一个组件的开发使新进人员比较快速熟悉项目、了解到开发规范 你的直接责任可能是编写代码,但你的终极目标是在创建产品