提供可制定化的路由加载方式,Vue 如何做到?

时间:2022-09-21 12:50:27

提供可制定化的路由加载方式,Vue 如何做到?

背景

在开始之前,先介绍一下我们目前新项目的采用的技术栈

  • 前端公共库:vue3 + typescript + jsx + antdVue
  • 后台项目:vue3 + typescript + jsx + antdVue

没错,我们现在都采用 ts + jsx 语法来开发新项目,这里可能会有小伙伴说了,不用 template 吗,装啥装。这里面要讨论内容很多,下次有机会在分享,今天不讨论这个问题。

回到正文~~

这个月老大在技术优化上(前端公共库)派了几个任务给我,其中的一个是"路由注册改造,采用组件内的异步加载",大家一看,肯定会想,就这?,这个不是配合 router.beforeEach 和 router.afterEach 在加个显示进度条的库 NProgress 不就完事了嘛。没错,就是按传统的方式会有一些问题,后面会讲,这里我们先来看传统方式是怎么做的。

传统方式

这个方法大家应该都用过,就是在路由切换的时候,顶部显示一个加载的进度条,我们这里借助的库是 NProgress。

第一步,需要安装插件:

  1. yarn add  nprogress 

第二步,main.ts中引入插件。

  1. import NProgress from 'nprogress' 
  2. import 'nprogress/nprogress.css' 

第三步,监听路由跳转,进入页面执行插件动画。

路由跳转中

  1. router.beforeEach((tofromnext) => { 
  2.   // 开启进度条 
  3.   NProgress.start() 
  4.   next() 
  5. }) 

跳转结束

  1. router.afterEach(() => { 
  2.   // 关闭进度条 
  3.   NProgress.done() 
  4. }) 

很简单的一个配置,运行后,当我们切换路由时就会看到顶部有一个进度条了:

提供可制定化的路由加载方式,Vue 如何做到?

这种模式存在两个问题(目前能想到的):

  • 弱网络的情况,页面会卡那里,动的很慢。
  • 当网络断开时,进度条件会一直处于加载的状态,并没有及时反馈加载失败。
  • 当有比较特殊需求,如,当加载菜单二时,我想用骨架屏的方案来加载,当加载菜单三,我想要用传统的菊花样式加载,这种情况,我们现在的方案是很难做的。

弱网络

我们模拟一下弱网络,打开浏览器控制台,切到 NetWork,网络换成** Slow 3G**,然后在切换路由,下面是我实操的效果:

提供可制定化的路由加载方式,Vue 如何做到?

可以看到,我们切换到菜单二时,进度条件会慢慢走,页面没有及时切换到菜单二的界面,如果页面内容越多,效果越明显。

网络断开

我们再来模拟一下网络断开的情况,切到 NetWork,网络换成** Offline**,然后在切换路由,下面是我实操的效果:

提供可制定化的路由加载方式,Vue 如何做到?

会看到在没有网络的情况下,进度条件还是在那一直转,一直加载,没有及时的反馈,体验也是很差的。

我们想要啥效果

我们团队想要的效果是:

  • 只要点击菜单,页面就要切换,即使在弱网的情况
  • 在加载失败时要给予一个失败的反馈,而不是让用户傻傻的在那里等待
  • 支持每个路由跳转时特有的加载特效

寻找解决方案

为了解决上面的问题,我们需要一种能异步加载并且能自定义 loading 的方法,查阅了官方文档,Vue2.3 中新增了一个异步组件,允许我们自定义加载方式,用法如下:

  1. const AsyncComponent = () => ({ 
  2.   // 需要加载的组件 (应该是一个 `Promise` 对象) 
  3.   component: import('./MyComponent.vue'), 
  4.   // 异步组件加载时使用的组件 
  5.   loading: LoadingComponent, 
  6.   // 加载失败时使用的组件 
  7.   error: ErrorComponent, 
  8.   // 展示加载时组件的延时时间。默认值是 200 (毫秒) 
  9.   delay: 200, 
  10.   // 如果提供了超时时间且组件加载也超时了, 
  11.   // 则使用加载失败时使用的组件。默认值是:`Infinity` 
  12.   timeout: 3000 
  13. }) 

