提供可制定化的路由加載方式,Vue 如何做到?
背景
在開(kāi)始之前,先介紹一下我們目前新項(xiàng)目的采用的技術(shù)棧
- 前端公共庫(kù):vue3 + typescript + jsx + antdVue
- 后臺(tái)項(xiàng)目:vue3 + typescript + jsx + antdVue
沒(méi)錯(cuò),我們現(xiàn)在都采用 ts + jsx 語(yǔ)法來(lái)開(kāi)發(fā)新項(xiàng)目,這里可能會(huì)有小伙伴說(shuō)了,不用 template 嗎,裝啥裝。這里面要討論內(nèi)容很多,下次有機(jī)會(huì)在分享,今天不討論這個(gè)問(wèn)題。
回到正文~~
這個(gè)月老大在技術(shù)優(yōu)化上(前端公共庫(kù))派了幾個(gè)任務(wù)給我,其中的一個(gè)是"路由注冊(cè)改造,采用組件內(nèi)的異步加載",大家一看,肯定會(huì)想,就這?,這個(gè)不是配合 router.beforeEach 和 router.afterEach 在加個(gè)顯示進(jìn)度條的庫(kù) NProgress 不就完事了嘛。沒(méi)錯(cuò),就是按傳統(tǒng)的方式會(huì)有一些問(wèn)題,后面會(huì)講,這里我們先來(lái)看傳統(tǒng)方式是怎么做的。
傳統(tǒng)方式
這個(gè)方法大家應(yīng)該都用過(guò),就是在路由切換的時(shí)候,頂部顯示一個(gè)加載的進(jìn)度條,我們這里借助的庫(kù)是 NProgress。
第一步,需要安裝插件:
- yarn add nprogress
第二步,main.ts中引入插件。
- import NProgress from 'nprogress'
- import 'nprogress/nprogress.css'
第三步,監(jiān)聽(tīng)路由跳轉(zhuǎn),進(jìn)入頁(yè)面執(zhí)行插件動(dòng)畫(huà)。
路由跳轉(zhuǎn)中
- router.beforeEach((to, from, next) => {
- // 開(kāi)啟進(jìn)度條
- NProgress.start()
- next()
- })
跳轉(zhuǎn)結(jié)束
- router.afterEach(() => {
- // 關(guān)閉進(jìn)度條
- NProgress.done()
- })
很簡(jiǎn)單的一個(gè)配置,運(yùn)行后,當(dāng)我們切換路由時(shí)就會(huì)看到頂部有一個(gè)進(jìn)度條了:

這種模式存在兩個(gè)問(wèn)題(目前能想到的):
- 弱網(wǎng)絡(luò)的情況,頁(yè)面會(huì)卡那里,動(dòng)的很慢。
- 當(dāng)網(wǎng)絡(luò)斷開(kāi)時(shí),進(jìn)度條件會(huì)一直處于加載的狀態(tài),并沒(méi)有及時(shí)反饋加載失敗。
- 當(dāng)有比較特殊需求,如,當(dāng)加載菜單二時(shí),我想用骨架屏的方案來(lái)加載,當(dāng)加載菜單三,我想要用傳統(tǒng)的菊花樣式加載,這種情況,我們現(xiàn)在的方案是很難做的。
弱網(wǎng)絡(luò)
我們模擬一下弱網(wǎng)絡(luò),打開(kāi)瀏覽器控制臺(tái),切到 NetWork,網(wǎng)絡(luò)換成** Slow 3G**,然后在切換路由,下面是我實(shí)操的效果:

可以看到,我們切換到菜單二時(shí),進(jìn)度條件會(huì)慢慢走,頁(yè)面沒(méi)有及時(shí)切換到菜單二的界面,如果頁(yè)面內(nèi)容越多,效果越明顯。
網(wǎng)絡(luò)斷開(kāi)
我們?cè)賮?lái)模擬一下網(wǎng)絡(luò)斷開(kāi)的情況,切到 NetWork,網(wǎng)絡(luò)換成** Offline**,然后在切換路由,下面是我實(shí)操的效果:

