webpack 俨然已经成为前端最主流的构建工具,其功能多种多样,咱们今天就来分析下关于代码分割这部分的一点事,并在最后讲述如何实如今webpack编译出的代码里手动添加一个异步chunk。javascript
每一个chunkId对应的是一个js文件,每一个moduleId对应的是一个个js文件的内容的模块(一个js文件里面能够require多个资源,每一个资源分配一个moduleId),因此它两的关系就是一个chunkId可能由不少个moduleId组成。前端
在webpack 编译出来的代码有定义了一个名称为__webpack_require__的函数,这个函数就是用来加载模块的,因此它的参数天然就是moduleId,以下:java
/******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ }
这里咱们要讲代码分割,那天然也要看看webpack编译出来的代码是经过什么方法进行异步加载js文件的,从代码中咱们能够找到一个名为requireEnsure的函数,这个函数即是来作这个事情的,那天然而然它的参数就是chunkId了,以下:react
/******/ __webpack_require__.e = function requireEnsure(chunkId) { /******/ var installedChunkData = installedChunks[chunkId]; /******/ if(installedChunkData === 0) { /******/ return new Promise(function(resolve) { resolve(); }); /******/ } /******/ /******/ // a Promise means "currently loading". /******/ if(installedChunkData) { /******/ return installedChunkData[2]; /******/ } /******/ /******/ // setup Promise in chunk cache /******/ var promise = new Promise(function(resolve, reject) { /******/ installedChunkData = installedChunks[chunkId] = [resolve, reject]; /******/ }); /******/ installedChunkData[2] = promise; /******/ /******/ // start chunk loading /******/ var head = document.getElementsByTagName('head')[0]; /******/ var script = document.createElement('script'); /******/ script.type = 'text/javascript'; /******/ script.charset = 'utf-8'; /******/ script.async = true; /******/ script.timeout = 120000; /******/ /******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } /******/ script.src = __webpack_require__.p + "" + ({"0":"home-chunk","1":"users-chunk","2":"about-chunk"}[chunkId]||chunkId) + ".js"; /******/ var timeout = setTimeout(onScriptComplete, 120000); /******/ script.onerror = script.onload = onScriptComplete; /******/ function onScriptComplete() { /******/ // avoid mem leaks in IE. /******/ script.onerror = script.onload = null; /******/ clearTimeout(timeout); /******/ var chunk = installedChunks[chunkId]; /******/ if(chunk !== 0) { /******/ if(chunk) { /******/ chunk[1](new Error('Loading chunk ' + chunkId + ' failed.')); /******/ } /******/ installedChunks[chunkId] = undefined; /******/ } /******/ }; /******/ head.appendChild(script); /******/ /******/ return promise; /******/ };
从上面代码咱们能够看出requireEnsure其实就是经过动态建立script标签来加载js文件的,可是这里不是每次访问这个js文件,都进行建立script请求的,在建立script前,requireEnsure会先经过installedChunks读取下是否已经有缓存了,若是有缓存直接使用即可。webpack
干说原理,都抽象啊,仍是从一个demo来分析,你轻松我也轻松😉,废话少说,先亮出demo地址:https://github.com/canfoo/webpack-code-split。git
这个demo是一个react项目,既然要讲代码分割,那么确定要实现动态路由加载了,以下:github
export default { path: '/', component: Core, indexRoute: { getComponent(location, cb) { System.import(/* webpackChunkName: "home-chunk" */ './components/Home') .then(loadRoute(cb)) .catch(errorLoading); }, }, childRoutes: [ { path: 'about', getComponent(location, cb) { System.import(/* webpackChunkName: "about-chunk" */ './components/About') .then(loadRoute(cb)) .catch(errorLoading); }, }, { path: 'users', getComponent(location, cb) { System.import(/* webpackChunkName: "users-chunk" */ './components/Users') .then(loadRoute(cb)) .catch(errorLoading); }, }, { path: '*', getComponent(location, cb) { System.import(/* webpackChunkName: "home-chunk" */ './components/Home') .then(loadRoute(cb)) .catch(errorLoading); }, }, ], };
如上,demo总共实现三个页面,三个页面都是经过分割的形式进行加载,名称分别为:home-chunk、about-chunk、users-chunk,build下,启动服务(anywhere),打开浏览器,从控制台咱们也看到咱们想要的结果:web
这时候咱们打开编辑器看看webpack编译的代码app.js,直接搜索“about-chunk”,咱们就定位到上文所说的requireEnsure的函数里有一段这样的代码:promise
/******/ script.src = __webpack_require__.p + "" + ({"0":"home-chunk","1":"users-chunk","2":"about-chunk"}[chunkId]||chunkId) + ".js";
从这段代码,能够看到每一个chunk对应key都是一个数字,这个数字正是chunkId,因此这也是为何上文说requireEnsure函数的参数是chunkId的缘由,由于这个函数正是经过chunkId来异步加载对应的js文件的。浏览器
接下来咱们来看看代码是在什么时候调用了这个方法的,全局搜索__webpack_require__.e,咱们便知道了:
exports.default = { path: '/', component: _Core2.default, indexRoute: { getComponent: function getComponent(location, cb) { __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 227)).then(loadRoute(cb)).catch(errorLoading); } }, childRoutes: [{ path: 'about', getComponent: function getComponent(location, cb) { __webpack_require__.e/* import() */(2).then(__webpack_require__.bind(null, 268)).then(loadRoute(cb)).catch(errorLoading); } }, { path: 'users', getComponent: function getComponent(location, cb) { __webpack_require__.e/* import() */(1).then(__webpack_require__.bind(null, 269)).then(loadRoute(cb)).catch(errorLoading); } }, { path: '*', getComponent: function getComponent(location, cb) { __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 227)).then(loadRoute(cb)).catch(errorLoading); } }] };
从上面代码就能够看到,正是路由访问适合,调取了这个函数的。咱们直接分析其中一个路由就能够了,好比about路由,由上面代码能够看到,当路径访问about时候,就调取对应的getComponent函数,这个函数里面首先执行__webpack_require__.e方法,成功再经过then执行__webpack_require__方法,即先去加载chunk文件,而后再去加载当前chunk文件里的模块,所以咱们能够从这里推断出,上面方法中由两个数字 1 和 268 ,这两个数字确定就是chunkId和modleId了,很显然,1 就是chunkId,而 268 就是moduleId。到了这里,咱们是否是也能够推论出 about-chunk.js 文件中是否是也会存在 1 和 268 这两个数字呢?答案是确定的,咱们打开about-chunk.js文件:
webpackJsonp([2],{ /***/ 268: /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _react = __webpack_require__(13); var _react2 = _interopRequireDefault(_react); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var About = function About() { return _react2.default.createElement( 'div', null, 'About' ); }; exports.default = About; /***/ }) });
如上所示,这个文件里直接调用了webpackJsonp方法,而这个方法第一个参数就是chunkIds 列表,而第二个参数就是一个moduleId与模块的对象,而这里正出现咱们上文出现两个数字 1 和 268,那为何须要这两个数字呢。这时候咱们还得继续看代码,看webpackJsonp是何方神圣,直接贴代码:
/******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { /******/ // add "moreModules" to the modules object, /******/ // then flag all "chunkIds" as loaded and fire callback /******/ var moduleId, chunkId, i = 0, resolves = [], result; /******/ for(;i < chunkIds.length; i++) { /******/ chunkId = chunkIds[i]; /******/ if(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(chunkIds, moreModules, executeModules); /******/ while(resolves.length) { /******/ resolves.shift()(); /******/ } /******/ /******/ };
从上面代码能够看出,webpackJsonpCallback主要作的是就是将每一个chunk存入到resolves中,并最后依次执行,另外还行chunk里模块缓存到modules变量。
至此,咱们就粗略分析完了代码分割事情,那么咱们是否是能够直接在编译出的代码里添加一些代码就能够生成一个新的异步chunk了?固然能够了!
假设咱们要添加一个新的chunk,名称为test-chunk
第一步,首先到页面添加个路由,这个比较简单,就不贴代码了。
第二步,就在路由代码中添加一个新的路径,这里添加路径就须要手动生成chunkId和moduleId,咱们就取以最大的chunkId和moduleId分别加1,即令chunkId=3,moduleId=270:
{ path: 'test', getComponent: function getComponent(location, cb) { // 3 为 chunkId ,270 为moduleId __webpack_require__.e/* import() */(3).then(__webpack_require__.bind(null, 270)).then(loadRoute(cb)).catch(errorLoading); } }
第三步,手动生成一个新的test-chunk.js(copy about-chunk.js文件内容改改部门内容便可),这里要注意的是须要将 chunkId=3,moduleId=270 正确的填入到参数里:
webpackJsonp([3],{ // chunkId /***/ 270: // moduleId /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _react = __webpack_require__(13); var _react2 = _interopRequireDefault(_react); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var Test = function Test() { return _react2.default.createElement( 'div', null, 'Test' ); }; exports.default = Test; /***/ }) });
到此就添加完毕了,这时候刷新页面,并点击"Test"连接,就能够看到浏览器正确加载出js文件了(这个效果其实在上图效果有体现了,这里就不贴出了)