Vue 進(jìn)階必學(xué)之高階組件 HOC(保姆式教學(xué),沖擊20k必備)

前言

高階組件這個概念在 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ū)寫出你的答案!


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內(nèi)容