装逼时刻
懒于思索,不愿意钻研和深入理解,自满或满足于微不足道的知识,都是智力贫乏的原因。这种贫乏用一个词来称呼,就是“愚蠢”。---高尔基
逼装完了,正式进入本文的主题!
核心问题
在做菜单管理业务中,本次业务中采用统一后台配置的方式控制菜单权限。此次开发中,存在几个疑惑点?
- 前端路由中并没有配置变化的路由,变化的路由的配置由后端提供,为什么通过 vue-router的addRoute将后台提供的配置添加后,页面就能正常的访问?
- 后端提供的配置中,component仅仅是个字符串组件路径,并没有 "组件对象实例",这到底整个过程是怎么用作的?
涉及知识点
- webpack、vite 这两个构建时的应用。
- vue-router addRoute。
- vue 中是如何处理异步路由。
过程解析
- 为什么 后端提供的配置中component是个路径字符串,通过addRoute之后,就能正常访问到对应的页面内容? 原因webpack、vite的构建机制。
以webpack为例,正常情况:
... ...
路由配置如下
const routes: RouteRecordRaw[] = [
//静态部分
{
path: "/",
redirect: "/system"
},
{
path: '/page1'
compoent: import("@/pages/page1.vue"),
},
{
path: '/page2'
compoent: import("@/pages/page2.vue"),
}
]
由于路径是写死在前端中,在webpack最终打包构建的时候,会将imoprt("@/pages/page1.vue")等,
打包编译成运行时的Promise。.then的参数就是已经编译好的运行时component实例对象。即运行时的所有
组件实例对象都在打包后的js文件中。所以是可以找到的。
那么为什么,在前台没有配置、后台仅仅传递对应的配置,就能访问到组件实例:
简单的后台配置如下:
{
path: '/page1',
component: 'page1',
...
}
原因依靠webpack的编译功能:在处理后台返回的路由配置中会用到 该写法 import(`@/pages/${config}.vue`)。介绍如下:
Dynamic expressions in import()
It is not possible to use a fully dynamic import statement, such as import(foo). Because foo could potentially be any path to any file in your system or project.
The import() must contain at least some information about where the module is located. Bundling can be limited to a specific directory or set of files so that when you are using a dynamic expression - every module that could potentially be requested on an import() call is included. For example, import(`./locale/${language}.json`) will cause every .json file in the ./locale directory to be bundled into the new chunk. At run time, when the variable language has been computed, any file like english.json or german.json will be available for consumption.
大体意思:' import() '必须至少包含一些关于模块所在位置的信息。 绑定可以被限制到一个特定的目录或一组文件,这样当您使用动态表达式时——每个可能在' import() '调用中被请求的模块都被包括在内。
import(@/pages/${config}.vue),因为webpack在语法分析依赖时,见到config变量,它没法确切的分析具体的文件,所以会将 @/pages/目录下的所有.vue 文件都会打包成一个新的bundle。
简单举例:
// 获取后台路由数据
const routsResource = fecth(...)
[
{
path: '/page1',
component: 'page1',
...
},
{
path: '/page1',
component: 'page1',
...
}
]
// 组装前台需要的路由配置(递归处理,这里简单认为只有一层)
const dynamicRouts = []
routsResource.forEach(item => {
routs.push({
path: item.path,
component: import(`@/pages/${item.component}.vue`) // pages下的组件会被打包,
//同时该语句会被webpack处理.__webpack_require__.e("xxx_chuck_name") 是一个promise。.then就会得到对应的组件实例对象。具体实现这里不做深究。
})
})
// 将变化的路由 和 静态路由(404,等)组合。
const baseRouts = this.$router.options.routes
this.$router.addRoute(...baseRouts, ...dynamicRouts)
总结:webpack打包过程中,对应的资源都打包进去了,同时将import()语法替换,在运行时在全局模块资源中找到对应的组件实例对象。
vite其实和webpack的原理是一样的, 各自的编译分析不同。vite官網
const modules = import.meta.glob('@/pages/**/**.tsx')
异步路由
上文使用了webpack的懒加载的方式。import() 返回一个Promise。那么Promise,vue是如何处理的。
借助vue3,defineAsyncComponent函数来理解
//简单的
function defineAsyncComponent(loader){
// 一个变量,用来存储异步加载的组件
let comp = null
//返回一个包装异步组件的高阶组件
return {
name: 'AsyncComponentWrapper', // 包装组件
setup(){
//判断异步是否加载成功
const loaded = ref(false)
// 打包后的加载器,在调用的时候才会去通过jsonp的方式加载,返回一个promise
loader().then(comp => {
comp = comp
loaded.value = true
})
return () => {
// 这里的return 相当于 template、render函数
return loaded.value ? { type: comp } : { type: Text, children: ''}
}
}
}
}
//变态 封装,传入options配置对象。功能更加强大。见vue官网。
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})