React设计模式和最佳实践总结
# 一、组件实践
# 1.1 设计原则
- 保持接口小,
props数量要少 - 根据数据边界来划分组件,充分利用组合
- 把 state 往上层组件提取,让下层组件只需要实现为纯函数
# 1.2 组件划分
任何一个复杂组件都是从简单组件开始的,一开始我们在 render 函数里写的代码不多,但是随着逻辑的复杂,JSX 代码越来越多,于是,就需要拆分函数中的内容
- 在 React 中,有一个误区,就是把 render 中的代码分拆到多个 renderXXXX 函数中去,比如下面这样
class StopWatch extends React.Component {
render() {
const majorClock = this.renderMajorClock();
const controlButtons = this.renderControlButtons();
const splitTimes = this.renderSplitTimes();
return (
<div>
{majorClock}
{controlButtons}
{splitTimes}
</div>
);
}
renderMajorClock() {
//TODO: 返回数字时钟的JSX
}
renderControlButtons() {
//TODO: 返回两个按钮的JSX
}
renderSplitTimes() {
//TODO: 返回所有计次时间的JSX
}
}
用上面的方法组织代码,当然比写一个巨大的 render 函数要强,但是,实现这么多 renderXXXX 函数并不是一个明智之举,因为这些 renderXXXX 函数访问的是同样的 props 和 state,这样代码依然耦合在了一起。更好的方法,是把这些 renderXXXX 重构成各自独立的 React 组件,像下面这样
class StopWatch extends React.Component {
render() {
return (
<div>
<MajorClock>
<ControlButtons>
<SplitTimes>
</div>
);
}
}
const MajorClock = (props) => {
//TODO: 返回数字时钟的JSX
};
const ControlButtons = (props) => {
//TODO: 返回两个按钮的JSX
};
const SplitTimes = (props) => {
//TODO: 返回所有计次时间的JSX
}
我们创造了
MajorClock、ControlButtons和SplitTimes这三个组件,目前,我们并不知道它们是否应该有自己的state,但是从简单开始,首先假设它们没有自己的state,定义为函数形式的无状态组件
组件 props 的设计
使用 propTypes 来定义组件的 props
const ControlButtons = (props) => {
//TODO: 返回两个按钮的JSX
};
ControlButtons.propTypes = {
activated: PropTypes.bool,
onStart: PropTypes.func.isRquired,
onPause: PropTypes.func.isRquired,
onSplit: PropTypes.func.isRquired,
onReset: PropTypes.func.isRquired,
splits: PropTypes.arrayOf(PropTypes.number)
};
# 1.3 组件内部实现
- 尽量每个组件都有自己专属的源代码文件
- 用解构赋值(destructuring assignment)的方法获取参数 props 的每个属性值
- 利用属性初始化(property initializer)来定义 state 和成员函数
属性初始化方法
尽量不要在 JSX 中写内联函数(inline function),比如这样写,是很不恰当的
<ControlButtons
activated={this.state.isStarted}
onStart={() => { /* TODO */}}
onPause={() => { /* TODO */}}
onReset={() => { /* TODO */}}
onSplit={() => { /* TODO */}}
/>
当然,按照上面那种写法,也可以完成程序的功能,但是,会带来性能的代价。首先,每一次渲染这段
JSX,都会产生全新的函数对象,这是一种浪费;其次,因为每一次传给ControlButtons的都是新的props,这样ControlButtons也无法通过shouldComponentUpdate对props的检查来避免重复渲染
# 二、组件设计模式
# 2.1 高阶组件
- 在开发 React 组件过程中,很容易发现这样一种现象,某些功能是多个组件通用的,如果每个组件都重复实现这样的逻辑,肯定十分浪费,而且违反了“不要重复自己”(DRY,Don’t Repeat Yourself)的编码原则,我们肯定想要把这部分共用逻辑提取出来重用
- 我们说过,在 React 的世界里,组件是第一公民,首先想到的是当然是把共用逻辑提取为一个 React 组件。不过,有些情况下,这些共用逻辑还没法成为一个独立组件,换句话说,这些共用逻辑单独无法使用,它们只是对其他组件的功能加强
高阶组件的基本形式
“高阶组件”名为“组件”,其实并不是一个组件,而是一个函数,只不过这个函数比较特殊,它接受至少一个 React 组件为参数,并且能够返回一个全新的 React 组件作为结果,当然,这个新产生的 React 组件是对作为参数的组件的包装,所以,有机会赋予新组件一些增强的“神力”
一个最简单的高阶组件是这样的形式
const withDoNothing = (Component) => {
const NewComponent = (props) => {
return <Component {...props} />;
};
return NewComponent;
};
- 上面的函数 withDoNothing 就是一个高阶组件,作为一项业界通用的代码规范,高阶组件的命名一般都带 with 前缀,命名中后面的部分代表这个高阶组件的功能
就如同 withDoNothing 这个名字所说的一样,这个高阶组件什么都没做,但是从中可以看出高阶组件的基本代码套路
- 高阶组件不能去修改作为参数的组件,高阶组件必须是一个纯函数,不应该有任何副作用。
- 高阶组件返回的结果必须是一个新的 React 组件,这个新的组件的 JSX 部分肯定会包含作为参数的组件
- 高阶组件一般需要把传给自己的 props 转手传递给作为参数的组件
用高阶组件抽取共同逻辑
接下来,我们对 withDoNothing 进行一些改进,让它实现“只有在登录时才显示”这个功能
假设我们已经有一个函数 getUserId 能够从 cookies 中读取登录用户的 ID,如果用户未登录,这个 getUserId 就返回空,那么“退出登录按钮“就需要这么写:
const LogoutButton = () => {
if (getUserId()) {
return ...; // 显示”退出登录“的JSX
} else {
return null;
}
};
同样,购物车的代码就是这样
const ShoppintCart = () => {
if (getUserId()) {
return ...; // 显示”购物车“的JSX
} else {
return null;
}
};
上面两个组件明显有重复的代码,我们可以把重复代码抽取出来,形成 withLogin 这个高阶组件,代码如下
const withLogin = (Component) => {
const NewComponent = (props) => {
if (getUserId()) {
return <Component {...props} />;
} else {
return null;
}
}
return NewComponent;
};
如此一来,我们就只需要这样定义 LogoutButton 和 ShoppintCart:
const LogoutButton = withLogin((props) => {
return ...; // 显示”退出登录“的JSX
});
const ShoppingCart = withLogin(() => {
return ...; // 显示”购物车“的JSX
});
我们避免了重复代码,以后如果要修改对用户是否登录的判断逻辑,也只需要修改
withLogin,而不用修改每个 React 组件
高阶组件的高级用法
- 高阶组件只需要返回一个 React 组件即可,没人规定高阶组件只能接受一个 React 组件作为参数,完全可以传入多个 React 组件给高阶组件
- 比如,我们可以改进上面的 withLogin,让它接受两个 React 组件,根据用户是否登录选择渲染合适的组件
const withLoginAndLogout = (ComponentForLogin, ComponentForLogout) => {
const NewComponent = (props) => {
if (getUserId()) {
return <ComponentForLogin {...props} />;
} else {
return <ComponentForLogout{...props} />;
}
}
return NewComponent;
};
有了上面的 withLoginAndLogout,就可以产生根据用户登录状态显示不同的内容
const TopButtons = withLoginAndLogout(
LogoutButton,
LoginButton
);
链式调用高阶组件
- 高阶组件最巧妙的一点,是可以链式调用。
- 假设,你有三个高阶组件分别是 withOne、withTwo 和 withThree,那么,如果要赋予一个组件 X 某个高阶组件的超能力,那么,你要做的就是挨个使用高阶组件包装,代码如下
const X1 = withOne(X);
const X2 = withTwo(X1);
const X3 = withThree(X2);
const SuperX = X3; //最终的SuperX具备三个高阶组件的超能力
很自然,我们可以避免使用中间变量 X1 和 X2,直接连续调用高阶组件,如下
const SuperX = withThree(withTwo(withOne(X)));
对于 X 而言,它被高阶组件包装了,至于被一个高阶组件包装,还是被 N 个高阶组件包装,没有什么差别。而高阶组件本身就是一个纯函数,纯函数是可以组合使用的,所以,我们其实可以把多个高阶组件组合为一个高阶组件,然后用这一个高阶组件去包装X,代码如下
上面代码中使用的 compose,是函数式编程中很基础的一种方法,作用就是把多个函数组合为一个函数,在很多开源的代码库中都可以看到,下面是一个参考实现
依赖注入