为什么需要动态加载
上一篇讲我写webpack针对markdown的loader的时候,提到了希望能动态加载以优化性能。篇幅原因没有展开说,打算单独写一篇文章讲一下。
当我们访问网站的时候,浏览器会向服务器请求页面资源。首先请求的当然是html文件。html文件里会带很多script标签,浏览器解析到这些标签的话,就会去下载这些js资源。当然有些script标签带了async或者defer属性,不会阻塞后续dom的解析。但是很多单页应用,本身渲染就是强依赖js的,js下载的慢,就会推迟页面开始渲染的时机。因此,我们肯定都是希望下载js越快越好的,因此我们就希望缩减js的体积。
缩减js体积的办法有很多,主要思路都是将非首屏必须的js资源拆出来,以减小首屏js的体积,首屏js加载完了就可以渲染出东西来了。所以所以我们可以对组件做动态加载。动态加载会让组件从项目js中拆出来,被打包成一个单独的js文件,被引入。webpack中我们可以通过import()语法,去动态加载组件
动态加载的实现
知其然,也想知其所以然。动态加载是怎么实现的呢?是webpack借助类似jsonp的方式来实现的。
说白了就是,被动态加载的组件会被单独打包,当你写的import()被执行的时候,webpack会去拼一个script标签出来,script标签的src属性就是被单独打包的组件的url。
window对象上的webpackJsonp数组
细致地说,在首屏的js中,webpack会在window上挂一个数组,用来记录已被动态加载完毕的组件。最初这个数组为空。然后被动态加载的组件,所在的js文件的开头,就是向window对象上的那个数组,push自己进去。因此当动态加载的js被下载完毕时,就会立刻执行push方法,后面有讲,这个push方法被动了手脚,在真的push之前,会执行一些必要的操作(详见webpackJsonpCallback)。
1 2 3 4 5 6 7
| ## 首屏js var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); jsonpArray.push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); var parentJsonpFunction = oldJsonpFunction;
|
1 2 3 4
| ## 被单独打包的js (window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{ }]);
|
动态加载的起点,requireEnsure方法
当执行到动态加载的时候,会调用webpack的requireEnsure方法。requireEnsure方法首先检查,这个模块是否被加载过,如果没加载过,就new一个Promise,然后去添加一个script标签,并为这个script标签设置好onerror和onload事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| ## 首屏js __webpack_require__.e = function requireEnsure(chunkId) { var promises = [];
var installedChunkData = installedChunks[chunkId]; if(installedChunkData !== 0) {
if(installedChunkData) { promises.push(installedChunkData[2]); } else { var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push(installedChunkData[2] = promise);
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);
var error = new Error(); onScriptComplete = function (event) { 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); };
|
加载完毕的回调,webpackJsonpCallback
然后还有一个很重要的方法,叫webpackJsonpCallback。window对象上的数组,push方法被劫持了,就是被改为webpackJsonpCallback方法。在webpackJsonpCallback里,会执行之前promise的resolve,然后把自己记录为已加载状态,最后再把自己push到数组里(劫持了原来的push方法,相当于在真的push之前作了上述操作)。这里也就能看出来,import()的本质是一个promise(严谨地说是promise.all)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| ## 首屏js function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1];
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()(); }
};
|
以上就是webpack动态加载的原理