前情提要 随着项目越来越大,性能问题已经成为了困扰业务发展的重要因素。 功能不停地累加后,核心页面已经不堪重负,访问速度愈来愈慢。 业务发展、用户体验都非常迫切的需要释放页面的负载,提高页面加载速度。
目前几种主流异步加载的方式
Webpack2:
require.ensure(dependencies, callback, chunName) 已废弃但保留 链接
ES提案:
() => import()
Vue:
component: resolve => require(‘xxx./vue’, resolve)
React:
react-loadable
希望通过这篇文章能带来以下的帮助?
webpack是如何将异步代码插入到页面上的
如何检测到异步代码已经查到页面上了?
有关webpack的code Spliting 本章主要是通过一个简单的异步加载的demo解读一下webpack打包后的代码是如何动态加载页面上的
webpack配置相关代码我就不贴了 main.js如下:
1 2 3 4 5 6 (async () => { const asyncAddWrapper = ( ) => import ('./add' ) const { default : add } = await asyncAddWrapper () console .log (add (1 , 0 )) })()
打包入口文件就是一个简单的通过es提案import动态引入add.js的内容并且打印计算结果
add.js
1 2 3 export default function add (a, b ) { return a + b }
add.js 默认暴露了一个两个数相加的函数
ok 我们运行打包命令 得到以下几个打包后的文件
0.js
index.html
main.js
用过异步组件的同学都知道 像这种0.js/1.js之类的文件都是我们的异步代码 只在需要的时候引入到页面上 从而达到减少首屏资源引入的效果 我们看看打包后的main.js是怎么样的 打包后的代码比较难看 我选了重要的片段
main.js
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 (function (modules ) { 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 ()(); } } var installedModules = {}; var installedChunks = { "index" : 0 }; function jsonpScriptSrc (chunkId ) { return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".js" } function __webpack_require__ (moduleId ) { if (installedModules[moduleId]) { return installedModules[moduleId].exports ; } var module = installedModules[moduleId] = { i : moduleId, l : false , exports : {} }; modules[moduleId].call (module .exports , module , module .exports , __webpack_require__); module .l = true ; return module .exports ; } __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); }; __webpack_require__.m = modules; __webpack_require__.c = installedModules; __webpack_require__.d = function (exports , name, getter ) { if (!__webpack_require__.o (exports , name)) { Object .defineProperty (exports , name, { enumerable : true , get : getter }); } }; __webpack_require__.r = function (exports ) { if (typeof Symbol !== 'undefined' && Symbol .toStringTag ) { Object .defineProperty (exports , Symbol .toStringTag , { value : 'Module' }); } Object .defineProperty (exports , '__esModule' , { value : true }); }; __webpack_require__.t = function (value, mode ) { if (mode & 1 ) value = __webpack_require__ (value); if (mode & 8 ) return value; if ((mode & 4 ) && typeof value === 'object' && value && value.__esModule ) return value; var ns = Object .create (null ); __webpack_require__.r (ns); Object .defineProperty (ns, 'default' , { enumerable : true , value : value }); if (mode & 2 && typeof value != 'string' ) for (var key in value) __webpack_require__.d (ns, key, function (key ) { return value[key]; }.bind (null , key)); return ns; }; __webpack_require__.n = function (module ) { var getter = module && module .__esModule ? function getDefault ( ) { return module ['default' ]; } : function getModuleExports ( ) { return module ; }; __webpack_require__.d (getter, 'a' , getter); return getter; }; __webpack_require__.o = function (object, property ) { return Object .prototype .hasOwnProperty .call (object, property); }; __webpack_require__.p = "./" ; __webpack_require__.oe = function (err ) { console .error (err); throw err; }; 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; return __webpack_require__ (__webpack_require__.s = "./src/main.js" ) })({ './src/main.js' : (function (module , exports , __webpack_require__ ) { (async () => { const asyncAddWrapper = ( ) => __webpack_require__.e ( 0 ).then (__webpack_require__.bind (null , "./src/add.js" )) const { default : add } = await asyncAddWrapper () console .log (add (1 , 0 )) })() })(), })
可以看到打包之后的main.js文件就是一个立即执行函数 入参modules是一个对象 key值是入口文件 value是打包之前main.js的内容 只不过import被webpack替换成了promise的形式 我们看一下立即执行函数的主函数做了什么事情吧 从上一路向下看 声明了一堆变量和方法 先不管 看到最下面
1 2 3 4 5 6 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;
这段是比较重要的一段代码 首先 声明了一个jsonpArray(临时值)把window.webpackJsonp或者空数组赋值给他 然后然后把jsonpArray的push方法赋值给oldJsonpFunction 这个oldJsonpFunction.push此时还是原型上的push 然后将webpackJsonpCallback重载成jsonpArray的push 到这一步window.webpackJsonp.push执行的就是webpackJsonpCallback这个方法 这个webpackJsonpCallback方法在加载异代码的时候会用到 然后执行了jsonpArray = jsonpArray.slice() 将jsonpArray的原型再次指向Array 最后将真正的window[“webpackJsonp”].push赋值给parentJsonpFunction
再看最后的执行代码
1 return __webpack_require__ (__webpack_require__.s = "./src/main.js" )
这个代码是真正执行main.js的代码 我们先看看__webpack_require__是何方神圣
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 var installedModules = {}; var installedChunks = { "index" : 0 }; function __webpack_require__ (moduleId ) { if (installedModules[moduleId]) { return installedModules[moduleId].exports ; } var module = installedModules[moduleId] = { i : moduleId, l : false , exports : {} }; modules[moduleId].call (module .exports , module , module .exports , __webpack_require__); module .l = true ; return module .exports ; }
上述代码生命了两个变量 installedModules和installedChunks installedModules用于存放已经加载的主入口的模块 installedChunks用于存放已加载异步的模块 key表示当前引入异步模块的编号 也就是文件名 value表示引入的状态 0表示已经加载 如果是个数组表示正在加载
我们看看__webpack_require__做了什么? 先判断了这个主模块是否已经加载过 如果加载过 就返回exports 如果是首次加载 将这个模块插入installedModules
1 modules[moduleId].call (module .exports , module , module .exports , __webpack_require__)
这一步调用了主模块的代码 并且传入了module/module.exports/__webpack_require__三个参数 ok 到这里估计还是一头雾水 我们再回到立即执行函数的参数modules里 看看执行了什么
1 2 3 4 5 6 7 8 (function (module , exports , __webpack_require__ ) { (async () => { const asyncAddWrapper = ( ) => __webpack_require__.e ( 0 ).then (__webpack_require__.bind (null , "./src/add.js" )) const { default : add } = await asyncAddWrapper () console .log (add (1 , 0 )) })() })()
可以看到 import关键字已经被编译成了__webpack_require__.e(0).then(webpack_require .bind(null, ‘./src/add.js’))
由此我们去找找__webpack_require__.e这个方法 如下:
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 __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); }
入参chunkId代表的是异步模块的编号 这个方法主要是将插入的过程封装成promise数组 利用promise.all将异步引入的过程变成同步的方法 先通过installedChunks判断该异步模块是否是第一次被引入
如果是第一次被引入
1.1 如果是正在引入的状态 直接将该模块插入时的promise放进promises数组
2.2 如果还没引入过 声明一个promise 并将该promise的fulfilled、reject回调和promise本身赋值给installedChunks[chunkId] 并加入promises数组 然后通过插入script标签的形式插入到页面上
再回到这步
1 const asyncAddWrapper = ( ) => __webpack_require__.e ( 0 ).then (__webpack_require__.bind (null , "./src/add.js" ))
webpack_require .e作用是将需要异步引入的模块通过promise封装成同步的形式插入到页面 我们看一下打包异步模块生成的代码 0.js
1 2 3 4 5 6 7 8 9 10 (window .webpackJsonp = window .webpackJsonp || []).push ([0 ], { './src/add.js' : (function (module , __webpack_exports__, __webpack_require__ ) { "use strict" ; __webpack_require__.r (__webpack_exports__); __webpack_require__.d (__webpack_exports__, "default" , function ( ) { return add; }); function add (a, b ) { return a + b } }), })
上述讲到window.webpackJsonp.push方法被重写为webpackJsonpCallback 我们看一下这个方法
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 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 ()(); } }
这个方法接收的参数是一个二维数组 数组第一项是异步模块的编号集合 第二项是异步模块内容 异步代码被插入页面后 会自动执行webpackJsonpCallback方法
遍历本次插入的异步模块列表 如果该chunkId在installedChunks中 并且installedChunks[chunkId]不为0 说明这个异步模块出于未加载状态 此时installedChunks[chunkId]的值为[resolve, reject, promise] 并将resolve插入resolves数组 最后通过while循环结束掉所有的promise 此时__webpack_require__.e执行完毕
遍历异步代码里的模块部分 放到modules做缓存 供热更新使用 parentJsonpFunction此时就是真正的window.webpackJsonp.push方法