前端微组件之Vue2动态加载异步组件
前言
去年工作中,有幸参与了公司业务拆分,公司业务繁杂,同个业务模块代码散落在多个业务系统中,并且随着公司发展各个业务系统又下放到不同的部门,有些部门的业务就需要跨部门协作;那需要跨部门的业务,一般是业务方提供组件库,协作方在他们系统完成对应业务的接入。一般来说这样的合作模式是可行的,但存在某些情况,这样的合作模式无法达到快速迭代的效果、加大沟通成本、项目上线周期长。例如:一个收集信息的业务(可能是一个弹窗组件),在多条业务线都涉及,每次收集信息的业务进行迭代,都需要通知业务线系统更新组件库版本,假如公司的上线模式在发布阶段有灰度时间计划,那这种收集信息的业务需要依赖宿主系统,采用npm依赖包的模式,就非常的CaoDan,你不得不去和对应业务线借系统制定灰度时间计划,碰巧人家系统在你的计划的灰度时间内正在使用,那你的项目大概率就得延期了。为了兼容这种业务场景和上线模式,组内搞出了一种基于异步组件的方案。
方案的落地
业界有很多微应用的案例,qiankun、single-spa、micro-app...等等都非常不错,但还是有一定的学习成本和改造成本,所以没使用,但借鉴了它们的思路,我们设计了一套基于Vue2异步组件的简易版的微组件加载器。
微组件加载器的作用是请求组件资源、加载组件资源;
Vue加载异步组件
vue提供了异步的方式来加载组件,异步组件
new Vue({
// ...
components: {
'my-component-a': () => import('./my-async-component-a')
'my-component-b': () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponentB.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
}
})
针对Vue的业务工程,我们可以尝试基于异步组件的方式,以最小的改动来实现公共业务组件代码抽离出各个业务系统。示例代码如下:
<template>
<div>
<ComponentA />
</div>
</template>
<script>
import MicroComponentLoader from "MicroComponentLoader";
import LoadingComponent from "./LoadingComponent";
import LoadingComponent from "./ErrorComponent";
export default {
components: {
ComponentA: MicroComponentLoader.get('ComponentA'),
ComponentB: () => ({
component: MicroComponentLoader('ComponentB'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 3000
})
}
};
</script>
<style>
</style>
MicroComponentLoader 就是上述提到的微组件加载器,其原理这里不赘述,之前给女朋友讲解异步组件后,她详细的讲了MicroComponentLoader的实现方式,有兴趣可以看看微组件加载器原理与代码实现(需要科学上网--)。其原理很简单,通过组件参数,获取对应的组件js/css资源,再动态创建script去挂载js/css,拿到组件对象,再通过script的load事件回调获取挂载在window上的组件对象,再将其返回给业务系统去注册组件。
Vue动态加载异步组件
上面大篇幅讲了微组件的方案,但实际落地,发现每个业务组件的注册都需要声明式的写错误兜底组件/加载效果组件/超时时间,当一个页面有多个异步组件,那写起来相当繁琐。
我们就在想,是否可以通过一个高阶组件来封装异步组件,实现动态组件加载。
Vue2的组件注册
组件注册,Vue官网提供了全局组件注册/局部组件注册/异步组件注册的文档,如果我们想动态注册异步组件: 按照官网的方式,用Vue.component(),进行全局注册
function registerComponent(name, Vue) {
Vue.component(name, () => ({
component: MicroComponentLoader(name),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 3000
}))
}
但我们在工程系统中异步注册组件,一般都是局部注册,当我们试图在options.components
获取当前组件实例:
<script>
export default {
components: {
componentOne: () => {
console.log(this) // 报错,this不存在
return {
component: componentOne
}
}
}
}
</script>
这是因为options.components
是保存在VueComponent.options.components
上的,组件实例化过程中,并不会把components
挂载在组件实例上。那是否有方式可以进行动态注册局部组件呢?
答案是有的, 接下来就从Vue注册异步组件的方式逐步寻找动态注册局部组件的方法。
Vue的异步组件注册原理
解析template
字符串,发现当前解析的标签是组件的话,会先获取组件的options.components[组件名]
的子组件信息,调用createComponent
方法,
createComponent
方法是在core/vdom/create-component.js
文件中定义的,异步组件会先调用resolveAsyncComponent
方法试图获取组件构造函数,获取不到,则会创建一个占位节点,等到组件数据返回之后,再将占位节点替换成组件节点。
我们看下resolveAsyncComponent
方法是怎么实现组件注册的,如下图,前面的各种判断我们本文就不展开讲了,只关注resolve
方法,里面调用了ensureCtor
,并把组件数据和baseCtor
当做参数传入,baseCtor
是构造函数Vue
。
ensureCtor
做了什么呢?
判断当前组件数据是否为对象,是对象,返回
base.extend(comp)
,extend
大家应该都不陌生,其作用是创建一个VueComponent
构造函数。
我们可以看到,
resolveAsyncComponent
方法最终是返回一个组件构造函数。
好了,现在我们知道options.components
中异步组件其实最开始也是一个组件构造函数,那我们思考一下,是否可以尝试在组件的options.components
动态塞入子组件数据?答案是:必须是可行的!!!
动态加载异步组件的实现
export default {
created() {
this.constructor.component('组件名', () => ({
component: MicroComponentLoader('ComponentB'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 3000
}))
}
}
如上述代码,我们在组件生命周期的created
内,调用组件构造函数的component
方法,来给组件的options.components
中注入一个异步组件。component
方法是怎么实现的呢?在extend
方法中,有这样一段代码:
const ASSET_TYPES = [
'component',
'directive',
'filter'
]
它将父组件构造函数的上述三个方法挂载到子组件构造函数。而最初的父组件构造函数就是Vue
,Vue.component
是Vue
执行之初,调用了initAssetRegisters(Vue)
方法实现的,在core/global-api/assets.js
文件中定义了initAssetRegisters
方法。
component
方法执行的时候,会在构造函数的options.components
对象上挂载子组件的构造函数。
至此,我们在组件生命周期的created
内,调用组件构造函数的component
方法,来给组件注册子组件的方式是可行的。
示例组件展示
<template>
<div>
<AsyncComp />
<component v-bind="$attrs" v-on="$listeners" :is="componentName"/>
</div>
</template>
<script>
import ComponentOne from '../component-one';
import LoadingComp from '../loading-comp';
export default {
name: 'MicroComp',
props: {
componentName: {
type: String,
required: true
}
},
components: {
// 对照组件(用这个和通过this.constructor.component的方式创建的组件有何异同)
AsyncComp: () => import('../async-comp/index.vue')
},
created() {
this.constructor.component(this.componentName, () => {
// 这里模拟异步组件,实际场景,是使用微组件加载器去获取组件配置对象
const resolve = new Promise((_resolve) => {
setTimeout(() => _resolve(ComponentOne), 3000)
})
return {
component: resolve,
loading: LoadingComp
}
})
}
}
</script>
<template>
<div id="app">
<MicroComponent componentName="ComponentOne"/>
</div>
</template>
<script>
import MicroComponent from './components/micro-comp/index';
export default {
name: 'App',
components: {
MicroComponent
}
}
</script>
<style>
</style>
我们通过组件实例对象,可以看到,this.constructor.component
注册的子组件已经挂载到组件构造函数的options.components
上了,并且正常渲染,数据也是响应式的。
后续就是围绕动态注册异步组件的兜底处理,例如异步组件资源加载失败/超时做日志上报、超时时间过后,加载兜底组件等等逻辑了。
小结
为了实现异步组件的统一兜底组件,不得不翻源码,找可实现方式。该看源码的时候,还真得看源码,有些使用方式使用场景,官方文档可能不会详细介绍,一切都需要自己探索。给自己打打气加加油。
文章若有错误,或者该方式有何不妥,请指正。😗
转载自:https://juejin.cn/post/7249299811497017399