會(huì)看到在沒(méi)有網(wǎng)絡(luò)的情況下,進(jìn)度條件還是在那一直轉(zhuǎn),一直加載,沒(méi)有及時(shí)的反饋,體驗(yàn)也是很差的。
我們想要啥效果
我們團(tuán)隊(duì)想要的效果是:
- 只要點(diǎn)擊菜單,頁(yè)面就要切換,即使在弱網(wǎng)的情況
- 在加載失敗時(shí)要給予一個(gè)失敗的反饋,而不是讓用戶傻傻的在那里等待
- 支持每個(gè)路由跳轉(zhuǎn)時(shí)特有的加載特效
尋找解決方案
為了解決上面的問(wèn)題,我們需要一種能異步加載并且能自定義 loading 的方法,查閱了官方文檔,Vue2.3 中新增了一個(gè)異步組件,允許我們自定義加載方式,用法如下:
- const AsyncComponent = () => ({
- // 需要加載的組件 (應(yīng)該是一個(gè) `Promise` 對(duì)象)
- component: import('./MyComponent.vue'),
- // 異步組件加載時(shí)使用的組件
- loading: LoadingComponent,
- // 加載失敗時(shí)使用的組件
- error: ErrorComponent,
- // 展示加載時(shí)組件的延時(shí)時(shí)間。默認(rèn)值是 200 (毫秒)
- delay: 200,
- // 如果提供了超時(shí)時(shí)間且組件加載也超時(shí)了,
- // 則使用加載失敗時(shí)使用的組件。默認(rèn)值是:`Infinity`
- timeout: 3000
- })
注意如果你希望在 Vue Router 的路由組件中使用上述語(yǔ)法的話,你必須使用 Vue Router 2.4.0+ 版本。
但我們現(xiàn)在是使用 Vue3 開(kāi)發(fā)的,所以還得看下 Vue3 有沒(méi)有類(lèi)似的方法。查閱了官方文檔,也找到了一個(gè)方法 defineAsyncComponent,用法大概如下:
- import { defineAsyncComponent } from 'vue'
- const AsyncComp = defineAsyncComponent({
- // 工廠函數(shù)
- loader: () => import('./Foo.vue'),
- // 加載異步組件時(shí)要使用的組件
- loadingComponent: LoadingComponent,
- // 加載失敗時(shí)要使用的組件
- errorComponent: ErrorComponent,
- // 在顯示 loadingComponent 之前的延遲 | 默認(rèn)值:200(單位 ms)
- delay: 200,
- // 如果提供了 timeout ,并且加載組件的時(shí)間超過(guò)了設(shè)定值,將顯示錯(cuò)誤組件
- // 默認(rèn)值:Infinity(即永不超時(shí),單位 ms)
- timeout: 3000,
- // 定義組件是否可掛起 | 默認(rèn)值:true
- suspensible: false,
- /**
- *
- * @param {*} error 錯(cuò)誤信息對(duì)象
- * @param {*} retry 一個(gè)函數(shù),用于指示當(dāng) promise 加載器 reject 時(shí),加載器是否應(yīng)該重試
- * @param {*} fail 一個(gè)函數(shù),指示加載程序結(jié)束退出
- * @param {*} attempts 允許的最大重試次數(shù)
- */
- onError(error, retry, fail, attempts) {
- if (error.message.match(/fetch/) && attempts <= 3) {
- // 請(qǐng)求發(fā)生錯(cuò)誤時(shí)重試,最多可嘗試 3 次
- retry()
- } else {
- // 注意,retry/fail 就像 promise 的 resolve/reject 一樣:
- // 必須調(diào)用其中一個(gè)才能繼續(xù)錯(cuò)誤處理。
- fail()
- }
- }
- })
但在官方 V3 遷移指南中 官方有指出下面這段話:
- Vue Router 支持一個(gè)類(lèi)似的機(jī)制來(lái)異步加載路由組件,也就是俗稱(chēng)的懶加載。盡管類(lèi)似,這個(gè)功能和 Vue 支持的異步組件是不同的。當(dāng)用 Vue Router 配置路由組件時(shí),你不應(yīng)該使用 defineAsyncComponent。你可以在 Vue Router 文檔的懶加載路由章節(jié)閱讀更多相關(guān)內(nèi)容。
官網(wǎng)說(shuō)不應(yīng)該使用defineAsyncComponent來(lái)做路由懶加載,但沒(méi)說(shuō)不能使用,而我們現(xiàn)在需要這個(gè)方法,所以還是選擇用了(后面遇到坑在分享出來(lái))。
思路
有了上面的方法,我們現(xiàn)在的思路就是重寫(xiě) Vue3 中的 createRouter方法,在createRouter 我們遞歸遍歷傳進(jìn)來(lái)的 routes, 判斷當(dāng)前的組件是否是異步加載組件,如果是我們用 defineAsyncComponent方法給它包裝起來(lái)。
下面是我現(xiàn)在封裝的代碼:
- import { RouteRecordMenu } from '@/components/AdminLayout';
- import PageLoading from '@/components/AdminLayout/components/PageLoading';
- import PageResult from '@/components/AdminLayout/components/PageResult';
- import {
- AsyncComponentLoader,
- AsyncComponentOptions,
- defineAsyncComponent,
- h,
- } from 'vue';
- import { createRouter as vueCreateRouter, RouterOptions } from 'vue-router';
- /**
- *
- * @param routerOptions vue createRouter 的參數(shù)
- * @param asyncComponentOptions 異步組件配置參數(shù)
- * @returns
- */
- export default function createRouter(
- routerOptions: RouterOptions,
- {
- loadingComponent = PageLoading,
- errorComponent = PageResult,
- delay = 200,
- timeout = 3000,
- suspensible = false,
- onError,
- }: Omit<AsyncComponentOptions, 'loader'> = {},
- ) {
- const treedRoutes = (childrenRoutes: RouteRecordMenu[]) => {
- return childrenRoutes.map((childrenRoute: RouteRecordMenu) => {
- if (childrenRoute.children) {
- childrenRoute.children = treedRoutes(childrenRoute.children);
- } else {
- if (typeof childrenRoute.component === 'function') {
- childrenRoute.component = defineAsyncComponent({
- loader: childrenRoute.component as AsyncComponentLoader,
- loadingComponent,
- errorComponent,
- delay,
- timeout,
- suspensible,
- onError,
- });
- }
- }
- return childrenRoute;
- });
- };
- treedRoutes(routerOptions.routes);
- return vueCreateRouter(routerOptions);
- }
上面重寫(xiě)了 createRouter 方法,并提供了可選的配置參數(shù) routerOptions,routerOptions里面的字段其實(shí)就是defineAsyncComponent里面了的參數(shù),除了 loder。
有了現(xiàn)在的 createRouter,我們來(lái)看相同場(chǎng)景,不同效果。
弱網(wǎng)絡(luò)

