如何正确的实现组件复用
共 6251字,需浏览 13分钟
·
2021-09-25 00:07
在业务开发过程中,我们总是会期望某些功能一定程度的复用。很基础的那些元素,比如按钮,输入框,它们的使用方式都已经被大部分人熟知,但是一旦某块功能复杂起来,成为一种业务组件
的时候,就会陷入一些很奇怪的境况,最初是期望抽出来的这块组件能有比较好的复用性,但是,可能当另外一个业务想要复用它的时候,往往遇到很多问题:
不能满足需求 为了满足多个业务的复用需求,不得不把组件修改到很别扭的程度 参数失控 版本无法管理
诸如此类,时常使人怀疑,在一个业务体系中,组件化到底应该如何去做?
本文试图围绕这个主题,给出一些可能的解决思路。
组件的实现
状态与渲染
通常,我们会有一些简单而通用的场景,需要处理状态的存放:
被单独使用 被组合使用
一般来说,我们有两种策略来实现,分别是状态外置和内置。
有状态组件:
const StatefulInput = () => {
const [value, setValue] = useState('')
return <input value={value} onChange={setValue} />
}
无状态组件:
type StatelessInputProps = {
value: string
setValue: (v: string) => void
}
const StatelessInput = (props: StatelessInputProps) => {
const { value, setValue } = props
return <input value={value} onChange={setValue} />
}
通常有状态组件可以位于更顶层,不受其他约束,而无状态组件则依赖于外部传入的状态与控制。有状态组件也可以在内部分成两层,一层专门处理状态,一层专门处理渲染,后者也是一个无状态组件。
一般来说,对于纯交互类组件,将最核心的状态外置通常是更好的策略,因为它的可组合性需求更强。
使用上下文管控依赖项
我们在实现一个相对复杂组件的时候,有可能面临一些外部依赖项。
比如说:
选择地址的组件,可能需要外部提供地址的查询能力
一般来说,我们给组件提供外置配置项的方式有这么几种:
通过组件自身的参数(props)传入 通过上下文传入 组件自己从某个全局性的位置引入
这三种里面,我们需要尽可能避免直接引入全局依赖,举例来说,如果不刻意控制外部依赖,就会存在许多在组件中直接引用 request 的情况,比如说:
import request from 'xxx'
const Component = () => {
useEffect(() => {
request(xxx)
}, [])
}
注意这里,我们一般意识不到直接 import 这个 request 有什么不对,但实际上,按照这个实现方式,我们可能在一个应用系统中,存在很多个直接依赖 request 的组件,它的典型后果有:
一旦整体的请求方式被变更,比如添加了统一的请求头或者异常处理,那就可能改动每个组件。
这个问题,可能有的研发团队中会选择先封装一下 request,然后再引入,这是可以消除这种问题的。
如果多个不同的项目合并集成了,就存在多种不同的数据来源,不一定能做到直接统一这个请求配置。
因此,要尽量避免直接引入全局性的依赖,哪怕它当前真的是某种全局,也要假定未来是可能变动的,包括但不限于:
请求方式 用户登录状态 视觉主题 多语言国际化 环境与平台相关的 API
需要尽可能把这些东西控制住,封装在某种上下文里,并且提供便利的使用方式:
// 统一封装控制
const ServiceContext = () => {
const request = useCallback(() => {
return // 这里是统一引入控制的 request
}, [])
const context: ServiceContextValue = {
request,
}
return <ServiceContext.Provider value={context}>{children}</ServiceContext.Provider>
}
// 包装一个 hook
const useService = () => {
return useContext(ServiceContext)
}
// 在组件中使用
const Component = () => {
const { request } = useService()
// 这里使用 request
}
这样,我们在整个大组件树上的视角就是:某一个子树往下,可以统一使用某种控制策略,这种策略在模块集成的时候会比较有用。
使用 Context,我们可以更好地表达整组的状态与操作,并且,当下层组件结构产生调整的时候,需要调整的数据连接关系较少(通常我们倾向于使用一些全局状态管理方案的原因也是这样)。
状态的可组合性
在实现组件的时候,我们往往发现它们之间存在很多共性,比如:
所有的表单输入项,都可以控制是否禁用 多选项卡组件与卡片组,都是在一个列表形态上的扩展
从更深的层次出发,我们可以意识到,几乎任意一个组件,它所使用的状态与控制能力都是由若干原子化的能力组合而出,这些原子能力可能是相关的,也可能是不相关的。
举例来说:
const Editable = (props: PropsWithChildren<{}>) => {
const { children } = props
const [editable, setEditable] = useState<boolean>(false)
const context: EditableContextValue = {
editable,
setEditable,
}
return <EditableContext.Provider value={context}>{children}</EditableContext.Provider>
}
这样的一个组件,表达的就是对只读状态的读写操作。如果某个组件内部需要这么一些功能,可以选择直接将它组合进去。
更复杂的情况下,比如当我们想要表达这样一种特殊的表单卡片组,其主要功能包括:
可迭代 可动态添加删除项 可设置是否能编辑 可缓存草稿,也可以提交 可多选
分析其特征,发现来自几种互相不相关的原子交互:
通用列表操作 编辑状态的启用控制 可编辑项 列表多选
它的实现就可能是这样:
const CardList = () => {
const { list, setList, addItem } = useContext(ListContext)
const { editable, setEditable } = useContext(EditContext)
const { commit } = useContext(DraftContext)
const { selectedItems, setSelectedItems } = useContext(ListSelectionContext)
// 然后内部组合使用
}
由此,我们有可能在每个组件开发的时候,将其内部结构分解为若干独立原子交互的组合,在组件实现中,只是组合并且使用它们。
注意,有可能部分状态组之间存在组合顺序依赖关系,比如:“可选择”依赖于“列表”,必须被组合在它下层,这部分可以在另外的体系中进行约束。
分层复用
在业务中,组件的复用方式并不总是一样的。我们有可能需要:
复用一个交互方式 复用一段逻辑 复用一个组合了逻辑与交互的“业务组件”
每当我们需要设计一个“业务组件”的时候,就需要慎重考虑了。可以尝试询问自己一些问题:
我们在复用它的时候,会更改它的外部依赖吗? 它内部的逻辑会被单独复用吗? 这个交互形态会跟其他逻辑组合起来复用吗?
比如说,一个内置了选择省市县的多级地址选择器,它就是这么一种“业务组件”。我们以此为例,尝试重新解构它的可复用性。
存在外部依赖吗?它有可能被更改吗?
对于地址的查询,就是外部依赖。注意,尽管大部分情况下这个是不会改的,但是仍然存在这个可能性,需要提前考虑这类事情,通常,遇到有数据请求之类的东西,尽量去抽象一下。
逻辑会被单独复用吗?
如果需要建立另外一种选地址的组件,交互形态不同,但逻辑可以是一样的。
这个交互形态会跟其他逻辑组合起来复用吗?
有可能被用来选择其他东西。
所以,回答了这些问题之后,我们就可以设计组件结构了:
业务上下文
const Business = () => {
const [state, setState] = useState()
return <BusinessContext.Provider value={context}>{children}</BusinessContext.Provider>
}
交互上下文
const Interaction = () => {
const [state, setState] = useState()
return <InteractionContext.Provider value={context}>{children}</InteractionContext.Provider>
}
在组件的实现中:
const ComponentA = () => {
const {} = useContext(BusinessContext)
const {} = useContext(InteractionContext)
// 在这里连接业务与交互
}
使用的时候:
const App = () => {
// 下面每层传入各自需要的配置信息
return (
<Business>
<Interaction>
<ComponentA />
</Interaction>
</Business>
)
}
在这个部分,总的原则是:
业务状态与 UI 状态隔离 UI 状态与交互呈现隔离
在细分实现中,再考虑两个部分分别由什么东西组合而成。
在一些比较复杂的场景下,状态结构也很复杂,需要管理来自不同信息源的数据。在某些实践中,选择将一切状态聚合到一个超大结构中,然后分别订阅,这当然是可行的,但是对维护就提高了一些难度。
通常,我们有机会把状态去做一些分组,最容易理解的分组方式就是将业务和交互隔离。这种思考方式可以让我们的关注点更聚焦:
写业务的时候,就不去思考交互形态 写交互形态的时候,就不去思考业务逻辑 然后剩下的时间花在把它们连接起来
❤️ 看完三件事
非常棒的一篇Ts实用文章,
如果你觉得这篇内容对你挺有启发,不妨:
点个【在看】,或者分享转发,让更多的人也能看到这篇内容
点击↓面关注我们,一起学前端
长按↓面二维码,添加鬼哥微信,一起学前端