高阶组件的应用场景

不谈场景的技术就是在耍流氓,如何在业务场景中使用高阶组件?

权限控制

利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别页面元素级别,这里以页面级别来举一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// HOC.js
function withAdminAuth(WrappedComponent) {
return class extends React.Component {
state = {
isAdmin: false,
}
async componentWillMount() {
const currentRole = await getCurrentUserRole();
this.setState({
isAdmin: currentRole === 'Admin',
});
}
render() {
if (this.state.isAdmin) {
return <WrappedComponent {...this.props} />;
} else {
return (<div>您没有权限查看该页面,请联系管理员!</div>);
}
}
};
}

其实还可以更高效,就是在高阶组件之上再抽象一层,无需实现各种 withXXXAuth 高阶组件,因为这些高阶组件本身代码就是高度相似的,所以我们要做的就是实现一个 返回高阶组件的函数,把 变的部分(Admin、VIP) 抽离出来,保留 不变的部分,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// HOC.js
const withAuth = role => WrappedComponent => {
return class extends React.Component {
state = {
permission: false,
}
async componentWillMount() {
const currentRole = await getCurrentUserRole();
this.setState({
permission: currentRole === role,
});
}
render() {
if (this.state.permission) {
return <WrappedComponent {...this.props} />;
} else {
return (<div>您没有权限查看该页面,请联系管理员!</div>);
}
}
};
}

可以发现经过对高阶组件再进行了一层抽象后,前面的 withAdminAuth 可以写成 withAuth(‘Admin’) 了,如果此时需要 VIP 权限的话,只需在 withAuth 函数中传入 ‘VIP’ 就可以了。

有没有发现和 react-redux 的 connect 方法的使用方式非常像?没错,connect 其实也是一个 返回高阶组件的函数

组件渲染性能追踪

借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Home extends React.Component {
render() {
return (<h1>Hello World.</h1>);
}
}
function withTiming(WrappedComponent) {
return class extends WrappedComponent {
constructor(props) {
super(props);
this.start = 0;
this.end = 0;
}
componentWillMount() {
super.componentWillMount && super.componentWillMount();
this.start = Date.now();
}
componentDidMount() {
super.componentDidMount && super.componentDidMount();
this.end = Date.now();
console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);
}
render() {
return super.render();
}
};
}

export default withTiming(Home);
页面复用

假设我们有两个页面 pageA 和 pageB 分别渲染两个分类的电影列表,普通写法可能是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// pages/page-a.js
class PageA extends React.Component {
state = {
movies: [],
}
// ...
async componentWillMount() {
const movies = await fetchMoviesByType('science-fiction');
this.setState({
movies,
});
}
render() {
return <MovieList movies={this.state.movies} />
}
}
export default PageA;

// pages/page-b.js
class PageB extends React.Component {
state = {
movies: [],
}
// ...
async componentWillMount() {
const movies = await fetchMoviesByType('action');
this.setState({
movies,
});
}
render() {
return <MovieList movies={this.state.movies} />
}
}
export default PageB;

页面少的时候可能没什么问题,但是假如随着业务的进展,需要上线的越来越多类型的电影,就会写很多的重复代码,所以我们需要重构一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const withFetching = fetching => WrappedComponent => {
return class extends React.Component {
state = {
data: [],
}
async componentWillMount() {
const data = await fetching();
this.setState({
data,
});
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
}
}

// pages/page-a.js
export default withFetching(fetching('science-fiction'))(MovieList);
// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);
// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);

会发现 withFetching 其实和前面的 withAuth 函数类似,把 变的部分(fetching(type)) 抽离到外部传入,从而实现页面的复用。

装饰者模式?高阶组件?AOP?

高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件(函数或类)后在函数内部对该组件(函数或类)进行功能的增强(不修改传入参数的前提下),最后返回这个组件(函数或类),即允许向一个现有的组件添加新的功能,同时又不去修改该组件,属于 包装模式(Wrapper Pattern) 的一种。

什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为

相比于使用继承,装饰者模式是一种更轻便灵活的做法。

使用装饰者模式实现** AOP**:

面向切面编程(AOP)和面向对象编程(OOP)一样,只是一种编程范式,并没有规定说要用什么方式去实现 AOP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在需要执行的函数之前执行某个新添加的功能函数
Function.prototype.before = function(before = () => {}) {
return () => {
before.apply(this, arguments);
return this.apply(this, arguments);
};
}
// 在需要执行的函数之后执行某个新添加的功能函数
Function.prototype.after = function(after = () => {}) {
return () => {
const result = after.apply(this, arguments);
this.apply(this, arguments);
return result;
};
}

可以发现其实 before 和 after 就是一个 高阶函数,和高阶组件非常类似。

面向切面编程(AOP)主要应用在 与核心业务无关但又在多个模块使用的功能比如权限控制、日志记录、数据校验、异常处理、统计上报等等领域

类比一下 AOP 应该就知道高阶组件通常是处理哪一类型的问题了。

总结

React 中的 高阶组件 其实是一个非常简单的概念,但又非常实用。在实际的业务场景中合理的使用高阶组件,可以提高代码的复用性和灵活性。

最后的最后,再对高阶组件进行一个小小的总结:

高阶组件 不是组件,是 一个把某个组件转换成另一个组件的 函数
高阶组件的主要作用是 代码复用
高阶组件是 装饰器模式在 React 中的实现