本篇文章将介绍4种不同的组件设计模式,其中包含了每个模式的优缺点以及在代码中的使用方式;
问题
作为React的使用人员,本人在做组件库开发时,一般会思考以下问题:
- 怎么才能让组件适配更多的使用场景?
- 怎么才能设计出比较简单并且合理的API,让它变得易用?
- 怎么才能让该组件的UI界面和功能有较强的可扩展性?
- ...
对这些问题的反复🤔思考,并在React相关社区反复求证,总结出以下四种组件开发模式;
为了方便讲解,接下来将以 Counter
组件的设计为例,对这四种模式进行讲解:
我们首先定义两个标准:
- Inversion of control:组件使用的灵活性,以及对组件可控制的级别
- Implementation complexity: 使用组件以及实现改组件功能的复杂度
另外,在文章中讲解到每个组件设计模式的时候,我会推荐一些在一些开源组件库中又代表的示例
1. 复合组件模式
如果想要设计一个定制化程度高,API
方便理解的组件,可以考虑这个模式;这种模式不会出现多层props传递的情况;
代码示例: github.com/alex83130/a…
import React from "react";
import { Counter } from "./Counter";
function Usage() {
const handleChangeCounter = (count) => {
console.log("count", count);
};
return (
<Counter onChange={handleChangeCounter}>
<Counter.Decrement icon="minus" />
<Counter.Label>Counter</Counter.Label>
<Counter.Count max={10} />
<Counter.Increment icon="plus" />
</Counter>
);
}
export { Usage };
优点:
降低组件 API 的复杂度:
不再把所有的 props
都放在一个大的父容器组件中, 再传递给子组件;
而是直接将相应的props
附加在相应的子组件上。这样做API
的复杂度是最低的。
灵活的UI结构:
这种模式下;开发出的组件有较强的的 UI
灵活性;允许从单个组件创建各种情况。例如,用户可以更改子组件的顺序或者定义应该显示哪一个
职责分离:
更多的逻辑以及组件的 state
在 Counter
组件中,通过 React.Context
共享 state
以及方法给所有的子节点,做到组件间的通讯
;每个组件都有比较明确的分工
缺点:
界面灵活性太大:
拥有灵活性的同时,还有可能引发意外行为; 例如
- 将不需要的组件的子组件放置在一起
- 将组件的子组件弄乱
- 忘记包含强制子组件 使用不当,就会出现各种奇怪问题,而且会导致做出来的系统没有按照标准的公司交互规范去做;
写更多的 JSX:
这样设计出的组件将会增加JSX
的行数;如果过使用的 eslint、Prettier
等code formatter工具可能行数更多;
从单个组件来看没有大问题,整个页面随着使用组件的变多,就会出现较大的影响
准则
- Inversion of control: 1/4
- Implementation complexity: 1/4
使用该模式的组件库
2. Props 受控
这个模式理解比较容易,组件转换为受控组件,通过直接修改 Props
影响组件内部的状态;一般在输入性组件中比较常用这种模式
Github: github.com/alex83130/a…
import React, { useState } from "react";
import { Counter } from "./Counter";
function Usage() {
const [count, setCount] = useState(0);
const handleChangeCounter = (newCount) => {
setCount(newCount);
};
return (
<Counter value={count} onChange={handleChangeCounter}>
<Counter.Decrement icon={"minus"} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count max={10} />
<Counter.Increment icon={"plus"} />
</Counter>
);
}
export { Usage };
优点:
提供更多的控制:
由于组件的一些状态暴露在组件之外,允许用户通过控制它,而直接影响组件
缺点:
实现复杂度高:
需要有更多的逻辑对受控值进行监控,并要做判断出入的值类型是否正确等方面;实现起来复杂;
准则
- Inversion of control: 2/4
- Implementation complexity: 1/4
使用该模式的组件库
3. 自定义钩子
将主要的逻辑转移到一个自定义钩子中。用户可以访问这个钩子,并公开了几个内部逻辑(状态、处理程序) ,使用户能够更好地控制组件。
Github: github.com/alex83130/a…
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";
function Usage() {
const { count, handleIncrement, handleDecrement } = useCounter(0);
const MAX_COUNT = 10;
const handleClickIncrement = () => {
//Put your custom logic
if (count < MAX_COUNT) {
handleIncrement();
}
};
return (
<>
<Counter value={count}>
<Counter.Decrement
icon={"minus"}
onClick={handleDecrement}
disabled={count === 0}
/>
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment
icon={"plus"}
onClick={handleClickIncrement}
disabled={count === MAX_COUNT}
/>
</Counter>
<button onClick={handleClickIncrement} disabled={count === MAX_COUNT}>
Custom increment btn 1
</button>
</>
);
}
export { Usage };
优点:
提供更多的控制:
用户可以在钩子和 JSX
元素之间插入自己的逻辑,允许修改组件的默认行为
缺点:
实现复杂度:
由于逻辑部分与呈现部分分离,因此用户必须将两者链接起来。要正确实现组件,需要对组件的工作方式有很好的理解
准则
- Inversion of control: 2/4
- Implementation complexity: 2/4
使用该模式的组件库
4. Props Getters
关于[模式3]中的自定义 Hooks
提供了很好的控制方式;但是比较难以集成,使用者需要按照组件提供的 Hooks
与 React native Hooks、定义state
相结合进行编写逻辑;提高了集成的复杂度;
Props Getters 模式试图去掩盖这种模式的复杂度;通过 自定义 hooks
或者 ref
,返回一系列的函数或者属性;每个函数和属性都有比较明确的命名,方便使用者将 JSX
正确的关联起来;
Github: github.com/alex83130/a…
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";
const MAX_COUNT = 10;
function Usage() {
const {
count,
getCounterProps,
getIncrementProps,
getDecrementProps
} = useCounter({
initial: 0,
max: MAX_COUNT
});
const handleBtn1Clicked = () => {
console.log("btn 1 clicked");
};
return (
<>
<Counter {...getCounterProps()}>
<Counter.Decrement icon={"minus"} {...getDecrementProps()} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment icon={"plus"} {...getIncrementProps()} />
</Counter>
<button {...getIncrementProps({ onClick: handleBtn1Clicked })}>
Custom increment btn 1
</button>
<button {...getIncrementProps({ disabled: count > MAX_COUNT - 2 })}>
Custom increment btn 2
</button>
</>
);
}
export { Usage };
优点:
易用性:
方便将组件集成到已有的页面中;使用者只需要将相应的 getter
放置在合适的 JSX
中,而不需要再定义一些 state
赋值给组件
缺点:
缺乏可见性:
犹豫整过组件的使用以及赋值等操作,均有 getter
进行管理;导致整个使用流程是不透明的。用户想要修改部分逻辑,就需要要详细的知道每个 getter
相应的逻辑
准则
- Inversion of control: 3/4
- Integration complexity: 3/4
使用该模式的组件库
总结
以上的4种模式,应该已经覆盖了大部分组件的设计方式;
总体来说,设计的组件越灵活,功能也就越强大;但是,用户的学习成本就越高,远离了即插即用的思维模式。作为开发人员,建议大家根据自己的业务逻辑以及使用人群,灵活使用以上的设计模式;同时也可以结合使用。