https://github.com/coolriver/coolriver-site/blob/master/markdown/vue-mixin-hoc.md
組件復用場景
什么時候會需要組件復用呢?空談很虛(Talk is cheap, show me the code!),直接給出實際場景:
有一個使用了vue-router和vuex的單面應用制圈。在N個(下面以兩個為例子)獨立頁面功能完成后葱峡,需要增加權限控制的功能谆膳。有的頁面需要特定的用戶權限才能進入责静,否則如果強行輸入url進入的話,會提示“沒有權限訪問本頁面”厕九。
場景介紹
下面是沒有權限控制時挣饥,系統(tǒng)主要的幾個代碼文件:
頁面入口: 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');
}
}
}
以上的方式舵稠,在只有兩個頁面的時候,可能不覺得麻煩。如果頁面多了之后柱查,就十分難維護了,同時會有大量的重復代碼云石。魯迅說過: 不要重復你自己(Do not repeat yourself)
[圖片上傳失敗...(image-56436b-1566719930294)]
為了利用權限控制的公共邏輯唉工,接下來我們先嘗試使用官方推薦的mixin方式來進行優(yōu)化。
使用mixin實現復用
在使用mixin前汹忠,先把那個分散在各種頁面組件中的無權限提示淋硝,提取到單獨的組件中以便復用 提取出錯誤提示組件: no-right-tips.vue
export default {
template: `<div>不好意思,由于不夠帥宽菜,你沒有權限訪問本頁面</div>`,
name: 'no-right-tips'
}
接來下我們創(chuàng)建一個用于權限控制的mixin, 目標是使頁面組件(page1, page2)不用關心權限校驗是如何運行的谣膳。在這個例子中,只需要把hasRight這個計算屬性提取到mixin中铅乡。
right-mixin.js
export default {
computed: {
hasRight() { // 判斷用戶是否有權限進入本頁面的計算屬性
// 這里的user是之前在app中通過接口返回注入store的用戶信息
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() { // 判斷用戶是否有權限進入本頁面的計算屬性
// 這里的user是之前在app中通過接口返回注入store的用戶信息
const { rightList } = this.$store.state.user;
return rightList.indexOf(rightType); // 問題解決荠割,美滋滋
}
}
})
上面的所說的 用函數包一層
妹卿,聽起來好low是吧?我們來給這種方式起個高逼格一點的名字吧蔑鹦,我們稱上面的方式為 高階mixin
夺克。是不是瞬間聽起來不一樣了?
[圖片上傳失敗...(image-2da783-1566719930293)]
這個名字聽起來是不是和我們后面要講的 高階組件
如出一轍举反?
我們先不糾結名字了懊直,看看我們上面的方式如何在頁面組件中使用吧:
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的定義
根據上面的定義,我們可以引申為:通過函數向現有XXX添加功能耍群,就是高階XXX
义桂。在上面mixin的例子中,通用函數蹈垢,給普通mixin提供了可配置的權限檢測參數慷吊,所以可稱之為高階mixin。
到這里曹抬,其實高階函數的定義已經在上面帶出來了溉瓶。根據react官方文檔里最新的定義: 高階組件是一個方法,這個方法接收一個原始組件作為參數谤民,并返回新的組件
嚷闭。我想這個命名和定義應該也是參照高階函數的吧。
大家可能覺得奇怪赖临,為什么我要用react官方里的定義來說明高階組件呢胞锰?是因為高階組件最開始就是在react中提出來的。關于高階組件的歷史兢榨,我們可以后面再討論嗅榕。不如先看一下,如何使用高階組件來實現上面場景中的組件復用功能吵聪。
我們創(chuàng)建如下高階組件:
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>`,
}
發(fā)現沒有凌那?所有的權限相關代碼都抽出來了!組件回歸到的之前沒有權限功能時的樣子吟逝,是不是很清爽帽蝶。那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') },
]
}
使用高階組件同樣實現了組件復用励稳。而且看起來似乎更優(yōu)雅?我們來對比一下高階組件和mixin兩種方式囱井,在以上場景中的區(qū)別:
HOC
:
- 增加了一個hoc文件驹尼, hoc文件中引入no-right-tips
- 路由配置中,使用頁面組件的地方引入并使用了hoc
mixin
:
- 增加了一個mixin文件
- 每個組件代碼中庞呕,引入mixin新翎、no-right-tips, 并且增加相應的模板邏輯(v-if)
我認為程帕,在本文的場景中,使用HOC相比使用mixin有以下優(yōu)勢:
- 減少對原始組件的入侵地啰,降低耦合愁拭。HOC中,原始組件只用考慮自身邏輯亏吝,不用考慮敛苇,也感知不到HOC對它做了什么。而mixin顺呕,組件在內部需要使用mixin的計算屬性(更復雜的mixin還會用到生命周期和methods方法).
- 權限控制方便集中管理,直接在routes配置中管理各個頁面配置括饶,而不是分散在各個頁面組件內部株茶。
- 避免命名沖突。如果頁面自己有自己內部的權限控制图焰,剛好也有個computed屬性叫
hasRight
呢启盛?在HOC下沒問題,但mixin就不行了技羔。
React中的HOC現狀
其實最早在React中僵闯,也是使用mixin來實現組件功能復用的,但從v0.13.0開始藤滥,React的ES6 class組件寫法中就不支持mixin了鳖粟。這應該算是比較大的特性調整了。在此之后拙绊,已經使用了React的項目向图,可以繼續(xù)使用React.createClass
定義組件的方式來繼續(xù)使用mixin,如果要使用ES6 class并且實現同樣的組件復用标沪,就必須使用HOC了榄攀。
React為什么做了這個決定呢?人家不是沒事搞事情金句,而是有原因的檩赢。官方博客專門發(fā)文列舉mixin可能帶來的一些問題: mixin Considered Harmful。這篇文章里結合實際例子列舉了mixin在React中可能帶來的幾個問題违寞,并且給出了mixin遷移到HOC的一些指導贞瞒。原文是英文,并且篇幅較長趁曼,所以這里簡單地把文章里提到的mixin可能帶來的幾個問題列舉一下:
mixin會導致依賴不明確
mixin會調用組件內部方法/數據憔狞,組件會調用mixin方法/數據, 無法保證雙方方法穩(wěn)定存在.
多個mixin同時作用時,依賴關系對于被mixin的組件來說會更困惑mixin會導致命名沖突
多個mixin和組件本身彰阴,方法名稱會有命名沖突風險瘾敢,如果遇到了,不得不重命名某些方法mixin會帶來滾雪球般的復雜度
原文中列舉了一個復雜的mixin例子,我沒看懂簇抵。庆杜。。碟摆。
也就是說晃财,現在React體系中mixin已經不推薦使用,而推薦使用HOC典蜕。下圖是《深入React技術椂鲜ⅲ》一書中關于mixin和HOC的對比
Vue中的HOC現狀
相比于React,Vue目前還是使用mixin作為官方的組件復用方式愉舔。我在探索Vue中HOC的時候钢猛,發(fā)現很少有相關描述和實踐和文章。在百度里搜不出來轩缤,在google里也只能搜出寥寥幾個命迈。在我找到的資源中,有一個vuejs的github issue十分有代表價值: Discussion: Best way to create a HOC
在上面的issue討論中火的,我很高興有相同的志士也在想Vue中如何使用HOC壶愤。雖然我上面的例子簡單地實現了HOC,但是實際的場景可能更復雜馏鹤,涉及屬性傳遞征椒,slots等問題。而上面的issue就是在討論這個問題湃累。目前這個issue已經關閉陕靠,結論有兩個:
- 暫時由熱心人士產出了一個npm包: vue-hoc來幫助Vue方便地實現HOC.
- 官方暫時不考慮將HOC加入vue core中,因為覺得相比于mixin的優(yōu)勢不夠巨大脱茉。
后話
HOC在React被認為是更好的mixin替代方式剪芥。最初HOC也是在React社區(qū)中產生的,然后由官方進行采納和推廣琴许。在Vue中税肪,我不清楚是因為沒人想到這個問題還是什么,HOC很少有人關注榜田。所以我寫了這篇文章益兄,做了自己的HOC實踐,感覺效果不錯箭券。同時净捅,我也在知乎了提了相關的問題: 為何在React中推薦使用HOC,而不是mixins來實現組件復用辩块。但在Vue中蛔六,很少有HOC的嘗試荆永?,希望有大神能解答国章。
關于以上場景為什么不用router鉤子來做統(tǒng)一權限控制的補充說明
為什么不用
beforeEach
全局路由勾子來檢驗權限具钥? 因為,包含權限的用戶信息是在app.vue中異步加載液兽,并且存儲到vuex store中的骂删。在beforeEach函數中沒有找到可以訪問store中異步填入的數據的方法。要做的話四啰,只能在beforeEach函數里宁玫,將next方法放在獲取用戶信息的ajax回調里,以實現等待用戶信息加載完畢再判斷路由是否有權限進入的效果柑晒。如果真這樣做了欧瘪,在每次點擊鏈接導航至其它路由前,豈不是都要執(zhí)行ajax請求了敦迄?當然,可以做用戶信息數據緩存凭迹,但這樣就把事情變更復雜了不是嗎罚屋?而且,在ajax數據到達前嗅绸,路由下控制的頁面組件是完全阻塞住的脾猛,想展示Loading態(tài)都不行。為什么要把用戶信息放在vuex store中鱼鸠? 因為除了權限檢驗需要用到用戶信息之外猛拴,實際在其它模塊組件(header模塊,和側邊欄菜單模塊)中也需要用于用戶信息蚀狰。統(tǒng)一在app.vue的mounted中請求并保存在vuex store中愉昆,可以方便地提供給各組件進行用戶數據共享復用。