前言
由于不可避免的遗忘曲线和最初潦草学习等诸多因素,我获得的某些技术点会存在没有吃透或者出现了被遗忘的情况。幸运的是,我有经验积累这个增益buff,能够不时的有新的开发思路诞生。
此消彼长,也不是长久之计。于是我想通过温故知新的方式,重拾部分技术知识或者提炼一些有趣的“奇思妙想”。
本篇主要重新梳理一下React的HOC。
温故而知新,可以为师矣。
带着问题去寻找答案
如果我还是单纯的看一遍文档,到最后估计收获不大。所以,我在开始前,列了一些自己的疑问以及带着这些疑问要找到答案。
- 为什么提起HOC总会看到Mixins的身影?
- 横切关注点到底是什么?为什么文档里面提到HOC解决了横切关注点问题?
- React为什么用HOC替代Mixins?
- 为什么我平时用不上HOC?
- 我怎样才能用的上HOC?
......
HOC和它的兄弟Mixins
先来聊一下HOC和Mixins这对兄弟的前情提要。
为了解决横切关注点问题,早先React也是采用Mixins方式,但是随着组件的增加,Mixins方式带来了一系列的问题,比如隐式依赖、名称冲突、代码复杂性增加等。
所以React采用HOC的设计模式替代Mixins解决横切关注问题。
名词解释时间
横切关注点
React文档里面提到
如果完全不同的组件有相似的功能,这就会产生“横切关注点(cross-cutting concerns)“问题。
知乎文章 《面向对象困境之 —— 横切关注点》 ,介绍了关注点和横切关注点,并举了日志功能的例子,写的非常好,我把前面的介绍贴出来,帮助大家更好理解。
什么是关注点(Concern)?
A Concern is a term that refers to a part of the system divided on the basis of the functionality.
关注点是指基于功能划分系统的一部分。
什么是横切关注点(Crosscutting Concern)?
部分关注点「横切」程序代码中的数个模块,即在多个模块中都有出现,它们即被称作「横切关注点(Cross-cutting concerns, Horizontal concerns)」。
这样说好像还是特别抽象?那我们举个例子。
日志功能就是横切关注点的一个典型案例。日志功能往往横跨系统中的每个业务模块,即“横切”所有需要日志功能的类和方法体。所以我们说日志成为了横切整个系统对象结构的关注点 —— 也就叫做横切关注点啦。
解决横切关注点不是提炼某个公共方法那么简单,公共方法具有功能的完整性,如果想要适配所有的业务模块,随着业务的迭代,公共方法中的代码会越来越复杂。所以Vue框架中采用Mixins功能,React使用HOC替换Mixins来解决横切关注点问题。
Mixins
Vue官网中对 mixins介 绍如下
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
官网例子
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
上面的例子通过mixins方式将对象myMixin的方法添加到Component组件上去。
HOC
React官网对HOC介绍如下
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
具体而言,高阶组件是参数为组件,返回值为新组件的函数。
从这两段话中,我们不难提炼出复用组件、高级技巧、设计模式、参数为组件、返回为新组件的函数等关键词,发散一下思维,HOC允许开发者在定义一个特定的地方定义某个逻辑,并在许多组件之间共享它,进而解决横切关注点的问题。
Mixins和HOC,为什么React选择了后者?
Mixins的诞生很好的帮助解决了功能复用的问题,但是随着时间的推移,Mixins也逐渐暴露出它的短板。
Mixins的短板
React对于Mixins带来的麻烦,有一篇文章(文章地址)进行了详细的介绍,简单总结为一下几点。
引入隐式依赖
JavaScript 是一种动态语言,因此很难强制执行或记录这些依赖关系。一般组件可以引入多个mixin,所以在使用某个mixin中的方法时,其实并不能够区分是使用的哪个mixin的方法,一旦有人移除了某个被引用的方法,而没有将所有依赖该方法的地方修改成正确的依赖,原本正常的功能就会发生错误。
这些隐含的依赖关系会给开发增加难度。
导致名称冲突
不能保证两个特定的 mixin 可以一起使用。例如,如果 FluxListenerMixin 定义了 handleChange(),而 WindowSizeMixin 定义了 handleChange(),则不能一起使用。也不能在自己的组件上定义具有此名称的方法。
另外,如果与第三方包中的 mixin 有名称冲突,不能只重命名它的方法。相反,必须在组件上使用笨拙的方法名称以避免冲突。
导致复杂性滚雪球
即使 mixin 一开始很简单,但随着时间的推移,它们往往会变得复杂。
一个mixin可以被多个组件引入,导致使用相同 mixin 的组件变得越来越耦合。任何新功能都会被添加到使用该 mixin 的所有组件中。如果不复制代码或在 mixin 之间引入更多的依赖关系和间接性,就无法拆分 mixin 的“更简单”部分,也就导致mixin的复杂性越来越高。
HOC是怎么做的?
为了解决功能复用问题,同时为了规避Mixins产生的问题,HOC做了以下设计。
透传props
将与HOC自身无关的props透传,提升使用的灵活性,降低组件间的耦合性。
组合HOC
将HOC与容器组件组合,不直接修改传入组件的HOC,避免引入隐式依赖。
最大化可组合性
HOC可以接收多个参数,这样可以降低单一参数带了的功能复杂性不断增加的问题。
HOC的实际应用
名将的归宿是战场,技术的归宿是实践。
通过探索HOC的实际应用,也能能帮助解决“为什么我平时用不上HOC?”和“我怎样才能用的上HOC?”两个疑问。
我先想明白了为什么我平时用不上HOC
因为我没有跳出思维定式的圈子,在早期的业务开发中,更多的是封装完整的功能组件,对于高阶组件没有理解的很透彻,一方面是不知道用在哪,另一方面是使用了可能也没有想到这种设计模式就是HOC。
所以,我后来就开始注意,在实际开发中,哪些能用HOC,以及使用之后,是否开发效率和代码可维护性变的更高。
哪些功能可以让你用上HOC
一通百通,一悟千悟。
我将项目中用到HOC的功能进行了整理归纳,大致有以下应用场景。
注:是项目中用到的,并不全是我一个人开发的,有很多是我同事的智慧。
按钮权限控制
按钮权限控制几乎是后台管理系统必备的功能。
功能分析
用一句话概括一个基础的按钮权限控制功能,即不同角色用户可进行的页面操作可通过数据配置进行控制。
功能实现
新增了PowerButton组件
- 因为每个页面的每个按钮都需要加权限控制,所以使用组合的方式,将权限组件包裹在按钮容器外侧达到控制的目的;
- 缓存权限控制按钮数据,方便统一管理,该数据为页面pathname对应权限按钮数组的枚举;
- 获取权限控制按钮数据,获取缓存中powerButtonData的值和当前页面的pathname,进而获取当前页面的权限按钮数组;
- 获取当前按钮的code值在权限按钮数组中的索引。如果索引值大于-1表示当前按钮存在,页面也会展示按钮;反之则表示按钮不存在,页面不会展示该按钮;
/**
* @description 按钮权限控制
*/
import React from 'react';
class PowerButton extends React.Component {
static propTypes = {
code: PropTypes.func.isRequired, // 操作按钮的code值
};
constructor(props) {
super(props);
this.state = {
powerIndex: -1, // 当前按钮在用户拥有的按钮列表中的索引
};
}
componentDidMount() {
/** @name 缓存中的用户拥有的权限按钮数据 */
let powerButtonData = JSON.parse(sessionStorage.getItem(powerButtonData) || '{}');
/** @name 当前打开页面 */
const pathname = window.location.pathname;
/** @name 权限按钮数组对象 */
const buttonList = powerButtonData[pathname];
if (buttonList) {
const powerIndex = buttonList.findIndex(btn => btn.title === code);
this.setState({
powerIndex,
});
}
}
render() {
const { powerIndex } = this.state;
const { children } = this.props;
if (powerIndex > -1) {
return <>{children}</>;
} else {
return null;
}
}
}
export default PowerButton;
页面使用
我们目前使用的中文字符做为按钮的唯一值,这样方便业务方在后台配置每个页面的按钮权限。如果有英文字符容易配错。
import { PowerButton } from '@/components';
<PowerButton code='新增'>
<Button type='primary'>新增</Button>
</PowerButton>
页面鉴权
ToC的业务,总少不了对于用户登录的处理功能,主要在于某些页面信息必须等拿到用户登录的信息之后才能正常的展示,比如用户的订单信息、收获地址,但是一些页面不需要用户登录也可以正常在用户的设备上展示,比如首页、商品详情页等。这个时候就需要对页面权限做处理页就是我们常说的页面鉴权功能。
功能分析
- 用户在进入不需要登录鉴权的页面时,会直接打开页面,设备上展示完整的页面内容;
- 用户在进入需要登录鉴权的页面时,前端会进行路由拦截(整个处理过程用户无感知),内部处理判断登录的逻辑,如果已登录直接进入页面,如果未登录进入登录页,登录成功之后再进入页面;
功能实现
新增Authority组件
/**
* @description 页面鉴权
*/
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
class Authority extends Component {
constructor(props) {
super(props);
}
/**
* 重定向方法
* @param {string} path 需要重定向的地址
* @return {void} 无
*/
redirect(path) {
let { pathname, search } = this.props.location;
// 重定向指定path
return <Redirect to={path} />;
}
render() {
const { path, component } = this.props;
const { pathname, search } = this.props.location;
let query = decodeQuery(search);
// =>true: 如果没有用户信息,跳转登录页
if (!sessionStorage.member) return this.redirect('/login');
return <Route path={path} component={component} />;
}
}
export default Authority;
页面使用
- 不需要鉴权的页面模块,会直接进行路由跳转;
- 需要鉴权的页面模块,使用Authority组件进行鉴权处理;
{
routes.map(item => {
//不需要鉴权的模块
if (item.notAuthorityFlag) {
return <Route key={item.path} path={item.path} component={item.component} />;
}
return <Authority key={item.path} path={item.path} component={item.component} />;
});
}
总结
上面HOC的实际应用,是否觉得功能挺眼熟的,空闲的时候,不妨捋捋自己项目的代码,没准会有不错的收获。
文章写的很基础,是因为作者本人还处于不断学习的阶段。不过温故确实能够知新。
虽然作者本人的功力还没有足够深厚,但是正在通过温习学习收获成长。希望未来奔跑着,能追上一些大佬的背影。