skip to content
logo

Search

业务体系中组件化设计思路

在业务开发过程中,我们总是会期望某些功能一定程度的复用。很基础的那些元素,比如按钮,输入框,它们的使用方式都已经被大部分人熟知,但是一旦某块功能复杂起来,成为一种“业务组件”的时候,就会陷入一些很奇怪的境况,最初是期望抽出来的这块组件能有比较好的复用性,但是,当另外一个业务想要复用它的时候,往往遇到很多问题:

  • 不能满足需求
  • 为了满足多个业务的复用需求,不得不把组件修改到很别扭的程度
  • 参数失控
  • 版本无法管理

诸如此类,时常使人怀疑,在一个业务体系中,组件化到底应该如何去做?

本文试图围绕这个主题,给出一些可能的解决思路。

组件的实现

状态与渲染

通常,我们会有一些简单而通用的场景,需要处理状态的存放:

  • 被单独使用
  • 被组合使用

一般来说,我们有两种策略来实现,分别是状态外置和内置。

有状态组件 —— 状态内置:

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} />
}

通常有状态组件可以位于更顶层,不受其他约束,而无状态组件则依赖于外部传入的状态与控制。有状态组件也可以在内部分成两层,一层专门处理状态,一层专门处理渲染,后者也是一个无状态组件。

一般来说,对于纯交互类组件,将最核心的状态外置通常是更好的策略,因为它的可组合性需求更强。

使用上下文管控依赖项

我们在实现一个相对复杂组件的时候,有可能面临一些外部依赖项。

比如说:

  • 选择地址的组件,可能需要外部提供地址的查询能力

一般来说,我们给组件提供外置配置项的方式有这么几种:

  1. 通过组件自身的参数(props)传入
  2. 通过上下文传入
  3. 组件自己从某个全局性的位置引入 ❌

这三种里面,我们需要尽可能避免直接引入全局依赖

举个例子,如果不刻意控制外部依赖,就会存在许多在组件中直接引用 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
}