前言
高階組件這個概念在 React 中一度非常流行,但是在 Vue 的社區(qū)里討論的不多,本篇文章就真正的帶你來玩一個進(jìn)階的騷操作。
先和大家說好,本篇文章的核心是學(xué)會這樣的思想,也就是?智能組件?和?木偶組件?的解耦合,沒聽過這個概念沒關(guān)系,下面會詳細(xì)說明。
這可以有很多方式,比如?slot-scopes,比如未來的composition-api。本篇所寫的代碼也不推薦用到生產(chǎn)環(huán)境,生產(chǎn)環(huán)境有更成熟的庫去使用,這篇強調(diào)的是?思想,順便把 React 社區(qū)的玩法移植過來皮一下。
不要噴我,不要噴我,不要噴我!!?此篇只為演示高階組件的思路,如果實際業(yè)務(wù)中想要簡化文中所提到的異步狀態(tài)管理,請使用基于?slot-scopes?的開源庫?vue-promised
另外標(biāo)題中提到的?20k?其實有點標(biāo)題黨,我更多的想表達(dá)的是我們要有這樣的精神,只會這一個技巧肯定不能讓你達(dá)到?20k。但我相信只要大家有這樣鉆研高級用法,不斷優(yōu)化業(yè)務(wù)代碼,不斷提效的的精神,我們總會達(dá)到的,而且這一天不會很遠(yuǎn)。
例子
本文就以平常開發(fā)中最常見的需求,也就是異步數(shù)據(jù)的請求為例,先來個普通玩家的寫法:
<template>
? ? <div v-if="error">failed to load</div>
? ? <div v-else-if="loading">loading...</div>
? ? <div v-else>hello {{result.name}}!</div>
</template>
<script>
export default {
? data() {
? ? return {
? ? ? ? result: {
? ? ? ? ? name: '',
? ? ? ? },
? ? ? ? loading: false,
? ? ? ? error: false,
? ? },
? },
? async created() {
? ? ? try {
? ? ? ? // 管理loading
? ? ? ? this.loading = true
? ? ? ? // 取數(shù)據(jù)
? ? ? ? const data = await this.$axios('/api/user')?
? ? ? ? this.data = data
? ? ? } catch (e) {
? ? ? ? // 管理error
? ? ? ? this.error = true?
? ? ? } finally {
? ? ? ? // 管理loading
? ? ? ? this.loading = false
? ? ? }
? },
}
</script>
一般我們都這樣寫,平常也沒感覺有啥問題,但是其實我們每次在寫異步請求的時候都要有?loading、?error?狀態(tài),都需要有?取數(shù)據(jù)?的邏輯,并且要管理這些狀態(tài)。
那么想個辦法抽象它?好像特別好的辦法也不多,React 社區(qū)在 Hook 流行之前,經(jīng)常用?HOC(high order component) 也就是高階組件來處理這樣的抽象。
高階組件是什么?
說到這里,我們就要思考一下高階組件到底是什么概念,其實說到底,高階組件就是:
一個函數(shù)接受一個組件為參數(shù),返回一個包裝后的組件。
在 React 中
在 React 里,組件是?Class,所以高階組件有時候會用?裝飾器?語法來實現(xiàn),因為?裝飾器?的本質(zhì)也是接受一個?Class?返回一個新的?Class。
在 React 的世界里,高階組件就是?f(Class) -> 新的Class。
在 Vue 中
在 Vue 的世界里,組件是一個對象,所以高階組件就是一個函數(shù)接受一個對象,返回一個新的包裝好的對象。
類比到 Vue 的世界里,高階組件就是?f(object) -> 新的object。
智能組件和木偶組件
如果你還不知道?木偶?組件和?智能?組件的概念,我來給你簡單的講一下,這是 React 社區(qū)里一個很成熟的概念了。
木偶?組件: 就像一個牽線木偶一樣,只根據(jù)外部傳入的?props?去渲染相應(yīng)的視圖,而不管這個數(shù)據(jù)是從哪里來的。
智能?組件: 一般包在?木偶?組件的外部,通過請求等方式獲取到數(shù)據(jù),傳入給?木偶?組件,控制它的渲染。
一般來說,它們的結(jié)構(gòu)關(guān)系是這樣的:
<智能組件>
? <木偶組件 />
</智能組件>
它們還有另一個別名,就是?容器組件?和?ui組件,是不是很形象。
實現(xiàn)
具體到上面這個例子中(如果你忘了,趕緊回去看看,哈哈),我們的思路是這樣的,
高階組件接受?木偶組件?和?請求的方法?作為參數(shù)
在?mounted?生命周期中請求到數(shù)據(jù)
把請求的數(shù)據(jù)通過 props 傳遞給 木偶組件。
接下來就實現(xiàn)這個思路,首先上文提到了,HOC 是個函數(shù),本次我們的需求是實現(xiàn)請求管理的 HOC,那么先定義它接受兩個參數(shù),我們把這個 HOC 叫做 withPromise。
并且 loading、error 等狀態(tài),還有 加載中、加載錯誤 等對應(yīng)的視圖,我們都要在新返回的包裝組件 ,也就是下面的函數(shù)中 return 的那個新的對象 中定義好。
const withPromise = (wrapped, promiseFn) => {
? return {
? ? name: "with-promise",
? ? data() {
? ? ? return {
? ? ? ? loading: false,
? ? ? ? error: false,
? ? ? ? result: null,
? ? ? };
? ? },
? ? async mounted() {
? ? ? this.loading = true;
? ? ? const result = await promiseFn().finally(() => {
? ? ? ? this.loading = false;
? ? ? });
? ? ? this.result = result;
? ? },
? };
};
在參數(shù)中:
wrapped?也就是需要被包裹的組件對象。
promiseFunc?也就是請求對應(yīng)的函數(shù),需要返回一個 Promise
看起來不錯了,但是函數(shù)里我們好像不能像在?.vue?單文件里去書寫?template?那樣書寫模板了,
但是我們又知道模板最終還是被編譯成組件對象上的?render?函數(shù),那我們就直接寫這個?render?函數(shù)。(注意,本例子是因為便于演示才使用的原始語法,腳手架創(chuàng)建的項目可以直接用?jsx?語法。)
在這個?render?函數(shù)中,我們把傳入的?wrapped?也就是木偶組件給包裹起來。
這樣就形成了?智能組件獲取數(shù)據(jù)?->?木偶組件消費數(shù)據(jù),這樣的數(shù)據(jù)流動了。
const withPromise = (wrapped, promiseFn) => {
? return {
? ? data() { ... },
? ? async mounted() { ... },
? ? render(h) {
? ? ? return h(wrapped, {
? ? ? ? props: {
? ? ? ? ? result: this.result,
? ? ? ? ? loading: this.loading,
? ? ? ? },
? ? ? });
? ? },
? };
};
到了這一步,已經(jīng)是一個勉強可用的雛形了,我們來聲明一下?木偶?組件。
這其實是?邏輯和視圖分離?的一種思路。
const view = {
? template: `
? ? <span>
? ? ? <span>{{result?.name}}</span>
? ? </span>
? `,
? props: ["result", "loading"],
};
注意這里的組件就可以是任意?.vue?文件了,我這里只是為了簡化而采用這種寫法。
然后用神奇的事情發(fā)生了,別眨眼,我們用?withPromise?包裹這個?view?組件。
// 假裝這是一個 axios 請求函數(shù)
const request = () => {
? return new Promise((resolve) => {
? ? setTimeout(() => {
? ? ? resolve({ name: "ssh" });
? ? }, 1000);
? });
};
const hoc = withPromise(view, request)
然后在父組件中渲染它:
<div id="app">
? <hoc />
</div>
<script>
const hoc = withPromise(view, request)
new Vue({
? ? el: 'app',
? ? components: {
? ? ? hoc
? ? }
})
</script>
此時,組件在空白了一秒后,渲染出了我的大名?ssh,整個異步數(shù)據(jù)流就跑通了。
現(xiàn)在在加上?加載中?和?加載失敗?視圖,讓交互更友好點。
const withPromise = (wrapped, promiseFn) => {
? return {
? ? data() { ... },
? ? async mounted() { ... },
? ? render(h) {
? ? ? const args = {
? ? ? ? props: {
? ? ? ? ? result: this.result,
? ? ? ? ? loading: this.loading,
? ? ? ? },
? ? ? };
? ? ? const wrapper = h("div", [
? ? ? ? h(wrapped, args),
? ? ? ? this.loading ? h("span", ["加載中……"]) : null,
? ? ? ? this.error ? h("span", ["加載錯誤"]) : null,
? ? ? ]);
? ? ? return wrapper;
? ? },
? };
};
到此為止的代碼可以在效果預(yù)覽里查看,控制臺的 source 里也可以直接預(yù)覽源代碼。
完善
到此為止的高階組件雖然可以演示,但是并不是完整的,它還缺少一些功能,比如
要拿到子組件上定義的參數(shù),作為初始化發(fā)送請求的參數(shù)。
要監(jiān)聽子組件中請求參數(shù)的變化,并且重新發(fā)送請求。
外部組件傳遞給?hoc?組件的參數(shù)現(xiàn)在沒有透傳下去。
第一點很好理解,我們請求的場景的參數(shù)是很靈活的。
第二點也是實際場景中常見的一個需求。
第三點為了避免有的同學(xué)不理解,這里再啰嗦下,比如我們在最外層使用?hoc?組件的時候,可能希望傳遞一些 額外的props?或者?attrs?甚至是?插槽slot?給最內(nèi)層的?木偶?組件。那么?hoc?組件作為橋梁,就要承擔(dān)起將它透傳下去的責(zé)任。
為了實現(xiàn)第一點,我們約定好?view?組件上需要掛載某個特定?key?的字段作為請求參數(shù),比如這里我們約定它叫做?requestParams。
const view = {
? template: `
? ? <span>
? ? ? <span>{{result?.name}}</span>
? ? </span>
? `,
? data() {
? ? // 發(fā)送請求的時候要帶上它
? ? requestParams: {
? ? ? name: 'ssh'
? ? }?
? },
? props: ["result", "loading"],
};
改寫下我們的?request?函數(shù),讓它為接受參數(shù)做好準(zhǔn)備,
并且讓它的?響應(yīng)數(shù)據(jù)?原樣返回?請求參數(shù)。
// 假裝這是一個 axios 請求函數(shù)
const request = (params) => {
? return new Promise((resolve) => {
? ? setTimeout(() => {
? ? ? resolve(params);
? ? }, 1000);
? });
};
那么問題現(xiàn)在就在于我們?nèi)绾卧?hoc?組件中拿到?view?組件的值了,
平常我們怎么拿子組件實例的? 沒錯就是?ref,這里也用它:
const withPromise = (wrapped, promiseFn) => {
? return {
? ? data() { ... },
? ? async mounted() {
? ? ? this.loading = true;
? ? ? // 從子組件實例里拿到數(shù)據(jù)
? ? ? const { requestParams } = this.$refs.wrapped
? ? ? // 傳遞給請求函數(shù)
? ? ? const result = await promiseFn(requestParams).finally(() => {
? ? ? ? this.loading = false;
? ? ? });
? ? ? this.result = result;
? ? },
? ? render(h) {
? ? ? const args = {
? ? ? ? props: {
? ? ? ? ? result: this.result,
? ? ? ? ? loading: this.loading,
? ? ? ? },
? ? ? ? // 這里傳個 ref,就能拿到子組件實例了,和平常模板中的用法一樣。
? ? ? ? ref: 'wrapped'
? ? ? };
? ? ? const wrapper = h("div", [
? ? ? ? this.loading ? h("span", ["加載中……"]) : null,
? ? ? ? this.error ? h("span", ["加載錯誤"]) : null,
? ? ? ? h(wrapped, args),
? ? ? ]);
? ? ? return wrapper;
? ? },
? };
};
再來完成第二點,子組件的請求參數(shù)發(fā)生變化時,父組件也要響應(yīng)式的重新發(fā)送請求,并且把新數(shù)據(jù)帶給子組件。
const withPromise = (wrapped, promiseFn) => {
? return {
? ? data() { ... },
? ? methods: {
? ? ? // 請求抽象成方法
? ? ? async request() {
? ? ? ? this.loading = true;
? ? ? ? // 從子組件實例里拿到數(shù)據(jù)
? ? ? ? const { requestParams } = this.$refs.wrapped;
? ? ? ? // 傳遞給請求函數(shù)
? ? ? ? const result = await promiseFn(requestParams).finally(() => {
? ? ? ? ? this.loading = false;
? ? ? ? });
? ? ? ? this.result = result;
? ? ? },
? ? },
? ? async mounted() {
? ? ? // 立刻發(fā)送請求,并且監(jiān)聽參數(shù)變化重新請求
? ? ? this.$refs.wrapped.$watch("requestParams", this.request.bind(this), {
? ? ? ? immediate: true,
? ? ? });
? ? },
? ? render(h) { ... },
? };
};
第三點透傳屬性,我們只要在渲染子組件的時候把?$attrs、$listeners、$scopedSlots?傳遞下去即可,
此處的?$attrs?就是外部模板上聲明的屬性,$listeners?就是外部模板上聲明的監(jiān)聽函數(shù),
以這個例子來說:
<my-input value="ssh" @change="onChange" />
組件內(nèi)部就能拿到這樣的結(jié)構(gòu):
{
? $attrs: {
? ? value: 'ssh'
? },
? $listeners: {
? ? change: onChange
? }
}
注意,傳遞?$attrs、$listeners?的需求不僅發(fā)生在高階組件中,平常我們假如要對?el-input?這種組件封裝一層變成?my-input?的話,如果要一個個聲明?el-input?接受的?props,那得累死,直接透傳?$attrs?、$listeners?即可,這樣?el-input?內(nèi)部還是可以照樣處理傳進(jìn)去的所有參數(shù)。
// my-input 內(nèi)部
<template>
? <el-input v-bind="$attrs" v-on="$listeners" />
</template>
那么在render函數(shù)中,可以這樣透傳:
const withPromise = (wrapped, promiseFn) => {
? return {
? ? ...,
? ? render(h) {
? ? ? const args = {
? ? ? ? props: {
? ? ? ? ? // 混入 $attrs
? ? ? ? ? ...this.$attrs,
? ? ? ? ? result: this.result,
? ? ? ? ? loading: this.loading,
? ? ? ? },
? ? ? ? // 傳遞事件
? ? ? ? on: this.$listeners,
? ? ? ? // 傳遞 $scopedSlots
? ? ? ? scopedSlots: this.$scopedSlots,
? ? ? ? ref: "wrapped",
? ? ? };
? ? ? const wrapper = h("div", [
? ? ? ? this.loading ? h("span", ["加載中……"]) : null,
? ? ? ? this.error ? h("span", ["加載錯誤"]) : null,
? ? ? ? h(wrapped, args),
? ? ? ]);
? ? ? return wrapper;
? ? },
? };
};
至此為止,完整的代碼也就實現(xiàn)了:
<!DOCTYPE html>
<html lang="en">
? <head>
? ? <meta charset="UTF-8" />
? ? <meta name="viewport" content="width=device-width, initial-scale=1.0" />
? ? <title>hoc-promise</title>
? </head>
? <body>
? ? <div id="app">
? ? ? <hoc msg="msg" @change="onChange">
? ? ? ? <template>
? ? ? ? ? <div>I am slot</div>
? ? ? ? </template>
? ? ? ? <template v-slot:named>
? ? ? ? ? <div>I am named slot</div>
? ? ? ? </template>
? ? ? </hoc>
? ? </div>
? ? <script src="./vue.js"></script>
? ? <script>
? ? ? var view = {
? ? ? ? props: ["result"],
? ? ? ? data() {
? ? ? ? ? return {
? ? ? ? ? ? requestParams: {
? ? ? ? ? ? ? name: "ssh",
? ? ? ? ? ? },
? ? ? ? ? };
? ? ? ? },
? ? ? ? methods: {
? ? ? ? ? reload() {
? ? ? ? ? ? this.requestParams = {
? ? ? ? ? ? ? name: "changed!!",
? ? ? ? ? ? };
? ? ? ? ? },
? ? ? ? },
? ? ? ? template: `
? ? ? ? ? <span>
? ? ? ? ? ? <span>{{result?.name}}</span>
? ? ? ? ? ? <slot></slot>
? ? ? ? ? ? <slot name="named"></slot>
? ? ? ? ? ? <button @click="reload">重新加載數(shù)據(jù)</button>
? ? ? ? ? </span>
? ? ? ? `,
? ? ? };
? ? ? const withPromise = (wrapped, promiseFn) => {
? ? ? ? return {
? ? ? ? ? data() {
? ? ? ? ? ? return {
? ? ? ? ? ? ? loading: false,
? ? ? ? ? ? ? error: false,
? ? ? ? ? ? ? result: null,
? ? ? ? ? ? };
? ? ? ? ? },
? ? ? ? ? methods: {
? ? ? ? ? ? async request() {
? ? ? ? ? ? ? this.loading = true;
? ? ? ? ? ? ? // 從子組件實例里拿到數(shù)據(jù)
? ? ? ? ? ? ? const { requestParams } = this.$refs.wrapped;
? ? ? ? ? ? ? // 傳遞給請求函數(shù)
? ? ? ? ? ? ? const result = await promiseFn(requestParams).finally(() => {
? ? ? ? ? ? ? ? this.loading = false;
? ? ? ? ? ? ? });
? ? ? ? ? ? ? this.result = result;
? ? ? ? ? ? },
? ? ? ? ? },
? ? ? ? ? async mounted() {
? ? ? ? ? ? // 立刻發(fā)送請求,并且監(jiān)聽參數(shù)變化重新請求
? ? ? ? ? ? this.$refs.wrapped.$watch(
? ? ? ? ? ? ? "requestParams",
? ? ? ? ? ? ? this.request.bind(this),
? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? immediate: true,
? ? ? ? ? ? ? }
? ? ? ? ? ? );
? ? ? ? ? },
? ? ? ? ? render(h) {
? ? ? ? ? ? const args = {
? ? ? ? ? ? ? props: {
? ? ? ? ? ? ? ? // 混入 $attrs
? ? ? ? ? ? ? ? ...this.$attrs,
? ? ? ? ? ? ? ? result: this.result,
? ? ? ? ? ? ? ? loading: this.loading,
? ? ? ? ? ? ? },
? ? ? ? ? ? ? // 傳遞事件
? ? ? ? ? ? ? on: this.$listeners,
? ? ? ? ? ? ? // 傳遞 $scopedSlots
? ? ? ? ? ? ? scopedSlots: this.$scopedSlots,
? ? ? ? ? ? ? ref: "wrapped",
? ? ? ? ? ? };
? ? ? ? ? ? const wrapper = h("div", [
? ? ? ? ? ? ? this.loading ? h("span", ["加載中……"]) : null,
? ? ? ? ? ? ? this.error ? h("span", ["加載錯誤"]) : null,
? ? ? ? ? ? ? h(wrapped, args),
? ? ? ? ? ? ]);
? ? ? ? ? ? return wrapper;
? ? ? ? ? },
? ? ? ? };
? ? ? };
? ? ? const request = (data) => {
? ? ? ? return new Promise((r) => {
? ? ? ? ? setTimeout(() => {
? ? ? ? ? ? r(data);
? ? ? ? ? }, 1000);
? ? ? ? });
? ? ? };
? ? ? var hoc = withPromise(view, request);
? ? ? new Vue({
? ? ? ? el: "#app",
? ? ? ? components: {
? ? ? ? ? hoc,
? ? ? ? },
? ? ? ? methods: {
? ? ? ? ? onChange() {},
? ? ? ? },
? ? ? });
? ? </script>
? </body>
</html>
可以在?這里?預(yù)覽代碼效果。
我們開發(fā)新的組件,只要拿?hoc?過來復(fù)用即可,它的業(yè)務(wù)價值就體現(xiàn)出來了,代碼被精簡到不敢想象。
import { getListData } from 'api'
import { withPromise } from 'hoc'
const listView = {
? props: ["result"],
? template: `
? ? <ul v-if="result>
? ? ? <li v-for="item in result">
? ? ? ? {{ item }}
? ? ? </li>
? ? </ul>
? `,
};
export default withPromise(listView, getListData)
一切變得簡潔而又優(yōu)雅。
組合
注意,這一章節(jié)對于沒有接觸過 React 開發(fā)的同學(xué)可能很困難,可以先適當(dāng)看一下或者跳過。
有一天,我們突然又很開心,寫了個高階組件叫?withLog,它很簡單,就是在?mounted?聲明周期幫忙打印一下日志。
const withLog = (wrapped) => {
? return {
? ? mounted() {
? ? ? console.log("I am mounted!")
? ? },
? ? render(h) {
? ? ? return h(wrapped)
? ? },
? }
}
這里我們發(fā)現(xiàn),又要把on、scopedSlots等屬性提取并且透傳下去,其實挺麻煩的,我們封裝一個從this上整合需要透傳屬性的函數(shù):
function normalizeProps(vm) {
? return {
? ? on: vm.$listeners,
? ? attr: vm.$attrs,
? ? // 傳遞 $scopedSlots
? ? scopedSlots: vm.$scopedSlots,
? }
}
然后在h的第二個參數(shù)提取并傳遞即可。
const withLog = (wrapped) => {
? return {
? ? mounted() {
? ? ? console.log("I am mounted!")
? ? },
? ? render(h) {
? ? ? return h(wrapped, normalizeProps(this))
? ? },
? }
}
然后再包在剛剛的hoc之外:
var hoc = withLog(withPromise(view, request));
可以看出,這樣的嵌套是比較讓人頭疼的,我們把?redux?這個庫里的?compose?函數(shù)給搬過來,這個?compose?函數(shù),其實就是不斷的把函數(shù)給高階化,返回一個新的函數(shù)。
函數(shù)式 compose
function compose(...funcs) {
? return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
compose(a, b, c)?返回的是一個新的函數(shù),這個函數(shù)會把傳入的幾個函數(shù)?嵌套執(zhí)行
返回的函數(shù)簽名:(...args) => a(b(c(...args)))
這個函數(shù)對于第一次接觸的同學(xué)來說可能需要很長時間來理解,因為它確實非常復(fù)雜,但是一旦理解了,你的函數(shù)式思想又更上一層樓了。
我再?github?里對一個多參數(shù)的?compose?例子做了一個逐步拆解的分析,有興趣的話可以看看?compose拆解原理
循環(huán)式 compose
如果你不理解這種?函數(shù)式?的?compose?寫法,那我們用普通的循環(huán)來寫,就是返回一個函數(shù),把傳入的函數(shù)數(shù)組從右往左的執(zhí)行,并且上一個函數(shù)的返回值會作為下一個函數(shù)執(zhí)行的參數(shù)。
正常思路寫出來的?compose?函數(shù)是這樣的:
function compose(...args) {
? return function(arg) {
? ? let i = args.length - 1
? ? let res = arg
? ? while(i >= 0) {
? ? let func = args[i]
? ? res = func(res)
? ? i--
? ? }
? ? return res
? }
}
改造 withPromise
但是這也說明我們要改造?withPromise?高階函數(shù)了,因為仔細(xì)觀察這個?compose,它會包裝函數(shù),讓它接受一個參數(shù),并且把第一個函數(shù)的返回值?傳遞給下一個函數(shù)作為參數(shù)。
比如?compose(a, b)?來說,b(arg)?返回的值就會作為?a?的參數(shù),進(jìn)一步調(diào)用?a(b(args))
這需要保證 compose 里接受的函數(shù),每一項的參數(shù)都只有一個。
那么按照這個思路,我們改造?withPromise,其實就是要進(jìn)一步高階化它,讓它返回一個只接受一個參數(shù)的函數(shù):
const withPromise = (promiseFn) => {
? // 返回的這一層函數(shù) wrap,就符合我們的要求,只接受一個參數(shù)
? return function wrap(wrapped) {
? ? // 再往里一層 才返回組件
? ? return {
? ? ? mounted() {},
? ? ? render() {},
? ? }
? }
}
有了它以后,就可以更優(yōu)雅的組合高階組件了:
const compsosed = compose(
? ? withPromise(request),
? ? withLog,
)
const hoc = compsosed(view)
以上?compose?章節(jié)的完整代碼?在這。
注意,這一節(jié)如果第一次接觸這些概念看不懂很正常,這些在 React 社區(qū)里很流行,但是在 Vue 社區(qū)里很少有人討論!關(guān)于這個?compose?函數(shù),第一次在 React 社區(qū)接觸到它的時候我完全看不懂,先知道它的用法,慢慢理解也不遲。
真實業(yè)務(wù)場景
可能很多人覺得上面的代碼實用價值不大,但是?vue-router?的?高級用法文檔?里就真實的出現(xiàn)了一個用高階組件去解決問題的場景。
先簡單的描述下場景,我們知道?vue-router?可以配置異步路由,但是在網(wǎng)速很慢的情況下,這個異步路由對應(yīng)的?chunk?也就是組件代碼,要等到下載完成后才會進(jìn)行跳轉(zhuǎn)。
這段下載異步組件的時間我們想讓頁面展示一個?Loading?組件,讓交互更加友好。
在?Vue 文檔-異步組件?這一章節(jié),可以明確的看出 Vue 是支持異步組件聲明?loading?對應(yīng)的渲染組件的:
const AsyncComponent = () => ({
? // 需要加載的組件 (應(yīng)該是一個 `Promise` 對象)
? component: import('./MyComponent.vue'),
? // 異步組件加載時使用的組件
? loading: LoadingComponent,
? // 加載失敗時使用的組件
? error: ErrorComponent,
? // 展示加載時組件的延時時間。默認(rèn)值是 200 (毫秒)
? delay: 200,
? // 如果提供了超時時間且組件加載也超時了,
? // 則使用加載失敗時使用的組件。默認(rèn)值是:`Infinity`
? timeout: 3000
})
我們試著把這段代碼寫到vue-router里,改寫原先的異步路由:
new VueRouter({
? ? routes: [{
? ? ? ? path: '/',
-? ? ? ? component: () => import('./MyComponent.vue')
+? ? ? ? component: AsyncComponent
? ? }]
})
會發(fā)現(xiàn)根本不支持,深入調(diào)試了一下?vue-router?的源碼發(fā)現(xiàn),vue-router?內(nèi)部對于異步組件的解析和?vue?的處理完全是兩套不同的邏輯,在?vue-router?的實現(xiàn)中不會去幫你渲染?Loading?組件。
這個肯定難不倒機智的社區(qū)大佬們,我們轉(zhuǎn)變一個思路,讓?vue-router?先跳轉(zhuǎn)到一個?容器組件,這個?容器組件?幫我們利用 Vue 內(nèi)部的渲染機制去渲染?AsyncComponent?,不就可以渲染出?loading?狀態(tài)了?具體代碼如下:
由于 vue-router 的?component?字段接受一個?Promise,因此我們把組件用?Promise.resolve?包裹一層。
function lazyLoadView (AsyncView) {
? const AsyncHandler = () => ({
? ? component: AsyncView,
? ? loading: require('./Loading.vue').default,
? ? error: require('./Timeout.vue').default,
? ? delay: 400,
? ? timeout: 10000
? })
? return Promise.resolve({
? ? functional: true,
? ? render (h, { data, children }) {
? ? ? // 這里用 vue 內(nèi)部的渲染機制去渲染真正的異步組件
? ? ? return h(AsyncHandler, data, children)
? ? }
? })
}
const router = new VueRouter({
? routes: [
? ? {
? ? ? path: '/foo',
? ? ? component: () => lazyLoadView(import('./Foo.vue'))
? ? }
? ]
})
總結(jié)
本篇文章的所有代碼都保存在?Github倉庫?中,并且提供預(yù)覽。
謹(jǐn)以此文獻(xiàn)給在我源碼學(xué)習(xí)道路上給了我很大幫助的 《Vue技術(shù)內(nèi)幕》 作者?hcysun?大佬,雖然我還沒和他說過話,但是在我還是一個工作幾個月的小白的時候,一次業(yè)務(wù)需求的思考就讓我找到了這篇文章:探索Vue高階組件 | HcySunYang
當(dāng)時的我還不能看懂這篇文章中涉及到的源碼問題和修復(fù)方案,然后改用了另一種方式實現(xiàn)了業(yè)務(wù),但是這篇文章里提到的東西一直在我的心頭縈繞,我在忙碌的工作之余努力學(xué)習(xí)源碼,期望有朝一日能徹底看懂這篇文章。
時至今日我終于能理解文章中說到的?$vnode?和?context?代表什么含義,但是這個 bug 在 Vue 2.6 版本由于?slot?的實現(xiàn)方式被重寫,也順帶修復(fù)掉了,現(xiàn)在在 Vue 中使用最新的?slot?語法配合高階函數(shù),已經(jīng)不會遇到這篇文章中提到的 bug 了。
??感謝大家
1.如果本文對你有幫助,就點個贊支持下吧,你的「贊」是我創(chuàng)作的動力。
2.關(guān)注公眾號「半糖學(xué)前端」即可加我好友,大家一起進(jìn)步
益智問答:如果你知道請在評論區(qū)寫出你的答案!