注意如果你希望在 Vue Router 的路由组件中使用上述语法的话,你必须使用 Vue Router 2.4.0+ 版本。

但我们现在是使用 Vue3 开发的,所以还得看下 Vue3 有没有类似的方法。查阅了官方文档,也找到了一个方法 defineAsyncComponent,用法大概如下:

  1. import { defineAsyncComponent } from 'vue' 
  2.  
  3. const AsyncComp = defineAsyncComponent({ 
  4.   // 工厂函数 
  5.   loader: () => import('./Foo.vue'), 
  6.   // 加载异步组件时要使用的组件 
  7.   loadingComponent: LoadingComponent, 
  8.   // 加载失败时要使用的组件 
  9.   errorComponent: ErrorComponent, 
  10.   // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms) 
  11.   delay: 200, 
  12.   // 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件 
  13.   // 默认值:Infinity(即永不超时,单位 ms) 
  14.   timeout: 3000, 
  15.   // 定义组件是否可挂起 | 默认值:true 
  16.   suspensible: false
  17.   /** 
  18.    * 
  19.    * @param {*} error 错误信息对象 
  20.    * @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试 
  21.    * @param {*} fail  一个函数,指示加载程序结束退出 
  22.    * @param {*} attempts 允许的最大重试次数 
  23.    */ 
  24.   onError(error, retry, fail, attempts) { 
  25.     if (error.message.match(/fetch/) && attempts <= 3) { 
  26.       // 请求发生错误时重试,最多可尝试 3 次 
  27.       retry() 
  28.     } else { 
  29.       // 注意,retry/fail 就像 promise 的 resolve/reject 一样: 
  30.       // 必须调用其中一个才能继续错误处理。 
  31.       fail() 
  32.     } 
  33.   } 
  34. }) 

但在官方 V3 迁移指南中 官方有指出下面这段话:

  • Vue Router 支持一个类似的机制来异步加载路由组件,也就是俗称的懒加载。尽管类似,这个功能和 Vue 支持的异步组件是不同的。当用 Vue Router 配置路由组件时,你不应该使用 defineAsyncComponent。你可以在 Vue Router 文档的懒加载路由章节阅读更多相关内容。

官网说不应该使用defineAsyncComponent来做路由懒加载,但没说不能使用,而我们现在需要这个方法,所以还是选择用了(后面遇到坑在分享出来)。

思路

有了上面的方法,我们现在的思路就是重写 Vue3 中的 createRouter方法,在createRouter 我们递归遍历传进来的 routes, 判断当前的组件是否是异步加载组件,如果是我们用 defineAsyncComponent方法给它包装起来。

