前端组件化开发

192 阅读9分钟

组件化历史

  • 一开始前端页面全部用标签
  • 后来有了语义化,每个标签不用div,用一些有意义的标签
  • 后来随着模板引擎的出现,可以把网页的代码分割成片段(Fragments)或模板(Templates),例如导航,内容,页脚等等,之后在需要的地方,使用引入(Include)语法,例如JSP,PHP。ASP,JSF
  • 后来有了框架,有了开源样式

什么是组件

常见的基础组件有按钮、导航、提示框、表单输入控件、对话框、表格、列表等,我们在它们的基础上又能组合出更复杂的组件,那么对于前端中的组件的定义就非常广泛了,小到一个按钮,大到一个页面都可以形成一个组件,例如两个相似的页面,可以复用一个页面组件,只需要通过修改组件的属性,来形成一个新的页面。

组件化优点

  1. 减少代码重复率
  2. 方便维护
  3. 设计风格统一,提高了系统的伸展性
  4. 开发效率更高(写新页面更快),降低复杂度。
  5. 团队共享,复用率高,降低重复开发的成本。节约时间

组件设计原则

封装组件要考虑好多通用型,适用性,扩展性和各种兼容性

  1. 层次结构和 UML 类图
  2. 扁平化、面向数据的 state/props
  3. 更加纯粹的 State 变化
  4. 低耦合
  5. 辅助代码分离
  6. 提炼精华
  7. 及时模块化
  8. 集中/统一的状态管理

怎么进行组件化开发?

通过设计稿来规划view组件

  • 如果公司设计师提供了详细的设计规范,那么直接按照规范中的组件来开发就可以了。
  • 如果只有设计稿,那么就看看哪些内容有 2 次或更多次在其它页面中用到了,这些大概率需要设计为组件。
  • 如果连设计稿都没有,那么可以用市面上现成的组件库,例如 ant design。
  • 如果没有设计稿,还要自己从零开发,那么可以根据通用的套路,把基础组件,例如按钮、菜单等,规划出来,并把可能会多次用到的页面内容,也规划出来,例如导航,底部信息、联系方式区域,这样可以只改动需要变化的部分,不影响其它部分。

通过UML类图——组件层次结构

uml:统一建模语言

设计时候要考虑前端组件的

  • 类的属性状态State
  • 传递的参数Props
  • Methods
  • 与其他组件的关系( Relationship to other components )

组件的职责有:

  • 组件内维护自身的数据和状态
  • 组件内维护自身的事件(方法)
  • 对外提供的配置接口,来控制展示及具体的功能
  • 通过对外提供的查询接口,可获取组件状态和数据

组件管理

代码存放

  • 组件的代码应该遵循就近原则,也就是说:

    • 和组件有关的 HTML、CSS 、JS 代码和图片等静态资源应该放在同一个目录下,方便引用。
    • 组件里的代码应该只包括跟本组件相关的 HTML 模板、CSS 样式和 JS 数据逻辑。
    • 所有的组件应放到一个统一的『组件文件夹中』。
  • 如果组件之间有可以复用的 HTML 和 CSS,那么这个复用的部分可以直接定义成一个新的组件。

  • 如果组件之间有可以复用的 JS 数据逻辑,那么可以把公用的数据逻辑抽离出来,放到公共的业务逻辑目录下,使用到该逻辑的组件,统一从这个目录中导入。

  • 如果项目中使用到了全局状态管理,那么状态管理的数据应放在独立的目录里,这个目录还会存放分割好的状态片段 reducer,之后统一在 store 中合并。

  • 对于项目中的 API 处理,可以把它们单独放到一个文件夹里,对于同一个数据类型的操作,可以放到同一个 js 文件里,例如对 user 用户数据的增删改查,这样能尽最大可能进行复用,之后在组件里可以直接引入相关 api 进行调用。如果直接写在组件里,那么使用相同 API 的组件就会造成代码重复。 例如下面是一个组件化开发的目录结构示例(框架无关):

  • project
    |– components   # 所有组件

      |-- Card                *# Card 组件*
          |-- index.js        *# Card 组件 js 代码*
          |-- Card.html       *# Card 组件 html 模板*
          |-- Card.css        *# Card 组件 css 样式*
          |-- icon.svg        *# Card 组件用到的 icon*
    

    |– logics   # 公共业务逻辑

      |-- getUserInfo.js      *# 例如获取用户信息*
    

    |– data   # 全局状态

      |-- store.js            *# 全局状态管理 store*
      |-- user.reducer.js     *# user reducer*
      |-- blogPost.reducer.js *# blogPost reducer*
    

    |– apis   # 远程请求 API 业务逻辑

      |-- user.js             *# user API*
      |-- blogPost.js         *# blogPost API*
    

