前言
在做首屏性能优化的时候,我们针对一些首屏使用不到的资源,通常采用异步组件。只在需要的时候才从服务器加载一个模块。Vue 允许我们以一个工厂函数的方式定义我们的组件,这个工厂函数会异步解析我们的组件。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,并且会把结果缓存起来供重新渲染。通常我们将异步组件和 webpack 的 code-splitting 功能一起配合使用。今天我们就来看看,动态引入的方式和实现原理。
组件的动态导入方式
小小的另一篇文章 【vue系列】当 element-ui 按需引入遇到 vue-router 路由懒加载有讲过,我们再来重温一遍。看看是哪三种模式。
方式一:ES提案的动态import
目前比较推崇的方式,将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身),在 webpack 中,我们可以使用 动态 import 语法来定义代码分块。但是这种方式针对三方UI库,按需引入可能有问题。
// …
const App = () => import('./pages/app')
const Subtable = () => import('./pages/subtable')
const routes = [
{
path: '/',
name: 'app',
meta: {
title: '下属报表'
},
component: App
},
{
path: '/subtable',
name: 'app',
meta: {
title: '下属统计表'
},
component: Subtable
}
]
// …
方式二:Vue式异步加载组件 - AMD风格
通过 resolve => require([‘../path’], resolve) 的方式可以实现按需加载,并且一个组件会打包成一个js文件。
const App = resolve => require(['./pages/app'], resolve)
const Subtable = resolve => require(['./pages/subtable'], resolve)
方式三:webpack的require.ensuire() - CMD风格
通过 require.ensure() 实现按需加载,接收两个参数,第二个参数可以指定 chunkName。
const App = r => require.ensure([], () => r(require('./pages/app')), 'app')
const Subtable = r => require.ensure([], () => r(require('./pages/subtable')), 'subtable')
小小的疑问是 webpack是如何实现异步加载的?,为了解决这个问题,小小基于 vue-cli 快速创建一个新项目webpack-dynamic-import,我们一起来看看。
异步加载的实现原理探寻
初始化 vue-cli 项目,动态引入组件
在 webpack-dynamic-import 项目里,我们修改 src>App.vue文件,将 import HelloWorld from './components/HelloWorld.vue' 修改为 const HelloWorld = () => import('./components/HelloWorld.vue') 动态组件引入。然后构建打包。
分析打包结果
先从 index.html 打包结果查看,在head标签里有几个以 link 标签引入的 js 文件,对应的rel属性,各不相同有的是 prefetch ,有的是 preload,还有的带有as属性。看到这里小小产生了疑问。
- 为什么js文件要以link的形式引入?
- prefetch 与 preload 的区别?
- as=“script” 有什么作用?
想了解以上问题,先来看看MDN对 通过rel="preload"进行内容预加载 的介绍。
元素的rel 的属性值 preload 能够让我们在我们的 HTML 页面中 元素内部预加载一些资源。采用这一机制可以更早的得到加载并可用,减少对页面的初步渲染的阻塞。通过 href 和 as 属性指定需要被预加载资源的资源路径和类型,这里 as=“script” 指名了文件的类型是 JavaScript文件,as支持的属性类型很多,具体可以到MDN查看。
<link rel="prefetch"> 预获取一些资源,比 preload 的优先级低。
<link href="/js/HelloWorld.js" rel="prefetch">
<link href="/js/app.js" rel="preload" as="script">
<link href="/js/chunk-vendors.js" rel="preload" as="script">
再来看代码,就能晓得 app.js和chunk-vendors.js 的优先级较高,采用资源预加载机制。HelloWorld.js 的优先级较低,是在我们点击按钮才会展现的一个组件文件。
继续往下看,在body标签的尾部通过script标签引入了两个js文件。
<script type="text/javascript" src="/js/chunk-vendors.js"></script>
<script type="text/javascript" src="/js/app.js"></script>
这里每个js文件的内容是什么,决定了它们的引入顺序。打包出来的 dist 的 index.html 的link和script标签引入的文件为什么不同。link标签主要是用来做资源预加载的,而script是代码执行的。chunk-vendors.js 为提取的公共资源,需要优先加载和执行,app.js 为我们的主内容资源。
点击按钮,动态资源被加载
点击按钮,HelloWorld 动态组件资源被加载,执行,在app.js中有这样一段代码,将 HelloWorld 打包成一个js文件。
return __webpack_require__.e(/*! import() | HelloWorld */ \"HelloWorld\").then(__webpack_require__.bind(null, /*! ./components/HelloWorld.vue */ \"./src/components/HelloWorld.vue\"));
返回一个 __webpack_require__.e 的执行结果,那是一个Promise的工厂函数。__webpack_require__.e 函数的定义我们找到了
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ var promises = [];
/******/
/******/
/******/ // JSONP chunk loading for javascript
/******/
/******/ var installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
/******/ promises.push(installedChunkData[2] = promise);
/******/
/******/ // start chunk loading
/******/ var script = document.createElement('script');
/******/ var onScriptComplete;
/******/
/******/ script.charset = 'utf-8';
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = jsonpScriptSrc(chunkId);
/******/
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ onScriptComplete = function (event) {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var chunk = installedChunks[chunkId];
/******/ if(chunk !== 0) {
/******/ if(chunk) {
/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/ var realSrc = event && event.target && event.target.src;
/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/ error.name = 'ChunkLoadError';
/******/ error.type = errorType;
/******/ error.request = realSrc;
/******/ chunk[1](error);
/******/ }
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
/******/ var timeout = setTimeout(function(){
/******/ onScriptComplete({ type: 'timeout', target: script });
/******/ }, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ document.head.appendChild(script);
/******/ }
/******/ }
/******/ return Promise.all(promises);
/******/ };
这个函数先判断这个chunkId是否被加载过,没有话就去创建一个Promise,在Promise里面去创建一个script标签,然后设置src的路径,该路径就是被webpack代码拆分打包的组件代码路径。当代码被加载完成,通过webpackJsonpCallback执行。
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/ var executeModules = data[2];
/******/
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ // add entry modules from loaded chunk to deferred list
/******/ deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/ // run deferred modules when all chunks ready
/******/ return checkDeferredModules();
/******/ };
需要注意的是 resolves.push(installedChunks[chunkId][0]); 其中 installedChunks[chunkId][0] 就是Promise 的 resolve 函数。
在回到 HelloWorld.js 文件中:
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["HelloWorld"],{4805:function(e,t,r){.../},)
当该文件加载完成,自执行,往
webpackJsonp 数组里添加了 chunkId 为 4805 模块,4805就是这个异步组件的对应的map的key。
其基本原理,就是动态创建一个 script 引入动态资源文件,采用典型的 jsonp 形式执行callback。
__webpack_require__.e函数,异步 import 的调用webpackJsonpCallback函数,加载异步模块完成的回调。installedModules作用,缓存已经加载过的 module。installedChunks作用,缓存已经加载过的 chunk 的状态。jsonpScriptSrc作用,publicPath+chunkId的方式获取到异步加载模块的url地址。