下面是我现在封装的代码:

  1. import { RouteRecordMenu } from '@/components/AdminLayout'
  2. import PageLoading from '@/components/AdminLayout/components/PageLoading'
  3. import PageResult from '@/components/AdminLayout/components/PageResult'
  4. import { 
  5.   AsyncComponentLoader, 
  6.   AsyncComponentOptions, 
  7.   defineAsyncComponent, 
  8.   h, 
  9. from 'vue'
  10. import { createRouter as vueCreateRouter, RouterOptions } from 'vue-router'
  11.  
  12. /** 
  13.  * 
  14.  * @param routerOptions vue createRouter 的参数 
  15.  * @param asyncComponentOptions 异步组件配置参数 
  16.  * @returns 
  17.  */ 
  18.  
  19. export default function createRouter( 
  20.   routerOptions: RouterOptions, 
  21.   { 
  22.     loadingComponent = PageLoading, 
  23.     errorComponent = PageResult, 
  24.     delay = 200, 
  25.     timeout = 3000, 
  26.     suspensible = false
  27.     onError, 
  28.   }: Omit<AsyncComponentOptions, 'loader'> = {}, 
  29. ) { 
  30.   const treedRoutes = (childrenRoutes: RouteRecordMenu[]) => { 
  31.     return childrenRoutes.map((childrenRoute: RouteRecordMenu) => { 
  32.       if (childrenRoute.children) { 
  33.         childrenRoute.children = treedRoutes(childrenRoute.children); 
  34.       } else { 
  35.         if (typeof childrenRoute.component === 'function') { 
  36.           childrenRoute.component = defineAsyncComponent({ 
  37.             loader: childrenRoute.component as AsyncComponentLoader, 
  38.             loadingComponent, 
  39.             errorComponent, 
  40.             delay, 
  41.             timeout, 
  42.             suspensible, 
  43.             onError, 
  44.           }); 
  45.         } 
  46.       } 
  47.       return childrenRoute; 
  48.     }); 
  49.   }; 
  50.   treedRoutes(routerOptions.routes); 
  51.   return vueCreateRouter(routerOptions); 

上面重写了 createRouter 方法,并提供了可选的配置参数 routerOptions,routerOptions里面的字段其实就是defineAsyncComponent里面了的参数,除了 loder。

有了现在的 createRouter,我们来看相同场景,不同效果。

弱网络

提供可制定化的路由加载方式,Vue 如何做到?

可以看到第二种方案在弱方案的情况下,只要我们切换路由,页面也会马上进行切换,过渡方式也是采用我们指定的。不像第一种方案一样,页面会停在点击之前的页面,然后在一下的刷过去。

当切换到菜单时,因为这里我指定的时间 timeout 为 3 秒,所以在3秒内如果没有加载出来,就会显示我们指定的 errorComponent。

现在,打开浏览器,切到 NetWork,网络换成** Offline**,也就是断网的情况,我们在来看下效果。

网络断开

提供可制定化的路由加载方式,Vue 如何做到?

可以看到,当我们网络断开的时候,在切换页面时,会显示我们指定 errorComponent,不像第一种方式一样会一直卡在页面上加载。

变换 Loading

下面来看看,我事例路由:

router.ts

  1. import { RouteRecordRaw, RouterView, createWebHistory } from 'vue-router' 
  2. import { RouteRecordMenu } from '@ztjy/antd-vue/es/components/AdminLayout' 
  3. import { AdminLayout, Login } from '@ztjy/antd-vue-admin' 
  4. import createRouter from './createRoute' 
  5.  
  6. export const routes: RouteRecordMenu[] = [ 
  7.   { 
  8.     path: '/menu'
  9.     name'Menu'
  10.     component: RouterView, 
  11.     redirect: '/menu/list'
  12.     meta: { 
  13.       icon: 'fas fa-ad'
  14.       title: '菜单一'
  15.     }, 
  16.     children: [ 
  17.       { 
  18.         path: '/menu/list'
  19.         component: () => import('@/pages/Menu1'), 
  20.         meta: { 
  21.           title: '列表'
  22.         }, 
  23.       }, 
  24.     ], 
  25.   }, 
  26.   { 
  27.     path: '/menu2'
  28.     name'Menu2'
  29.     component: RouterView, 
  30.     redirect: '/menu2/list'
  31.     meta: { 
  32.       icon: 'fas fa-ad'
  33.       title: '菜单二'
  34.     }, 
  35.     children: [ 
  36.       { 
  37.         path: '/menu2/list'
  38.         component: () => import('@/pages/Menu2'), 
  39.         meta: { 
  40.           title: '列表'
  41.         }, 
  42.       }, 
  43.     ], 
  44.   }, 
  45.   { 
  46.     path: '/menu3'
  47.     name'Menu3'
  48.     component: RouterView, 
  49.     redirect: '/menu3/list'
  50.     meta: { 
  51.       icon: 'fas fa-ad'
  52.       title: '菜单三'
  53.     }, 
  54.     children: [ 
  55.       { 
  56.         path: '/menu3/list'
  57.         component: () => import('@/pages/Menu3'), 
  58.         meta: { 
  59.           title: '列表'
  60.         }, 
  61.       }, 
  62.     ], 
  63.   }, 
  64.  
  65. const router = createRouter({ 
  66.   history: createWebHistory('/'), 
  67.   routes: [ 
  68.     { 
  69.       path: '/login'
  70.       component: Login, 
  71.       props: { 
  72.         title: '商化前端后台登录'
  73.       }, 
  74.     }, 
  75.     { 
  76.       path: '/'
  77.       redirect: '/menu'
  78.       component: AdminLayout, 
  79.       props: { 
  80.         title: '商化前端 后台 模板'
  81.         routes, 
  82.       }, 
  83.       meta: { 
  84.         title: '首页'
  85.       }, 
  86.       children: routes as RouteRecordRaw[], 
  87.     }, 
  88.   ], 
  89. }) 
  90.  
  91. export default router 

我们现在想用下面已经封装好的冒泡加载方式来代替菊花的样式:

提供可制定化的路由加载方式,Vue 如何做到?

很简单,我们只需要把对应加载组件(BubbleLoading)的名称,传给 createRouter 既可,为了演示效果,我们把网络切花到 Slow 3G,代码如下:

router.ts

  1. /***这里省略很多字**/ 
  2. const router = createRouter( 
  3.   { 
  4.     history: createWebHistory('/'), 
  5.     routes: [ 
  6.       /***这里省略很多字**/ 
  7.     ] 
  8.   }, 
  9.   { 
  10.     loadingComponent: BubbleLoading, // 看这里看这里 
  11.   } 
  12.  
  13. export default router 
提供可制定化的路由加载方式,Vue 如何做到?

花里胡哨

如果我们只要点击菜单二才用 BubbleLoading ,点击其它的就用菊花的加载,那又要怎么做呢?

这里,大家如果认真看上面二次封装的 createRouter 方法,可能就知道怎么做了,其中里面有一个判断就是

  1. typeof childrenRoute.component === 'function' 

其实我做的就是判断如果外面传进来的路由采用的异步加载的方式,我才对用 defineAsyncComponent 重写,其它的加载方式我是不管的,所以,我们想要自定义各自的加载方式,只要用 defineAsyncComponent 重写即可。

回到我们的 router.ts 代码,

  1. // 这里省略一些代码 
  2. export const routes: RouteRecordMenu[] = [ 
  3.    // 这里省略一些代码 
  4.   { 
  5.     path: '/menu2'
  6.     name'Menu2'
  7.     component: RouterView, 
  8.     redirect: '/menu2/list'
  9.     meta: { 
  10.       icon: 'fas fa-ad'
  11.       title: '菜单二'
  12.     }, 
  13.     children: [ 
  14.       { 
  15.         path: '/menu2/list'
  16.         component: defineAsyncComponent({  // 看这里 
  17.           loader: () => import('@/pages/Menu2'),// 看这里 
  18.           loadingComponent: BubbleLoading,// 看这里 
  19.         }), 
  20.         meta: { 
  21.           title: '列表'
  22.         }, 
  23.       }, 
  24.     ], 
  25.   }, 
  26.  // 这里省略一些代码 
  27.  // 这里省略一些代码 

在上面,我们用defineAsyncComponent定义菜单二的 component加载方式,运行效果如下:

提供可制定化的路由加载方式,Vue 如何做到?

从图片可以看出点击菜单一和三时,我们使用菊花的加载方式,点击菜单二就会显示我们自定义的加载方式。

注意

这里有一个显性的 bug,就是下面代码:

  1. component: defineAsyncComponent({  
  2.    loader: () => import('@/pages/Menu2'), 
  3.    loadingComponent: BubbleLoading, 
  4. }), 

不能用函数的方式来写,如下所示:

  1. component: () => defineAsyncComponent({  
  2.    loader: () => import('@/pages/Menu2'), 
  3.    loadingComponent: BubbleLoading, 
  4. }), 

这里因为我在 createRouter 方法中使用 typeof childrenRoute.component === 'function'来判断,所以上面代码又会被defineAsyncComponent包起来,变成两层的defineAsyncComponent,所以页面加载会出错。

我也想解决这个问题,但查了很多资料,没有找到如何在方法中,判断方法采用的是defineAsyncComponent 方式,即下面这种形式:

  1. component: () => defineAsyncComponent({  
  2.    loader: () => import('@/pages/Menu2'), 
  3.    loadingComponent: BubbleLoading, 
  4. }), 

本文到这里就分享完了,我是刷碗智,我们下期见~

提供可制定化的路由加载方式,Vue 如何做到?

原文链接:https://mp.weixin.qq.com/s/HuiGjesmY2p-r422CTuKxw