高阶组件(HOC)是 React 生态系统的常用词汇,React 中代码复用的主要方式就是使用高阶组件,并且这也是官方推荐的做法。而在 Vue 中,官方给出的组件复用方式则是 mixin 。本文将对常用的组件复用方式( mixin 和 HOC )进行对比和实践。
组件复用场景
什么时候会需要组件复用呢?实际场景:
有一个使用了vue-router和vuex的单面应用。在N个(下面以两个为例子)独立页面功能完成后,需要增加权限控制的功能。有的页面需要特定的用户权限才能进入,否则如果强行输入url进入的话,会提示“没有权限访问本页面”。
场景介绍
下面是没有权限控制时,系统主要的几个代码文件:
页面入口main.js
import Vue from 'vue';
import store from './store';
import router from './routes';
import App from './app';
new Vue({
el: '#app',
router,
store,
render: h => h(App)
});
页面根组件app.vue
<template>
<div id="app">
<div class="app-page" v-if="user.userLoaded">
<div class="app-page-cnt">
<router-view></router-view>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
name: 'app',
data() {
return {};
},
methods: {
// vuex中的action,会从接口请求包含权限的用户信息,并保存到store中的user字段
// store中具体的代码因为比较简单,并且在这里不重要,所以就不展示了,可以自己脑补
...mapActions(['getUserDetail']),
},
computed: {
// store中的user信息,在未从接口获取返回之前,为{ userLoaded: false }
...(mapState(['user'])),
},
created() {
this.getUserDetail();
}
};
</script>
路由配置routes.js
import Router from 'vue-router';
// 以下是组件异步加载的写法, 功能上等同于直接import
const Page1 = resolve => require(['./page1'], resolve);
const Page2 = resolve => require(['./page2'], resolve);
export default {
routes: [
{ path: '/page1', component: Page1 },
{ path: '/page2', component: Page2 },
]
}
页面组件: page1.vue, page2.vue
/**************** page1.vue ****************/
export default {
template: `<div>欢迎访问传说中的 page1 !</div>`
}
/**************** page2.vue ****************/
export default {
template: `<div>欢迎访问传说中的 page2 !</div>`
}
不考虑复用
在当前场景下,要对page1和page2两个页面添加权限控制,不考虑复用时可以这么粗暴地在page1.vue和page2.vue上进行如下改造来实现。
页面组件: page1.vue, page2.vue
/**************** page1.vue ****************/
export default {
template: `
<div v-if="hasRight">欢迎访问传说中的 page1 !</div>
<div v-else>不好意思,由于不够帅,你没有权限访问本页面</div>
`,
computed: {
hasRight() { // 判断用户是否有权限进入本页面的计算属性
// 这里的user是之前在app中通过接口返回注入store的用户信息
const { rightList } = this.$store.state.user;
return rightList.indexOf('RIGHT_PAGE_1');
}
}
}
/**************** page2.vue ****************/
export default {
template: `
<div v-if="hasRight">欢迎访问传说中的 page2 !</div>
<div v-else>不好意思,由于不够帅,你没有权限访问本页面</div>
`,
computed: {
hasRight() { // 判断用户是否有权限进入本页面的计算属性
// 这里的user是之前在app中通过接口返回注入store的用户信息
const { rightList } = this.$store.state.user;
return rightList.indexOf('RIGHT_PAGE_2');
}
}
}
以上的方式,在只有两个页面的时候,可能不觉得麻烦。如果页面多了之后,就十分难维护了,同时会有大量的重复代码。
使用mixin实现复用
在使用mixin前,先把那个分散在各种页面组件中的无权限提示,提取到单独的组件中以便复用。
提取出错误提示组件: no-right-tips.vue
export default {
template: `<div>不好意思,由于不够帅,你没有权限访问本页面</div>`,
name: 'no-right-tips'
}
创建一个用于权限控制的mixin,目标是使页面组件(page1,page2)不用关心权限校验是如何运行的。在这个例子中,只需要把hasRight这个计算属性提取到mixin中。
right-mixin.js
export default {
computed: {
hasRight() {
const { rightList } = this.$store.state.user;
return rightList.indexOf('RIGHT_PAGE_?'); // 注意这里,无法确定各个页面的权限标志
}
}
}
注意看上面代码的最后一行注释。我们希望把权限验证放到mixin中,但问题是不同页面所需要的权限是不一样的啊,无法将RIGHT_PAGE_1之类的具体权限写死在mixin中。怎么办呢?用函数包一层:
right-mixin.js
export default (rightType) => ({ // rightType作为参数传入,返回特定mixin
computed: {
hasRight() {
const { rightList } = this.$store.state.user;
return rightList.indexOf(rightType); // 问题解决
}
}
})
上面的所说的用函数包一层
,听起来好low是吧?我们来给这种方式起个高逼格一点的名字吧,我们称上面的方式为高阶mixin
。是不是瞬间听起来不一样了?
这个名字听起来是不是和我们后面要讲的高阶组件
如出一辙?
我们先不纠结名字了,看看我们上面的方式如何在页面组件中使用吧:
page1.vue, page2.vue
/**************** page1.vue ****************/
import NoRightTips from './no-right-tips';
import rightmixin from './right-mixin';
export default {
mixin: [rightmixin('RIGHT_PAGE_1')],
template: `
<div v-if="hasRight">欢迎访问传说中的 page1 !</div>
<no-right-tips v-else></no-right-tips>
`,
components: {
NoRightTips
}
}
/**************** page2.vue ****************/
import NoRightTips from './no-right-tips';
import rightmixin from './right-mixin';
export default {
mixin: [rightmixin('RIGHT_PAGE_2')],
template: `
<div v-if="hasRight">欢迎访问传说中的 page2 !</div>
<no-right-tips v-else></no-right-tips>
`,
components: {
NoRightTips
}
}
经过mixin改造和错误提示的组件提取之后,代码看起来复用度提高了,职责也分明了。现在页面组件不用关心权限是怎么检验的,只用管从mixin提供的computed属性中判断检验结果,并在没有权限时直接展示公共的错误提示组件。感觉不错!
使用高阶组件(HOC)实现复用
铺垫了这么多,终于要进入主题了!在使用高阶组件之前,先简单描述一下它。我们上面起了一个高逼格的名字: 高阶mixin
,用来表示被函数包了一层的普通mixin。是有一定依据的。
维基百科对于高阶函数
的定义(点击查看):
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
React中HOC的定义(点击查看):
a higher-order component is a function that takes a component and returns a new component 高阶组件是一个方法,这个方法接收一个原始组件作为参数,并返回新的组件。
根据上面的定义,我们可以引申为:通过函数向现有XXX添加功能,就是高阶XXX。在上面mixin的例子中,通用函数,给普通mixin提供了可配置的权限检测参数,所以可称之为高阶mixin。
大家可能觉得奇怪,为什么我要用react官方里的定义来说明高阶组件呢?是因为高阶组件最开始就是在react中提出来的。下边我们看一下,如何在vue中使用高阶组件来实现上面场景中的组件复用功能。
我们创建如下高阶组件: right-hoc.js
import NoRightTips from './no-right-tips';
export default (Comp, rightType) => ({
components: {
Comp,
NoRightTips,
},
computed: {
hasRight() {
const { rightList } = this.$store.state.user;
return rightList.indexOf(rightType);
}
},
render(h) {
return this.hasRight ? h(Comp, {}) : h(NoRightTips, {});
}
})
接下来去掉页面组件中已经提取到高阶组件中的部分逻辑: page1.vue, page2.vue
/**************** page1.vue ****************/
export default {
template: `<div>欢迎访问传说中的 page1 !</div>`,
}
/**************** page2.vue ****************/
export default {
template: `<div>欢迎访问传说中的 page2 !</div>`,
}
所有的权限相关代码都抽出来了!组件回归到的之前没有权限功能时的样子。那hoc在哪里与组件结合起来呢?答案是在routes里,在使用组件的地方:
路由配置: routes.js
import Router from 'vue-router';
import rightHoc from './right-hoc';
// 以下是组件异步加载的写法, 功能上等同于直接import
const Page1 = resolve => require(['./page1'], resolve);
const Page2 = resolve => require(['./page2'], resolve);
export default {
routes: [
{ path: '/page1', component: rightHoc(Page1, 'RIGHT_PAGE_1') },
{ path: '/page2', component: rightHoc(Page2, 'RIGHT_PAGE_2') },
]
}
使用高阶组件同样实现了组件复用。而且看起来似乎更优雅?我们来对比一下高阶组件和mixin两种方式,在以上场景中的区别:
HOC
- 增加了一个hoc文件, hoc文件中引入no-right-tips
- 路由配置中,使用页面组件的地方引入并使用了hoc
mixin
- 增加了一个mixin文件
- 每个组件代码中,引入mixin、no-right-tips, 并且增加相应的模板逻辑(v-if)
在本文的场景中,使用HOC相比使用mixin有以下优势:
- 减少对原始组件的入侵,降低耦合。HOC中,原始组件只用考虑自身逻辑,不用考虑,也感知不到HOC对它做了什么。而mixin,组件在内部需要使用mixin的计算属性(更复杂的mixin还会用到生命周期和methods方法)。
- 权限控制方便集中管理,直接在routes配置中管理各个页面配置,而不是分散在各个页面组件内部。
- 避免命名冲突。如果页面自己有自己内部的权限控制,刚好也有个computed属性叫hasRight呢?在HOC下没问题,但mixin就不行了。
React中的HOC现状
其实最早在React中,也是使用mixin来实现组件功能复用的,但从v0.13.0开始,React的ES6 class组件写法中就不支持mixin了。这应该算是比较大的特性调整了。在此之后,已经使用了React的项目,可以继续使用React.createClass定义组件的方式来继续使用mixin,如果要使用ES6 class并且实现同样的组件复用,就必须使用HOC了。
React为什么做了这个决定呢?人家不是没事搞事情,而是有原因的。官方博客专门发文列举mixin可能带来的一些问题: mixin Considered Harmful。这篇文章里结合实际例子列举了mixin在React中可能带来的几个问题,并且给出了mixin迁移到HOC的一些指导。这里简单地把文章里提到的mixin可能带来的几个问题列举一下:
1、mixin会导致依赖不明确
- mixin会调用组件内部方法/数据,组件会调用mixin方法/数据, 无法保证双方方法稳定存在。
- 多个mixin同时作用时,依赖关系对于被mixin的组件来说会更困惑。
2、mixin会导致命名冲突
- 多个mixin和组件本身,方法名称会有命名冲突风险,如果遇到了,不得不重命名某些方法。
3、mixin会带来滚雪球般的复杂度
- 原文中列举了一个复杂的mixin例子。
也就是说,现在React体系中mixin已经不推荐使用,而推荐使用HOC。下图是mixin和HOC的对比:

Vue中的HOC现状
相比于React,Vue目前还是使用mixin作为官方的组件复用方式。在探索Vue中HOC的时候,发现很少有相关描述和实践和文章。在百度里搜不出来,在google里也只能搜出寥寥几个。在我找到的资源中,有一个vuejs的github issue十分有代表价值:Discussion: Best way to create a HOC。
本文的例子简单地实现了HOC,但是实际的场景可能更复杂,涉及属性传递,slots等问题。而上面的issue就是在讨论这个问题。目前这个issue已经关闭,结论有两个:
- 暂时由热心人士产出了一个npm包: vue-hoc来帮助Vue方便地实现HOC。
- 官方暂时不考虑将HOC加入vue core中,因为觉得相比于mixin的优势不够巨大。