可以看到第二種方案在弱方案的情況下,只要我們切換路由,頁(yè)面也會(huì)馬上進(jìn)行切換,過(guò)渡方式也是采用我們指定的。不像第一種方案一樣,頁(yè)面會(huì)停在點(diǎn)擊之前的頁(yè)面,然后在一下的刷過(guò)去。
當(dāng)切換到菜單時(shí),因?yàn)檫@里我指定的時(shí)間 timeout 為 3 秒,所以在3秒內(nèi)如果沒(méi)有加載出來(lái),就會(huì)顯示我們指定的 errorComponent。
現(xiàn)在,打開(kāi)瀏覽器,切到 NetWork,網(wǎng)絡(luò)換成** Offline**,也就是斷網(wǎng)的情況,我們?cè)趤?lái)看下效果。
網(wǎng)絡(luò)斷開(kāi)

可以看到,當(dāng)我們網(wǎng)絡(luò)斷開(kāi)的時(shí)候,在切換頁(yè)面時(shí),會(huì)顯示我們指定 errorComponent,不像第一種方式一樣會(huì)一直卡在頁(yè)面上加載。
變換 Loading
下面來(lái)看看,我事例路由:
router.ts
- import { RouteRecordRaw, RouterView, createWebHistory } from 'vue-router'
- import { RouteRecordMenu } from '@ztjy/antd-vue/es/components/AdminLayout'
- import { AdminLayout, Login } from '@ztjy/antd-vue-admin'
- import createRouter from './createRoute'
- export const routes: RouteRecordMenu[] = [
- {
- path: '/menu',
- name: 'Menu',
- component: RouterView,
- redirect: '/menu/list',
- meta: {
- icon: 'fas fa-ad',
- title: '菜單一',
- },
- children: [
- {
- path: '/menu/list',
- component: () => import('@/pages/Menu1'),
- meta: {
- title: '列表',
- },
- },
- ],
- },
- {
- path: '/menu2',
- name: 'Menu2',
- component: RouterView,
- redirect: '/menu2/list',
- meta: {
- icon: 'fas fa-ad',
- title: '菜單二',
- },
- children: [
- {
- path: '/menu2/list',
- component: () => import('@/pages/Menu2'),
- meta: {
- title: '列表',
- },
- },
- ],
- },
- {
- path: '/menu3',
- name: 'Menu3',
- component: RouterView,
- redirect: '/menu3/list',
- meta: {
- icon: 'fas fa-ad',
- title: '菜單三',
- },
- children: [
- {
- path: '/menu3/list',
- component: () => import('@/pages/Menu3'),
- meta: {
- title: '列表',
- },
- },
- ],
- },
- ]
- const router = createRouter({
- history: createWebHistory('/'),
- routes: [
- {
- path: '/login',
- component: Login,
- props: {
- title: '商化前端后臺(tái)登錄',
- },
- },
- {
- path: '/',
- redirect: '/menu',
- component: AdminLayout,
- props: {
- title: '商化前端 后臺(tái) 模板',
- routes,
- },
- meta: {
- title: '首頁(yè)',
- },
- children: routes as RouteRecordRaw[],
- },
- ],
- })
- export default router
我們現(xiàn)在想用下面已經(jīng)封裝好的冒泡加載方式來(lái)代替菊花的樣式:

