Vue组件设计学习笔记,持续记录

关于组件化

组件化思想并不是前端独有的,但却是前端技术的延伸 任何软件开发过程,或多或少都有那么一些组件化的需求。

1.组件化的特点

  • 每个组件对应一个目录,组件所需的各种资源都在这个目录下就近维护;(最具软件工程价值)
  • 页面上的每个独立的可视/可交互区域视为一个组件;
  • 由于组件具有独立性,可以自由组合;
  • 页面是组件的容器,负责组合组件形成功能完整的界面;
  • 当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换
  • 分子是由原子组成的,分子分成原子,原子也可以重新组合成新的分子
  • 一个界面是由独立的分子组件搭建而成,分子组件由原子元件构成,这些原子可通过不同的组合方式,组成新分子组件,继而重组构成新的界面

组件设计的原则

组件化方案下,我们需要具有组件化设计思维,它是一种【整理术】帮助我们高效开发整合

1. 标准性

任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件

2. 独立性

描述了组件的细粒度,遵循单一职责原则,保持组件的纯粹性,属性配置等API对外开放,组件内部状态对外封闭,尽可能的少与业务耦合

3. 复用与易用

UI差异,消化在组件内部(注意并不是写一堆if/else),输入输出友好,易用 。追求短小精悍,Single Point Of Truth法则,就是尽量不要重复代码,出自《The Art of Unix Programming》;

避免暴露组件内部实现,避免直接操作DOM,避免使用ref,使用父组件的 state 控制子组件的状态而不是直接通过 ref 操作子组件 。

入口处检查参数的有效性,出口处检查返回的正确性。

4. 无环依赖原则(ADP)

Vue组件设计学习笔记,持续记录
设计不当导致环形依赖示意图

4.1 环形依赖

组件间耦合度高,集成测试难 一处修改,处处影响,交付周期长 因为组件之间存在循环依赖,变成了“先有鸡还是先有蛋”的问题。

4.2 避免环形依赖

沿着逆向的依赖关系即可寻找到所有受影响的组件,创建一个共同依赖的新组件。

Vue组件设计学习笔记,持续记录
共同依赖

5. 稳定抽象原则(SAP)

  • 组件的抽象程度与其稳定程度成正比
  • 一个稳定的组件应该是抽象的(逻辑无关的)
  • 一个不稳定的组件应该是具体的(逻辑相关的)
  • 为降低组件之间的耦合度,我们要针对抽象组件编程,而不是针对业务实现编程。

6. 避免冗余状态

  • 如果一个数据可以由另一个 state 变换得到,那么这个数据就不是一个 state,只需要写一个变换的处理函数,在 Vue 中可以使用计算属性
  • 如果一个数据是固定的,不会变化的常量,那么这个数据就如同 HTML 固定的站点标题一样,写死或作为全局配置属性等,不属于 state
  • 如果兄弟组件拥有相同的 state,那么这个state 应该放到更高的层级,使用 props 传递到两个组件中

7. 合理的依赖关系

父组件不依赖子组件,删除某个子组件不会造成功能异常

8. 扁平化参数

除了数据,避免复杂的对象,尽量只接收原始类型的值

9. 良好的接口设计

  • 把组件内部可以完成的工作做到极致,虽然提倡拥抱变化,但接口不是越多越好
  • 如果常量变为 props 能应对更多的场景,那么就可以作为 props,原有的常量可作为默认值。
  • 如果需要为了某一调用者编写大量特定需求的代码,那么可以考虑通过扩展等方式构建一个新的组件。
  • 保证组件的属性和事件足够的给大多数的组件使用。
  • API尽量和已知概念保持一致

组件分类

组件最大的不稳定性来自于展现层,一个组件只做一件事,基于功能做好职责划分。

1. 基础组件(ui组件库)

为了让开发者更关注业务逻辑,涌现出了很多优秀的UI组件库 比如antdelement-ui,我们只需要调用API便能满足大部分的业务场景,前端角色后置了,开发变得更简单了

2. 容器型组件(业务角度,非代码角度)

一个容器性质的组件,一般当作一个业务子模块的入口,比如一个路由指向的组件 。

Vue组件设计学习笔记,持续记录
容器组件
  • 容器组件内的子组件通常具有业务或数据依赖关系
  • 集中/统一的状态管理,向其他展示型/容器型组件提供数据(充当数据源)和行为逻辑处理(接收回调)
  • 如果使用了全局状态管理,那么容器内部的业务组件可以自行调用全局状态处理业务
  • 业务模块内子组件的通信等统筹处理,充当子级组件通信的状态中转站
  • 模版基本都是子级组件的集合,很少包含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)组件

主要表现为组件是怎样渲染的,就像一个简单的模版渲染过程。

Vue组件设计学习笔记,持续记录
展示型组件
  • 只通过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. 业务组件

通常是根据最小业务状态抽象而出,有些业务组件也具有一定的复用性,但大多数是一次性组件

Vue组件设计学习笔记,持续记录
业务组件

5.通用组件

可以在一个或多个APP内通用的组件

6. UI组件 

特点:复用性强,只通过 props、events 和 slots 等组件接口与外部通信

