有梦想,有干货,微信搜索 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。
背景
在开始之前,先介绍一下我们目前新项目的采用的技术栈
- 前端公共库:
vue3 + typescript + jsx + antdVue
- 后台项目:
vue3 + typescript + jsx + antdVue
没错,我们现在都采用 ts + jsx
语法来开发新项目,这里可能会有小伙伴说了,不用 template
吗,装啥装。这里面要讨论内容很多,下次有机会在分享,今天不讨论这个问题。
回到正文~~
这个月老大在技术优化上(前端公共库)派了几个任务给我,其中的一个是"路由注册改造,采用组件内的异步加载",大家一看,肯定会想,就这?,这个不是配合 router.beforeEach
和 router.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 = {},
) {
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'
其实我做的就是判断如果外面传进来的路由采用的异步加载的方式,我才对用 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和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。