组件化历史
- 一开始前端页面全部用标签
- 后来有了语义化,每个标签不用div,用一些有意义的标签
- 后来随着模板引擎的出现,可以把网页的代码分割成片段(Fragments)或模板(Templates),例如导航,内容,页脚等等,之后在需要的地方,使用引入(Include)语法,例如JSP,PHP。ASP,JSF
- 后来有了框架,有了开源样式
什么是组件
常见的基础组件有按钮、导航、提示框、表单输入控件、对话框、表格、列表等,我们在它们的基础上又能组合出更复杂的组件,那么对于前端中的组件的定义就非常广泛了,小到一个按钮,大到一个页面都可以形成一个组件,例如两个相似的页面,可以复用一个页面组件,只需要通过修改组件的属性,来形成一个新的页面。
组件化优点
- 减少代码重复率
- 方便维护
- 设计风格统一,提高了系统的伸展性
- 开发效率更高(写新页面更快),降低复杂度。
- 团队共享,复用率高,降低重复开发的成本。节约时间
组件设计原则
封装组件要考虑好多通用型,适用性,扩展性和各种兼容性
- 层次结构和 UML 类图
- 扁平化、面向数据的 state/props
- 更加纯粹的 State 变化
- 低耦合
- 辅助代码分离
- 提炼精华
- 及时模块化
- 集中/统一的状态管理
怎么进行组件化开发?
通过设计稿来规划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 的多少,以及内部使用的子组件(当然还有引用的部分,如第三方模块或用户脚本)。