Vue组件设计学习笔记,持续记录
- Vue学习
- 2022-06-18
- 1098热度
- 0评论
关于组件化
组件化思想并不是前端独有的,但却是前端技术的延伸 任何软件开发过程,或多或少都有那么一些组件化的需求。
1.组件化的特点
- 每个组件对应一个目录,组件所需的各种资源都在这个目录下就近维护;(最具软件工程价值)
- 页面上的每个独立的可视/可交互区域视为一个组件;
- 由于组件具有独立性,可以自由组合;
- 页面是组件的容器,负责组合组件形成功能完整的界面;
- 当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换
- 分子是由原子组成的,分子分成原子,原子也可以重新组合成新的分子
- 一个界面是由独立的分子组件搭建而成,分子组件由原子元件构成,这些原子可通过不同的组合方式,组成新分子组件,继而重组构成新的界面
组件设计的原则
组件化方案下,我们需要具有组件化设计思维,它是一种【整理术】帮助我们高效开发整合
1. 标准性
任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件
2. 独立性
描述了组件的细粒度,遵循单一职责原则,保持组件的纯粹性,属性配置等API对外开放,组件内部状态对外封闭,尽可能的少与业务耦合
3. 复用与易用
UI差异,消化在组件内部(注意并不是写一堆if/else),输入输出友好,易用 。追求短小精悍,Single Point Of Truth法则,就是尽量不要重复代码,出自《The Art of Unix Programming》;
避免暴露组件内部实现,避免直接操作DOM,避免使用ref,使用父组件的 state 控制子组件的状态而不是直接通过 ref 操作子组件 。
入口处检查参数的有效性,出口处检查返回的正确性。
4. 无环依赖原则(ADP)
4.1 环形依赖
组件间耦合度高,集成测试难 一处修改,处处影响,交付周期长 因为组件之间存在循环依赖,变成了“先有鸡还是先有蛋”的问题。
4.2 避免环形依赖
沿着逆向的依赖关系即可寻找到所有受影响的组件,创建一个共同依赖的新组件。
5. 稳定抽象原则(SAP)
- 组件的抽象程度与其稳定程度成正比
- 一个稳定的组件应该是抽象的(逻辑无关的)
- 一个不稳定的组件应该是具体的(逻辑相关的)
- 为降低组件之间的耦合度,我们要针对抽象组件编程,而不是针对业务实现编程。
6. 避免冗余状态
- 如果一个数据可以由另一个 state 变换得到,那么这个数据就不是一个 state,只需要写一个变换的处理函数,在 Vue 中可以使用计算属性
- 如果一个数据是固定的,不会变化的常量,那么这个数据就如同 HTML 固定的站点标题一样,写死或作为全局配置属性等,不属于 state
- 如果兄弟组件拥有相同的 state,那么这个state 应该放到更高的层级,使用 props 传递到两个组件中
7. 合理的依赖关系
父组件不依赖子组件,删除某个子组件不会造成功能异常
8. 扁平化参数
除了数据,避免复杂的对象,尽量只接收原始类型的值
9. 良好的接口设计
- 把组件内部可以完成的工作做到极致,虽然提倡拥抱变化,但接口不是越多越好
- 如果常量变为 props 能应对更多的场景,那么就可以作为 props,原有的常量可作为默认值。
- 如果需要为了某一调用者编写大量特定需求的代码,那么可以考虑通过扩展等方式构建一个新的组件。
- 保证组件的属性和事件足够的给大多数的组件使用。
- API尽量和已知概念保持一致
组件分类
组件最大的不稳定性来自于展现层,一个组件只做一件事,基于功能做好职责划分。
1. 基础组件(ui组件库)
为了让开发者更关注业务逻辑,涌现出了很多优秀的UI组件库 比如antd
,element-ui
,我们只需要调用API便能满足大部分的业务场景,前端角色后置了,开发变得更简单了
2. 容器型组件(业务角度,非代码角度)
一个容器性质的组件,一般当作一个业务子模块的入口,比如一个路由指向的组件 。
- 容器组件内的子组件通常具有业务或数据依赖关系
- 集中/统一的状态管理,向其他展示型/容器型组件提供数据(充当数据源)和行为逻辑处理(接收回调)
- 如果使用了全局状态管理,那么容器内部的业务组件可以自行调用全局状态处理业务
- 业务模块内子组件的通信等统筹处理,充当子级组件通信的状态中转站
- 模版基本都是子级组件的集合,很少包含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>
3. 展示型(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>
4. 业务组件
通常是根据最小业务状态抽象而出,有些业务组件也具有一定的复用性,但大多数是一次性组件
5.通用组件
可以在一个或多个APP内通用的组件
6. UI组件
特点:复用性强,只通过 props、events 和 slots 等组件接口与外部通信
<template>
<div class="empty">
<img src="/images/empty.png" alt>
<p>暂无数据</p>
</div>
</template>
7. 逻辑组件
不包含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的声明会带来少量的工作
组件设计的边界
1. 页面层级不宜嵌套超过三层,切勿过度设计
超过三层之后可见组件的数据传递的过程就会变得越复杂
2. 组件可否(有必要)再分?
- 划分粒度的根据实际情况权衡,太小会提升维护成本,太大又不够灵活和高复用性
- 每一个组件都应该有其独特的划分目的的,有的是为了复用实现,有的是为了封装复杂度清晰业务实现
- 组件划分的依据通常是业务逻辑、功能,要考虑各组件之间的关系是否明确,及可复用度
- 如果它只是几行代码,那么最终可能会创建更多的代码来分离它,有必要吗?我这么做的好处是否超过了成本?
- 如果你当前的逻辑不太可能出现在其他地方,那么将它嵌入其中更好,如果需要,你可以随时抽离,毕竟组件化没有终点
- 性能会受到影响吗? 如果状态频繁更改,并且当前在一个较大的,关系比较紧密的组件里,为了避免性能受到影响最好抽离出来
- 是否打破了一个逻辑上有意义的实体,倘若抽离的话,这个代码被复用的概率有多大
3.这个组件的依赖是否可再缩减?
缩减组件依赖可以提高组件的可复用度
4. 这个组件是否对其它组件造成侵入?
封装性不足或自身越界操作,就可能对自身之外造成了侵入,一个组件不应对其它兄弟组件造成直接影响 。
次优的方案是,当组件destroy前清理恢复
5. 这个组件可否复用于其它类似场景中?
需要考虑需要适用的不同场景,在组件接口设计时进行必要的兼容
6. 这个组件当别人用时,会怎么想?
接口设计符合规范和大众习惯,尽量让别人用起来简单易上手,易上手是指更符合直觉。
7. 假如业务需要不需要这个功能,是否方便清除?
各组件之前以组合的关系互相配合,也是对功能需求的模块化抽象,当需求变化时可以将实现以模块粒度进行调整 。
落实到具体业务中如何做
1. 划分依据
明确组件划分依据,目前是两种
- 根据业务划分
- 根据技术划分
- 我更多的是根据业务去设计我应用中的组件树,可能会画个草图或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>
更具代表性的示例图:
8. UI差异在哪里定义?
在业务逻辑层处理,首先要明确一点,这些差异并不是组件本身造成的,是你自己的业务逻辑造成的,所以容器组件(父组件)应该为此买单
9. 数据差异在哪里定义?
结合组件本身和业务上下文将差异合理的消除在内部
比如part3中,其他的part只有一个类似更多>>的link,但是它却有多个(一居,二居...)
这里推荐将这种差异体现在组件内部,设计方法也很多:
- 比如可以将link数组化为links;
- 比如可以将更多>>看作是一个default的link,而多余的部分则是用户自定义的特殊link,这两者合并组成了links。用户自定义的默认是没有的,需要引用组件时进行传入。
10. 组件命名规则?
组件设计初期,就应该拥有不耦合业务的名字,一个通用的或者说未来可能通用的,要有相对合理的命名,比如 Search,List,尽量不要出现与业务耦合过深的业务名词,通用组件与业务无关,只与自身抽象的组件有关。
在设计组件初期,就应该有这种思想,库通常都想让广大开发者用,在设计组件时,可以降低标准到先做到你的整个APP中通用
组件划分细粒度的考量
组件设计规则明明白白写着我们要遵循单一职责原则,这也带来了上文聊过的过度抽象(组件化)的问题,组件抽离的过程就是无限向无状态(展示型)组件无限靠近的过程。
- 对于组件设计,充分的准备固然,但在现实世界中,切实的结果才是最重要的,组件设计也不要过度设计更不要停滞不前,该做的时候就去做,发现不好就去改
- 有空闲时间就去思考早期不够理想的代码,它可以作为我们向前发展的基础
- 技术在变迁,但组件化的核心并没有改变,目标仍然是在API设计尽可能接近原生的情况下完成复用、解耦、封装、抽象的目标,最终服务于开发,提高效率降低错误率
- 组件化是对实现的分层,是更有效地代码组合方式
- 组件化是对资源的重组和优化,从而使项目资源管理更合理,方便拔插、方便集成、方便删除、方便删除后重新加入
- 这种化繁为简的思想在后端开发中的体现是微服务,而在前端开发中的体现就是组件化
- 组件化有利于单元测试与自测效率对重构较友好
- 新人加入可以直接分配组件进行开发、测试,而非需要熟悉整个项目,可以从一个组件的开发使新进人员比较快速熟悉项目、了解到开发规范
- 你的直接责任可能是编写代码,但你的终极目标是在创建产品