在业务开发过程中,我们总是会期望某些功能一定程度的复用。很基础的那些元素,比如按钮,输入框,它们的使用方式都已经被大部分人熟知,但是一旦某块功能复杂起来,成为一种“业务组件”的时候,就会陷入一些很奇怪的境况,最初是期望抽出来的这块组件能有比较好的复用性,但是,当另外一个业务想要复用它的时候,往往遇到很多问题:
- 不能满足需求
- 为了满足多个业务的复用需求,不得不把组件修改到很别扭的程度
- 参数失控
- 版本无法管理
诸如此类,时常使人怀疑,在一个业务体系中,组件化到底应该如何去做?
本文试图围绕这个主题,给出一些可能的解决思路。
组件的实现
状态与渲染
通常,我们会有一些简单而通用的场景,需要处理状态的存放:
- 被单独使用
- 被组合使用
一般来说,我们有两种策略来实现,分别是状态外置和内置。
有状态组件 —— 状态内置:
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
}