很簡(jiǎn)單,我們只需要把對(duì)應(yīng)加載組件(BubbleLoading)的名稱(chēng),傳給 createRouter 既可,為了演示效果,我們把網(wǎng)絡(luò)切花到 Slow 3G,代碼如下:
router.ts
- /***這里省略很多字**/
- const router = createRouter(
- {
- history: createWebHistory('/'),
- routes: [
- /***這里省略很多字**/
- ]
- },
- {
- loadingComponent: BubbleLoading, // 看這里看這里
- }
- )
- export default router

花里胡哨
如果我們只要點(diǎn)擊菜單二才用 BubbleLoading ,點(diǎn)擊其它的就用菊花的加載,那又要怎么做呢?
這里,大家如果認(rèn)真看上面二次封裝的 createRouter 方法,可能就知道怎么做了,其中里面有一個(gè)判斷就是
- typeof childrenRoute.component === 'function'
其實(shí)我做的就是判斷如果外面?zhèn)鬟M(jìn)來(lái)的路由采用的異步加載的方式,我才對(duì)用 defineAsyncComponent 重寫(xiě),其它的加載方式我是不管的,所以,我們想要自定義各自的加載方式,只要用 defineAsyncComponent 重寫(xiě)即可。
回到我們的 router.ts 代碼,
- // 這里省略一些代碼
- export const routes: RouteRecordMenu[] = [
- // 這里省略一些代碼
- {
- path: '/menu2',
- name: 'Menu2',
- component: RouterView,
- redirect: '/menu2/list',
- meta: {
- icon: 'fas fa-ad',
- title: '菜單二',
- },
- children: [
- {
- path: '/menu2/list',
- component: defineAsyncComponent({ // 看這里
- loader: () => import('@/pages/Menu2'),// 看這里
- loadingComponent: BubbleLoading,// 看這里
- }),
- meta: {
- title: '列表',
- },
- },
- ],
- },
- // 這里省略一些代碼
- ]
- // 這里省略一些代碼
在上面,我們用defineAsyncComponent定義菜單二的 component加載方式,運(yùn)行效果如下:

從圖片可以看出點(diǎn)擊菜單一和三時(shí),我們使用菊花的加載方式,點(diǎn)擊菜單二就會(huì)顯示我們自定義的加載方式。
注意
這里有一個(gè)顯性的 bug,就是下面代碼:
- component: defineAsyncComponent({
- loader: () => import('@/pages/Menu2'),
- loadingComponent: BubbleLoading,
- }),
不能用函數(shù)的方式來(lái)寫(xiě),如下所示:
- component: () => defineAsyncComponent({
- loader: () => import('@/pages/Menu2'),
- loadingComponent: BubbleLoading,
- }),
這里因?yàn)槲以?createRouter 方法中使用 typeof childrenRoute.component === 'function'來(lái)判斷,所以上面代碼又會(huì)被defineAsyncComponent包起來(lái),變成兩層的defineAsyncComponent,所以頁(yè)面加載會(huì)出錯(cuò)。
我也想解決這個(gè)問(wèn)題,但查了很多資料,沒(méi)有找到如何在方法中,判斷方法采用的是defineAsyncComponent 方式,即下面這種形式:
- component: () => defineAsyncComponent({
- loader: () => import('@/pages/Menu2'),
- loadingComponent: BubbleLoading,
- }),
本文到這里就分享完了,我是刷碗智,我們下期見(jiàn)~