1 背景
近期在做一个活动,涉及到的弹窗比较多,想着把这些弹窗都作为异步组件来加载,能尽可能地提高首屏加载速度。
可能提高不了多少,但是编码上的洁癖促使我尽可能不加载可能用不上的代码。
Vue本身支持异步组件,再加上webpack的构建,使用异步组件非常简单,没有用过异步组件的同学建议预习:

既然用了异步组件,就要考虑异常情况(网络的不确定性,不信你进个电梯试试。上图利用DevTools的请求拦截功能模拟网络异常的情况),而查阅Vue文档并没有发现异步组件重新加载的一些方法。Vue只对异步组件加载失败或超时进行了处理,并没有提供重试的方法。于是开始Google & 百度:
都没有找到答案,倒是找到一个相关的issue:页面中局部组件加载失败了怎么重新加载,Vue作者尤雨溪回复:“强制父组件重新渲染即可。”但是我测试了this.$forceUpdate()
没有成功(希望成功的同学留言指导一下),而且,如果只是子组件加载失败,就强制父组件重新渲染会不会有额外的性能损耗?(待探索)。
于是我想:难道就没有别的方法可以重新加载异步组件?
2 实现
在了解实现之前,我们先得了解异步组件是怎么渲染出来的。异步组件和一般的组件不同的地方在于,异步组件是异步加载回来,再实例化。所以异步组件渲染分为两步:
- 异步加载(通过ajax或script标签把组件内容请求回来)
- 实例化(和一般组件一样)
也就是我们要实现Vue异步组件进行加载失败重试,需要重新加载组件内容,然后完成组件实例化,这就是关键所在。
2.1 重新加载组件内容
在vue-vli生成的示例项目里,我们可以这样加载异步组件:
components: {
AsyncComponent: () => import('./components/AsyncComponent')
}
import
方法是webpack内部实现的用于加载异步模块的方法,(注意和es6本身的import
区分开来)。其本质是通过script标签来加载模块。异步组件就是一个模块,得益于webpack,我们直接使用import
即可加载特定组件。如果不想依赖webpack,可以自行加载,模块的加载方式取决于你使用的模块化解决方案:
- ajax:通过网络请求加载一份js代码,eval执行该代码获得模块内容;
- script标签:新增script标签,设置src为要加载的js路径,加载完成后自动执行js内容,返回模块内容;
webpack、sea.js、require.js模块加载方案都是用的第二种加载方案,感兴趣的同学可以深入学习一下。现在暂时知道
webpack + import
可以用来加载组件即可。
2.2 组件实例化
组件内容加载完后,就可以进行实例化了,具体怎么实例化呢?参考官网异步组件的文档,有两种实例化方式:
- 工厂函数
components: {
'async-example': function (resolve, reject) {
// 1. resolve就是Vue暴露给我们的实例化方法,在组件内容准备完成后,调用resolve方法即可完成组件实例化
// 2. 组件内容可以是一个对象,也可以是一个function(具体可以参考组件的定义方法)
resolve({
template: '<div>I am async!</div>'
})
})
- Promise函数
components: {
// import方法会返回一个promise,Vue内部在promise.then回调执行实例化方法
'my-component': () => import('./my-async-component')
}
综合这两种方法,我灵机一动,把Vue实例化组件的方法缓存下来,加载失败重试时,只需要去加载组件内容,然后利用缓存下来的方法实例化组件。
2.3 方案
<template>
<div id="app">
<div><button @click="loadAsyncComponent">加载异步组件【支持reload】</button><button @click="reload">reload</button></div>
<component :is="reloadAsyncComponentName"></component>
</div>
</template>
<script>
const resolveCache = {}
export default {
name: 'app',
components: {
AsyncReloadComponent: (resolve) => {
const res = import('./components/AsyncReloadComponent')
resolveCache.AsyncReloadComponent = resolve // 关键1:缓存resolve方法 - Vue创建组件的具体方法
res.then(resolve)
}
},
data: () => {
return {
reloadAsyncComponentName: ''
}
},
methods: {
loadAsyncComponent: function() {
this.reloadAsyncComponentName = 'AsyncReloadComponent'
},
reload: function() {
// 关键2:重新加载组件内容
const res = import('./components/AsyncReloadComponent')
const resolve = resolveCache.AsyncReloadComponent;
res.then((data) => {
delete resolveCache.AsyncReloadComponent // 成功执行后,释放cache内存
resolve(data)
});
}
}
}
</script>
<style>
</style>
至于为啥我知道组件的实例化提供了resolve方法,是因为我看了Vue的源码,异步组件创建会先调用业务定义的加载方法(例如我们这边的import),并将resolve和reject作为参数传给该方法实现回调,等待组件加载完成后,由业务调用resolve方法进行组件内部的实例化和渲染,相关的代码分析可以看下一节的链接。
这里再扩展一下,有时候我们需要的不只是异步加载,可能是动态加载。动态加载是指,只有在运行时,我才知道自己需要加载什么,例如ABTest实验中加载不同的页面模板。代码如下:
const res = import('./dynamic-components/' + key) // 动态import, webpack会把dynamicComponents目录下的文件都打包
动态加载的组件重试和上面方案一样,这里补全一下,作为参考:
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<div><button @click="load">加载异步组件【常规写法】</button></div>
<div><button @click="loadAsyncComponent">加载异步组件【支持reload】</button><button @click="reload">reload</button></div>
<div><button @click="loadDynamicComponent">加载动态组件【支持reload】</button><button @click="reloadDynamic">reload</button></div>
<component :is="generalAsyncComponentName"></component>
<component :is="reloadAsyncComponentName"></component>
<component :is="dynamicComponentName"></component>
</div>
</template>
<script>
const resolveCache = {};
const dynamicComponentKeys = ['DynamicComponent1', 'DynamicComponent2'];
const randomIndex = Math.floor(Math.random() * dynamicComponentKeys.length); // 模拟动态指定组件,随机使用组件1或组件2
const generateComponents = () => {
const components = {
// 常规写法
AsyncComponent: () => import('./components/AsyncComponent'),
// 支持reload的写法
AsyncReloadComponent: (resolve) => {
const res = import('./components/AsyncReloadComponent')
resolveCache.AsyncReloadComponent = resolve // 关键1:缓存resolve方法 - Vue创建组件的具体方法
res.then(resolve)
}
}
// 动态组件写法
const dynamicComponents = {};
dynamicComponentKeys.forEach((key) => {
dynamicComponents[key] = (resolve) => {
const res = import('./dynamic-components/' + key) // 动态import, webpack会把dynamicComponents目录下的文件都打包
resolveCache[key] = resolve;
res.then(resolve);
};
});
Object.assign(components, dynamicComponents);
return components;
}
export default {
name: 'app',
components: generateComponents(),
data: () => {
return {
generalAsyncComponentName: '',
reloadAsyncComponentName: '',
dynamicComponentName: ''
}
},
methods: {
load: function() {
this.generalAsyncComponentName = 'AsyncComponent'
},
loadAsyncComponent: function() {
this.reloadAsyncComponentName = 'AsyncReloadComponent'
},
reload: function() {
const res = import('./components/AsyncReloadComponent')
const resolve = resolveCache.AsyncReloadComponent;
res.then((data) => {
delete resolveCache.AsyncReloadComponent; // 成功执行后,释放cache内存
resolve(data);
});
},
loadDynamicComponent: function() {
this.dynamicComponentName = dynamicComponentKeys[randomIndex];
},
reloadDynamic: function() {
const key = dynamicComponentKeys[randomIndex]
const res = import('./dynamic-components/' + key)
const resolve = resolveCache[key]
res.then((data) => {
delete resolveCache.AsyncReloadComponent // 成功执行后,释放cache内存
resolve(data)
});
}
}
}
</script>
<style>
</style>
原理
Vue异步组件的原理下面这篇文章写得比较明白,感兴趣的同学推荐看看: