别让你的组件遗臭万年:开发友好型研发组件

96 阅读7分钟

目录和命名

命名

业务组件的命名遵循以下范式:

目的是第一次接手代码的人也能大概知道这部分组件的功能

`${业务功能}&{组件特征}`
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 //负责处理上传的数据处理

目的是减少组件之间的耦合,让组件变得容易维护

开闭原则

软件实体(类、模块、函数等)应该对拓展开发,对修改关闭

允许通过添加新的组件和功能来拓展系统,而不是修改原有的逻辑

减少引入错误的可能性

里氏替换

里氏替换原则要求子组件能够无缝替换其父组件,而不破坏整体功能

  1. 子组件的可替换性,父组件可以将子组件替换为任何同样类型的组件。
  2. 子组件不改变父组件的行为,子组件修改或者拓展功能的时候,不能修改父组件已经定义好的内容。子组件的实现不依赖于父组件的具体实现细节。
  3. 拓展而非覆盖:功能的修改和新增应该是拓展组件,而不是对组件原有方法做修改
  4. 接口一致性
  5. 尊重父组件的约束:包括数据格式,事件处理方式等

保证子组件不破坏整体功能,不引入新的行为和依赖。实现可插拔。

迪米特法则

又称最少知识原则,它规定一个对象应该对其他对象有尽可能少的了解,只与直接的对象通信。

每个组件应该之于他直接依赖的组件通信,而不是跨越多层去和其他组件沟通。意味着我们要慎用Context,他将破坏组件的内聚性。让组件和Context耦合,难以在其他地方复用。

降低类的耦合度,使功能更加独立。提高可维护性和可迭代性。

接口隔离原则

将大接口拆分为小接口,组件只使用依赖他们实际使用的接口

  • 只包含必要功能的小组件,而不是包含大量方法的大组件
  • 组件只暴露它真的需要的接口,而不是大而全的接口,防止组件的使用者依赖于他们不需要的功能

提高组件的可用性和灵活性