Vue组件设计学习笔记,持续记录
UI组件
<template>
  <div class="empty">
    <img src="/images/empty.png" alt>
    <p>暂无数据</p>
  </div>
</template>

7. 逻辑组件

不包含UI层的某个功能的逻辑集合

8.高阶组件(HOC)

高阶组件可以看做是函数式编程中的组合 可以把高阶组件看做是一个函数,他接收一个组件作为参数,并返回一个功能增强的组件

高阶组件可以抽象组件公共功能的方法而不污染你本身的组件 比如 debounce 与 throttle

9.组件协同

Vue组件设计学习笔记,持续记录
组件协同

10.容器/展示组件

Vue组件设计学习笔记,持续记录
容器/展示组件对比

引入容器组件的概念只是一种更好的组织方式。

  • 容器组件专门负责和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. 这个组件是否对其它组件造成侵入?

封装性不足或自身越界操作,就可能对自身之外造成了侵入,一个组件不应对其它兄弟组件造成直接影响 。

提示
较常见的一种情况是:组件运行时对window对象添加resize监听事件以实现组件响应视窗尺寸变化事件,这种需求的更好替代方案是:组件提供刷新方法,由父组件实现调用。
次优的方案是,当组件destroy前清理恢复

5. 这个组件可否复用于其它类似场景中?

需要考虑需要适用的不同场景,在组件接口设计时进行必要的兼容

6. 这个组件当别人用时,会怎么想?

接口设计符合规范和大众习惯,尽量让别人用起来简单易上手,易上手是指更符合直觉。

7. 假如业务需要不需要这个功能,是否方便清除?

各组件之前以组合的关系互相配合,也是对功能需求的模块化抽象,当需求变化时可以将实现以模块粒度进行调整 。

落实到具体业务中如何做 

1. 划分依据

明确组件划分依据,目前是两种

  • 根据业务划分
  • 根据技术划分
  • 我更多的是根据业务去设计我应用中的组件树,可能会画个草图或xmind,它可以帮我统观全局
  • 明确各个组件的边界,内部state的设计,props的设计以及与其他组件的关系(需要回调出去的事件)
  • 明确各个组件的定位与职能划分,设计好父子组件、兄弟组件的通信机制
  • 搭架子
  • 架子有了,开始填空

2.切割模版(页面结构模块化)

这是最容易想到的方法,当一个组件渲染了很多元素,就需要尝试分离这些组件的渲染逻辑 以掘金页面为例

Vue组件设计学习笔记,持续记录
例子

大体上看,可以分为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>

更具代表性的示例图:

Vue组件设计学习笔记,持续记录
案例

8. UI差异在哪里定义?

在业务逻辑层处理,首先要明确一点,这些差异并不是组件本身造成的,是你自己的业务逻辑造成的,所以容器组件(父组件)应该为此买单

9. 数据差异在哪里定义?

结合组件本身和业务上下文将差异合理的消除在内部

比如part3中,其他的part只有一个类似更多>>的link,但是它却有多个(一居,二居...)

这里推荐将这种差异体现在组件内部,设计方法也很多:

  • 比如可以将link数组化为links;
  • 比如可以将更多>>看作是一个default的link,而多余的部分则是用户自定义的特殊link,这两者合并组成了links。用户自定义的默认是没有的,需要引用组件时进行传入。

10. 组件命名规则?

组件设计初期,就应该拥有不耦合业务的名字,一个通用的或者说未来可能通用的,要有相对合理的命名,比如 Search,List,尽量不要出现与业务耦合过深的业务名词,通用组件与业务无关,只与自身抽象的组件有关。

在设计组件初期,就应该有这种思想,库通常都想让广大开发者用,在设计组件时,可以降低标准到先做到你的整个APP中通用

组件划分细粒度的考量 

组件设计规则明明白白写着我们要遵循单一职责原则,这也带来了上文聊过的过度抽象(组件化)的问题,组件抽离的过程就是无限向无状态(展示型)组件无限靠近的过程。

  • 对于组件设计,充分的准备固然,但在现实世界中,切实的结果才是最重要的,组件设计也不要过度设计更不要停滞不前,该做的时候就去做,发现不好就去改
  • 有空闲时间就去思考早期不够理想的代码,它可以作为我们向前发展的基础
  • 技术在变迁,但组件化的核心并没有改变,目标仍然是在API设计尽可能接近原生的情况下完成复用、解耦、封装、抽象的目标,最终服务于开发,提高效率降低错误率
  • 组件化是对实现的分层,是更有效地代码组合方式
  • 组件化是对资源的重组和优化,从而使项目资源管理更合理,方便拔插、方便集成、方便删除、方便删除后重新加入
  • 这种化繁为简的思想在后端开发中的体现是微服务,而在前端开发中的体现就是组件化
  • 组件化有利于单元测试与自测效率对重构较友好
  • 新人加入可以直接分配组件进行开发、测试,而非需要熟悉整个项目,可以从一个组件的开发使新进人员比较快速熟悉项目、了解到开发规范
  • 你的直接责任可能是编写代码,但你的终极目标是在创建产品