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

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

可以看到,我們切換到菜單二時,進度條件會慢慢走,頁面沒有及時切換到菜單二的界面,如果頁面內容越多,效果越明顯。
網絡斷開
我們再來模擬一下網絡斷開的情況,切到 NetWork,網絡換成** Offline**,然后在切換路由,下面是我實操的效果:

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

可以看到第二種方案在弱方案的情況下,只要我們切換路由,頁面也會馬上進行切換,過渡方式也是采用我們指定的。不像第一種方案一樣,頁面會停在點擊之前的頁面,然后在一下的刷過去。
當切換到菜單時,因為這里我指定的時間 timeout 為 3 秒,所以在3秒內如果沒有加載出來,就會顯示我們指定的 errorComponent。
現在,打開瀏覽器,切到 NetWork,網絡換成** Offline**,也就是斷網的情況,我們在來看下效果。
網絡斷開

可以看到,當我們網絡斷開的時候,在切換頁面時,會顯示我們指定 errorComponent,不像第一種方式一樣會一直卡在頁面上加載。
變換 Loading
下面來看看,我事例路由:
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: '商化前端后臺登錄',
- },
- },
- {
- path: '/',
- redirect: '/menu',
- component: AdminLayout,
- props: {
- title: '商化前端 后臺 模板',
- routes,
- },
- meta: {
- title: '首頁',
- },
- children: routes as RouteRecordRaw[],
- },
- ],
- })
- export default router
我們現在想用下面已經封裝好的冒泡加載方式來代替菊花的樣式:

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

花里胡哨
如果我們只要點擊菜單二才用 BubbleLoading ,點擊其它的就用菊花的加載,那又要怎么做呢?
這里,大家如果認真看上面二次封裝的 createRouter 方法,可能就知道怎么做了,其中里面有一個判斷就是
- typeof childrenRoute.component === 'function'
其實我做的就是判斷如果外面?zhèn)鬟M來的路由采用的異步加載的方式,我才對用 defineAsyncComponent 重寫,其它的加載方式我是不管的,所以,我們想要自定義各自的加載方式,只要用 defineAsyncComponent 重寫即可。
回到我們的 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加載方式,運行效果如下:

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