目录和命名
命名
业务组件的命名遵循以下范式:
目的是第一次接手代码的人也能大概知道这部分组件的功能
`${业务功能}&{组件特征}`
PaperDoucmentEditor
文书 - 编辑组件
DeletePrecheckModel
删除预检 - 模态框
黄色部分最好是一个动词,但是我们通常难以保证他是一个动词,不用强求。这部分的词语限制了组件的使用范围和功能,比如第一个例子,他是在编辑文书的使用时候用到的,第二个例子,他是在做删除操作之前用到的
绿色的部分必须是一个名词,最好是沿用常见的组件类型,比如Button , Form , Seacher , Selector , Tree , Tag 等等
目录
当一个组件比较简单的时候,他或许可以在一个文件中完成,但是
当一个组件比较复杂的时候,需要组合多个基本组件,比如以下的例子BaseTable
BaseTable组件中还有两个子组件 WrapTable 和 TableSeacher
BaseTable
-Index.tsx
-BaseTable.tsx
-TableSearcher.tsx
为了保证目录的结构一致,所有的公共组件/业务组件都建议写成一个文件夹,下带有一个index作为主组件的写法。
这样我们可以根据组件的复杂度来讲相关逻辑都拆分,然后收敛到这个文件夹下,比如
ExecutePrecheckAuthModel
-index.module.scss
-ExecutePrecheckAuthModel.tsx
-Index.tsx
-RulesTable.tsx
-usePermission.tsx
分层设计
简单的组件或许可以不用拆分,但是设计复杂的组件的时候,数据流的走向会很难以把握。智慧档案将这一点展现的是淋漓尽致,指难以把握。
分层的目的:让展示归展示,让逻辑归逻辑,组件/函数保持纯粹
为了编写更为复杂的组件,我们需要将复杂组件简单化。
人脑能够同时处理的上下文是很有限的,如果一个文件有1000行,我们就很难在脑子中处理它的上下文,但是我们把他们拆解成10个100行的文件,每一个文件我们就好把握了。
UI组件
对于仅仅做界面展示的组件我们叫做UI组件(dubm component)
复杂组件
复杂组件通常是需要对数据做很多转换,做AJAX操作数据的组件,对于这类组件,我们需要对他进行拆分。秉承着复杂问题简单化,只处理简单问题的初心,我们希望把他们拆解成UI组件和逻辑组件的聚合。在React中,逻辑组件也叫hooks
UI的抽离和复用叫做 组件
逻辑的抽离和复用叫做 hook
声明式组件
正如刚才的这个例子,我们要做的就是将 ExecutePrecheckAuthModel 这个组件分层,分为 逻辑处理和界面展示,而这里的逻辑处理就是usePermission.tsx,这里的ui展现就是Index.tsx和RulesTable。
而什么是声明式组件呢,其实我们做到这一步已经将这个组件封装为声明式的了,index作为UI组件,他不关心数据的变换和操作,他要做的只是声明我需要这个数据,而数据如何做逻辑操作,我们通过 usePermission 承接这部分代码
ExecutePrecheckAuthModel
-index.module.scss
-ExecutePrecheckAuthModel.tsx
-Index.tsx
-RulesTable.tsx
-usePermission.tsx
就比如我们编写一个render函数(HTML),我们展示数据的框架写好,而在render函数外的操作,就是通过各种各样的hook去获取数据和方法
错误示范
智慧档案 - src - Public - FileEditor - index.js
这个fileEditor中,没有做拆分,所有数据的处理都在一个文件中,一个文件承担了原本3,4个文件承担的内容,最终文件达到了惊人的1000行
正确做法
例子中layout这个组件是声明式的,获取menuItem的逻辑提到他的hooks
属性和方法
上面我们设计这个组件是不涉及于外部通信的,但是在开发中,有输入的组件又应该如何设计他的属性Props呢?
数据流
组件应当设计为纯函数,也就是对于同样的输入,会得到同样的展示结果。
这里的数据输入就是 props
控制流
在组件的控制流中,我们要坚持从上到下,也就是子组件不能控制父组件,而是父组件控制子组件。
比如 PaperDocumentEditior 中,编辑完成后需要触发父组件刷新,这时候我们应当从父组件传入一个 onSubmit,父组件在onSubmit中处理相关的逻辑。
错误的做法是从子组件中暴露一个 onRefresh , 然后在onRefresh中触发父组件更新,因为这样父组件无法控制更新的行为,这是一种逆向数据流。
Hooks
- 逻辑的抽离就是hooks,ui的抽离就是组件
- hook 必须以 use 开头, hook必须在内部使用了hook
对于组件的逻辑处理,我们不要写在UI组件内部,我们需要保证他是声明式的,保持他的纯粹,我们将这部分抽离出来变成自定义hook,如果这部分没有被复用,我们可以将hook放到对应的文件夹下
ExecutePrecheckAuthModel
-index.module.scss
-ExecutePrecheckAuthModel.tsx
-Index.tsx
-RulesTable.tsx
-usePermission.tsx
如果这部分hook(逻辑)可以被其他的组件使用,那么将hook提出到公共hook文件夹下
原则抽象
其实吧上面的内容抽象一下,就变成了一些前端的研发规范,也就是 SOLID
单一职责
每个组件只负责一项具体的业务,他的变化应该只由一个原因引起
PaperDocumentEditor
- Index.tsx //负责构建组件(组合功能)
- useUpload.ts //负责处理上传的数据处理
目的是减少组件之间的耦合,让组件变得容易维护
开闭原则
软件实体(类、模块、函数等)应该对拓展开发,对修改关闭
允许通过添加新的组件和功能来拓展系统,而不是修改原有的逻辑
减少引入错误的可能性
里氏替换
里氏替换原则要求子组件能够无缝替换其父组件,而不破坏整体功能
- 子组件的可替换性,父组件可以将子组件替换为任何同样类型的组件。
- 子组件不改变父组件的行为,子组件修改或者拓展功能的时候,不能修改父组件已经定义好的内容。子组件的实现不依赖于父组件的具体实现细节。
- 拓展而非覆盖:功能的修改和新增应该是拓展组件,而不是对组件原有方法做修改
- 接口一致性
- 尊重父组件的约束:包括数据格式,事件处理方式等
保证子组件不破坏整体功能,不引入新的行为和依赖。实现可插拔。
迪米特法则
又称最少知识原则,它规定一个对象应该对其他对象有尽可能少的了解,只与直接的对象通信。
每个组件应该之于他直接依赖的组件通信,而不是跨越多层去和其他组件沟通。意味着我们要慎用Context,他将破坏组件的内聚性。让组件和Context耦合,难以在其他地方复用。
降低类的耦合度,使功能更加独立。提高可维护性和可迭代性。
接口隔离原则
将大接口拆分为小接口,组件只使用依赖他们实际使用的接口
- 只包含必要功能的小组件,而不是包含大量方法的大组件
- 组件只暴露它真的需要的接口,而不是大而全的接口,防止组件的使用者依赖于他们不需要的功能
提高组件的可用性和灵活性