(一)前言
这里提到的设计模式并不是编程通用的设计模式,如常说的单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式等。而是在设计 React 组件时的一些解决方案与技巧,包括以下几种:
(1) 容器与展示组件
(2) 高阶组件
(3) render props
(4) context 模式
(5) 组合组件
(6) 继承
当然概念部分,大家能根据名字猜出,但是我还是要为每种,单独给出demo详细说下。
设计模式本身是因为产品需求,解决特定的物业场景,并非完全需要按照这样,所以个人开发中建议形成自己的开发模式。
(二)容器与展示组件
这可能是目前应用最广泛,也是最简单的,大家最推崇的设计方案。其实大概意思就是,将组件分为两种。
一个为容器组件,负责与外部数据进行交互,比如处理业务逻辑(连接redux),
一个为展示组件,只通过props传递业务逻辑,state控制内部交互,不包含任何业务逻辑,也不与外部数据源(连接redux)进行沟通,现在我来举个例子。
需求: 当我们点击按钮后,会去接口拉去数据,并将返回的数据渲染到视图,当然这种需求,简单可以将全部视图写在一起,但是我们需要使用容器和展示组件的方式来组织代码,那么代码如下
// 容器组件 textContainer.js
import React from 'react';
import Text from './text';
class TextContainer extends React.Component {
state = {
text: '',
};
getData = () => {
// 模拟异步请求
setTimeout(() => {
this.setState({ text: '测试数据' });
}, 1000);
};
render() {
const {
state: {
text,
},
} = this;
return (
);
}
}
export default TextContainer;
// 展示组件 text.js
import React from 'react';
import PropTypes from 'prop-types';
class Text extends React.PureComponent {
render() {
const {
props: {
text,
onClick,
},
} = this;
return (
接口返回的数据:
{text}
);
}
}
// 这里我建议,不是必须传入的参数,尽量不使用isRequired验证
// 比如input value 肯定是必须的 但是如果不做受控组件,那么回调onChange就不是必须
Text.defaultProps = {
text: '',
onClick: () => null,
};
Text.propTypes = {
text: PropTypes.string,
onClick: PropTypes.func,
};
export default Text;
从上面代码我们看出,我们采用软件设计原则中的“责任分离”, 即让一个模块只负责责任尽量单一
容器展示组件这个模式所解决的问题在于,当我们切换数据获取方式时,只需在容器组件修改相应逻辑即可,展示组件无需做改动。展示组件可完全不变,展示组件有了更高的可复用性。
但该模式的缺点也在于将一个组件分成了两部分,增加了代码跳转的成本。并不是说组件包含从外部获取数据,就要将其拆成容器组件与展示组件。
(三)高阶组件
1.概念:
高阶组件不是组件,是 增强函数,可以输入一个元组件,返回出一个新的增强组件;
高阶组件的主要作用是 代码复用,操作状态和参数;
2. 分类(主要两个。一个属性代理,一个反向继承)
这里有个获取display name的函数,下面新增的高阶组件都会调用这个方法,就不再次书写了
// 获取display name
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
1.1 默认参数: 可以为组件包裹一层默认参数;
export function ProxyHoc(Component) {
const NewComponent = (props) => {
const newProps = {
name: '高阶组件增加的属性',
age: 1,
};
return ;
};
return NewComponent;
}
1.2 提取状态: 可以通过 props 将被包裹组件中的 state 依赖外层,例如用于转换受控组件:
现在我们来实现一个简单表单高阶组件,不包含表单验证。
// withForm.js
import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';
import { getDisplayName } from './utils';
export default function WithForm(Component) {
class Enhance extends React.Component {
static displayName = `WithForm(${getDisplayName(Component)})`;
state = {
form: {},
};
onChange = key => (e) => {
const { form } = this.state;
form[key] = e.target.value;
this.setState({
form,
});
};
handleSubmit = () => this.state.form;
getField = fieldName => ({
onChange: this.onChange(fieldName),
});
render() {
const newProps = {
...this.props,
getForm: this.handleSubmit,
setFormItem: this.getField,
};
return ( );
}
}
// 自动拷贝所有非 React 的静态方法
hoistNonReactStatic(Enhance, Component);
return Enhance;
}
我们需要使用这个表单控件时候,那么代码如下
import React from 'react';
// components
import HocForm from './components/hoc/hocForm';
// style
import './app.css';
class App extends React.Component {
render() {
const {
props: { setFormItem, getForm },
} = this;
return (
表单数据:
{JSON.stringify(getForm())}
);
}
}
export default HocForm(App);
我们可以通过props,设置表单需要的key,最后调用getForm得到内部表单数据。
1.3 包裹组件: 可以为被包裹元素进行一层包装,
import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';
import { getDisplayName } from './utils';
export default function WithMask(Component) {
class Enhance extends React.Component {
static displayName = `WithMask(${getDisplayName(Component)})`;
render() {
return (
);
}
}
// 自动拷贝所有非 React 的静态方法
hoistNonReactStatic(Enhance, Component);
return Enhance;
}
这里给mask封装来一层背景包裹组件。
export default function WithHocSimple(Component) {
return class extends Component {
render() {
return super.render();
}
};
}
反向继承其实就是 一个函数接受一个 Component 组件作为参数传入,并返回一个继承了该传入 Component 组件的类,且在该类的 render() 方法中返回 super.render() 方法。
会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 Component。
反向继承可以用来做什么:
操作 state
渲染劫持(Render Highjacking)
2.1 操作state
高阶组件中可以读取、编辑和删除 WrappedComponent 组件实例中的 state。甚至可以增加更多的 state 项,但是 非常不建议这么做 因为这可能会导致 state 难以维护及管理。
import React from 'react';
export default function WithHocLoading(Component) {
return class extends Component {
render() {
return (
Debugger Component Logging...
state:
{JSON.stringify(this.state, null, 4)}
props:
{JSON.stringify(this.props, null, 4)}
{super.render()}
);
}
};
}
在这个例子中利用高阶函数中可以读取 state 和 props 的特性,对 Component 组件做了额外元素的嵌套,把 Component 组件的 state 和 props 都打印了出来,
2.2 渲染劫持
我们来实现一个通过修改由 render() 输出的 React 元素树
import React from 'react';
export default function WithTree(Component) {
return class extends Component {
render() {
const tree = super.render();
const newProps = {};
if (tree && tree.type === 'div') {
newProps.className = 'App add-view';
}
const props = {
...tree.props,
...newProps,
};
const newTree = React.cloneElement(tree, props, tree.props.children);
return newTree;
}
};
}
模式所解决的问题
同样的逻辑我们总不能重复写多次。高阶组件起到了抽离共通逻辑的作用。同时高阶组件的嵌套使用使得代码复用更加灵活了。
注意以下几点
(四)render props
概念:
指一种在 React 组件之间使用一个值为函数的prop来共享代码的简单技术。同高阶组件一样,render props的引入也是为了解决复用业务逻辑。
需求:
我们实现一个判断性别的组件,提供给购物车和商品列表组件使用,将共用性别判断逻辑抽离出来。
// providerGender.js
import React from 'react';
import PropTypes from 'prop-types';
const ProviderGender = (props) => {
// 判断是否是女性用户
const isWoman = Math.random() > 0.1;
if (isWoman) {
const allProps = { add: '高阶组件增加的属性', ...props };
return props.children(allProps);
}
return 女士专用,男士无权浏览;
};
ProviderGender.defaultProps = {
children: () => null,
};
ProviderGender.propTypes = {
children: PropTypes.func,
};
export default ProviderGender;
然后我们分别定义商品列表组件
// list.js
import React from 'react';
import PropTypes from 'prop-types';
const List = ({ add, name }) => (
{`${name}:`}
{add}
);
List.defaultProps = {
name: '',
add: '',
};
List.propTypes = {
name: PropTypes.string,
add: PropTypes.string,
};
export default List;
现在我们来该如何使用两者组合
import React from 'react';
// components
// render props
import ProviderGender from './components/renderProps/providerGender';
import List from './demo/shoppingPropsDemo';
// style
import './app.css';
class App extends React.Component {
state = {
name: '商品名称列表',
};
onToggleList = () => {
this.setState(state => ({
...state,
name: state.name === '商品名称列表' ? '购物车列表' : '商品名称列表',
}));
};
render() {
const {
state: { name },
} = this;
return (
{props =>
}
);
}
}
export default App;
上面代码中可以看出,属性代理的方案高阶组件也能实现,但是 render props 却有更加强大的一个点就是,因为可以在render中,所以能自定义给list组件,添加新的props,而不需要修改ProviderGender组件,这是高阶不能在render中调用,所不具备的最大优势。
当然,render props 的使用可以不局限在利用 children,组件任意的 prop 属性都可以达到相同效果,比如我们用 renderChildren 这个 prop 实现上面相同的效果。
比如我们将children改为renderChildren函数。
// providerGender.js
import React from 'react';
import PropTypes from 'prop-types';
const ProviderGender = (props) => {
// 判断是否是女性用户
const isWoman = Math.random() > 0.1;
if (isWoman) {
const allProps = { add: '高阶组件增加的属性', ...props };
return props.renderChildren(allProps);
}
return 女士专用,男士无权浏览;
};
ProviderGender.defaultProps = {
renderChildren: () => null,
};
ProviderGender.propTypes = {
renderChildren: PropTypes.func,
};
export default ProviderGender;
// app.js
class App extends React.Component {
render() {
const {
state: { name },
} = this;
renturn (
}
/>
)
}
}
注意事项:
将 Render Props 与 React.PureComponent 一起使用时要小心!如果你在 Provider 属性中创建函数,那么使用 render props 会抵消使用React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render props 将会生成一个新的值。
比如上面例子就是在每次render,重新生成一个新的函数作为 的 prop。那么解决方案就是,将匿名函数,提到外层组件的方法
class App extends React.Component {
state = {
name: '商品名称列表',
};
onToggleList = () => {
this.setState(state => ({
...state,
name: state.name === '商品名称列表' ? '购物车列表' : '商品名称列表',
}));
};
renderList = (props) => {
const {
state: { name },
} = this;
return (
);
};
render() {
return (
{this.renderList}
);
}
}
(五)Context模式
概念:
React 的 Context 接口提供了一个无需为每层组件手动添加 props ,就能在组件树间进行数据传递的方法。
// themeProvider.js
import React from 'react';
import PropTypes from 'prop-types';
const ThemeContext = React.createContext();
const ThemeProvider = ThemeContext.Provider;
export const ThemeConsumer = ThemeContext.Consumer;
const Context = ({ value, children }) => (
{children}
);
Context.defaultProps = {
value: {},
};
Context.propTypes = {
value: PropTypes.objectOf(PropTypes.any),
children: PropTypes.node.isRequired,
};
export default Context;
// consumer.js
import React from 'react';
// import PropTypes from 'prop-types';
import { ThemeConsumer } from '../components/context/themeProvider';
const Consumer = () => (
{
theme => (
内容区域
)
}
);
Consumer.defaultProps = {};
Consumer.propTypes = {};
export default Consumer;
在app.js中调用为
import React from 'react';
// context
import ThemeProvider from './components/context/themeProvider';
import Consumer from './demo/consumer';
// style
import './app.css';
class App extends React.Component {
render() {
return (
);
}
}
export default App;
我们看到,consumer.js中并直接传递props,通过ThemeContext.Consumer直接读取到context中的theme。
优势:
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据,可以跨组件访问一样的数据。
(五)组合组件模式
借用组合组件,使用者只需要传递子组件,子组件所需要的props在父组件会封装好,引用子组件的时候就没必要传递所有props了。
组合组件核心的两个方法是React.Children.map和React.cloneElement。React.Children.map 用来遍历获得组件的子元素。React.cloneElement 则用来复制元素,这个函数第一个参数就是被复制的元素,第二个参数可以增加新产生元素的 props ,我们就是利用这个函数,把想要的 props 传入子元素。
现在我们来实现一个antd的tabs组件,我们先看他如何使用的
import { Tabs } from 'antd';
const { TabPane } = Tabs;
function callback(key) {
console.log(key);
}
ReactDOM.render(
Content of Tab Pane 1
Content of Tab Pane 2
Content of Tab Pane 3
,
mountNode,
);
首先我们实现tabs
import React from 'react';
import PropTypes from 'prop-types';
class Tabs extends React.Component {
state = {
activeIndex: 0,
};
onClickItem = (index) => {
this.setState({ activeIndex: index });
this.props.onChange(index);
};
renderHeaderItem = () => {
const {
state: { activeIndex },
props: { children },
} = this;
return React.Children.map(children, (childElem, index) => {
if (!childElem.type) return null;
const active = activeIndex === index;
return (
this.onClickItem(index)}
>
{childElem.props.tab}
);
});
};
render() {
const {
state: { activeIndex },
props: { children },
} = this;
return (
{this.renderHeaderItem()}
{
React.Children.map(children, (childElem, index) => {
if (!(childElem.type && activeIndex === index)) return null;
return React.cloneElement(childElem, {
activeIndex,
index,
});
})
}
);
}
}
Tabs.defaultProps = {
onChange: () => null,
};
Tabs.propTypes = {
children: PropTypes.node.isRequired,
onChange: PropTypes.func,
};
export default Tabs;
然后实现tabPane.js
import React from 'react';
import PropTypes from 'prop-types';
const TabPane = ({ children }) => (
{children}
);
TabPane.defaultProps = {
// activeIndex: 0,
// index: 0,
children: '',
};
TabPane.propTypes = {
// activeIndex: PropTypes.number,
// index: PropTypes.number,
children: PropTypes.node,
};
export default TabPane;
在app.js中调用为
import React from 'react';
// tabs
import Tabs from './components/tabs/tabs';
import TabPane from './components/tabs/tabPane';
// style
import './app.css';
class App extends React.Component {
render() {
return (
内容1
内容2
内容3
);
}
}
export default App;
模式所解决的问题
组合组件设计模式一般应用在一些共享组件上。如 select 和 option , Tab 和TabItem 等,通过组合组件,使用者只需要传递子组件,子组件所需要的 props 在父组件会封装好,引用子组件的时候就没必要传递所有 props 了。我们可以在共享的组件中运用这种模式,简化组件使用者的调用方式,antd 当中你就能看到许多组合组件的使用。
(六)继承模式
我们最后来谈谈OOP的继承模式。
如果组件定义为class组件,那么我们当然可以使用继承的模式来实现组件的复用。我们通过一个基类来实现一些通用的逻辑,然后再通过继承分别实现两个子类。
// base.js
import React from 'react';
class Base extends React.PureComponent {
getHeader = () => null;
render() {
return (
{this.getHeader()}
这里是通用逻辑
);
}
}
export default Base;
// demo.js
import React from 'react';
// import PropTypes from 'prop-types';
// components
import Base from '../components/oop/base';
class Demo1 extends Base {
getHeader = () => demo1;
}
class Demo2 extends Base {
getHeader = () => Demo2;
}
export { Demo1, Demo2 };
从上面代码可以看出,可以将通用逻辑封装在基本类中,通过继承,实现方法,达到复用。
缺点:
这两个缺点使得继承相对于组合组件缺少了灵活性以及可扩展性。
(七)结语
通过上面例子我们总结常见的模式,但是请记住,组合优于继承!组件的复用请第一时间想到使用组合而非继承。