有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

背景

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

  • 前端公共库: vue3 + typescript + jsx + antdVue
  • 后盾我的项目:vue3 + typescript + jsx + antdVue

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

回到注释~~

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

传统形式

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

第一步,须要装置插件:

yarn add  nprogress

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

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

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

路由跳转中

router.beforeEach((to, from, next) => {  // 开启进度条  NProgress.start()  next()})

跳转完结

router.afterEach(() => {  // 敞开进度条  NProgress.done()})

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

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

  • 弱网络的状况,页面会卡那里,动的很慢
  • 当网络断开时,进度条件会始终处于加载的状态,并没有及时反馈加载失败
  • 当有比拟非凡需要,如,当加载菜单二时,我想用骨架屏的计划来加载,当加载菜单三,我想要用传统的菊花款式加载,这种状况,咱们当初的计划是很难做的。

弱网络

咱们模仿一下弱网络,关上浏览器控制台,切到 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 开发的,所以还得看下 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) {      // 申请产生谬误时重试,最多可尝试 3 次      retry()    } else {      // 留神,retry/fail 就像 promise 的 resolve/reject 一样:      // 必须调用其中一个能力持续错误处理。      fail()    }  }})

但在官网 V3 迁徙指南中 官网有指出上面这段话:

Vue Router 反对一个相似的机制来异步加载路由组件,也就是俗称的懒加载。只管相似,这个性能和 Vue 反对的异步组件是不同的。当用 Vue Router 配置路由组件时,你不应该应用 defineAsyncComponent。你能够在 Vue Router 文档的懒加载路由章节浏览更多相干内容。

官网说不应该应用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 办法,并提供了可选的配置参数 routerOptionsrouterOptions外面的字段其实就是defineAsyncComponent外面了的参数,除了 loder

有了当初的 createRouter,咱们来看雷同场景,不同成果。

弱网络

能够看到第二种计划在弱计划的状况下,只有咱们切换路由,页面也会马上进行切换,过渡形式也是采纳咱们指定的。不像第一种计划一样,页面会停在点击之前的页面,而后在一下的刷过来。

当切换到菜单时,因为这里我指定的工夫 timeout3 秒,所以在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'

其实我做的就是判断如果里面传进来的路由采纳的异步加载的形式,我才对用 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, }),

如果有小伙伴晓得的,能够私信通知我一下。

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

代码部署后可能存在的BUG没法实时晓得,预先为了解决这些BUG,花了大量的工夫进行log 调试,这边顺便给大家举荐一个好用的BUG监控工具 Fundebug。

交换

文章每周继续更新,能够微信搜寻「 大迁世界 」第一工夫浏览和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 曾经收录,整顿了很多我的文档,欢送Star和欠缺,大家面试能够参照考点温习,另外关注公众号,后盾回复福利,即可看到福利,你懂的。