组件样式

SASS预处理器去进行统一的css文件管理

定义变量,主题色

通过@extend指令在选择器之间复用属性

只设置组件 CSS 盒子内部的样式,影响外部布局的样式要尽可能的避免,而应该由使用该组件的父组件去设置。

例如,如果有一个组件设置了外边距,但是这个组件经常会用于 grid 或 flex 布局中,那么这个额外的边距会对布局造成影响,只能通过重置外边距的方式取消边距,这就不如组件不设置外边距,由父组件的布局决定它的位置,或者外边距。

定位相关的样式,绝对定位、固定定位等对文档流有影响,应交由父组件决定,除非这个组件只有绝对定位这一种情况,例如对话框。 组件中的 CSS 要局部化,以避免影响全局样式。传统的 CSS 样式是全局的,如果有两个不同的组件,使用了相同的 class 名字,那么后定义的样式会覆盖之前定义的。一般前端库中都有定义局部样式的功能,例如通过 CSS Modules。

组件属性——父组件传进来的属性,只有父组件能更改

1.属性值的类型是否是有效的。如果某个属性要求传递一个数组,那么传递过来的值不是数组时,就要抛出异常,并给出对应的提示。

2.属性是否是必填的。有的属性的值,是组件内不可缺少的时,就要是必填的,在组件初始化时要做是否传递的检查,如果没有传递,则需要抛出异常,并给出相应的提示。如果属性不是必填的,可以设定一个默认值,当属性没有被设置时,就使用默认值。

组件属性不直接和状态捆绑,只作为状态的初始值,不然会造成数据的不一致,破坏了单一数据流机制。

应该设置一个初始值属性,通过父组件传递进来,当作状态的初始值,然后丢弃,后续只通过该组件修改状态值。

const { initialTitle } = props;
const title = state(initialTitle);

*// 修改*
updateTitle() {
title = "...";
}

组件状态——组件内部状态,可以随便更改

  • 区分全局状态和局部状态
  • 子组件向父组件传值时,通过事件方式传。 尽最大可能避免使用 ref
  • 状态的变化还应只通过事件或生命周期来进行,不能在其它同步执行的代码中进行。

const someState = state();

// bad
const state = “newState”;

// good
handleButtonClick() {
someState = “newState”;
}

全局状态是多个组件共享的,如果有多个组件共享某个状态,那么应该把状态定义在这些组件统一的、最接近的父组件中,或者使用全局状态管理库。

vuex全局状态的修改,应该由类似于 actions 的行为触发,然后使用 reducer 修改状态,这样能追溯状态的变化路径,方便调试和打印日志。

组件组合和复用

  • 尽量使用slot插槽。减少组件的嵌套,避免多层传递属性或事件监听。
  • 对列表设置key值,防止重新渲染

组件分层设计原则

根据组件复用方式分为:

  • 复用数据交互方式
  • 复用一段逻辑
  • 复用业务组件

所以设计一个业务组件的时候,需要考虑:

  • 是否存在外部依赖?是不是可以更改?
  • 逻辑会不会被单独复用?
  • 交互形态能不能被其他逻辑组件复用?

业务状态与 UI 状态隔离

UI 状态与交互呈现隔离

模块的拆分

对于简单的管理端应用,采用类似MVC结构拆分

  • 视图模块
  • 数据模块
  • 逻辑控制模块

对于页面内容丰富的应用,结合业务进行拆分

  • 核心模块
  • 功能模块
  • 公共组件模块

对于交互和逻辑复杂的应用,根据系统架构,进行模块和层级的划分

  • 渲染层
  • 数据层
  • 网络层

组件划分

  • 通过代码复用划分
  • 通过视觉和交互划分——功能性,独立性

其他设计原则

辅助代码分离

配置文件、mock数据、非技术的文档

松耦合

组件的核心思想是它们是可复用的,为此要求它们必须具有功能性和完整性

松耦合能够独立运行,而不依赖于其他模块

就前端组件而言,耦合的主要部分是组件的功能依赖于其父级及其传递的 props 的多少,以及内部使用的子组件(当然还有引用的部分,如第三方模块或用户脚本)。