webpack的模块化不只支持commonjs和es module,还能经过code splitting实现模块的动态加载。根据wepack官方文档,实现动态加载的方式有两种:import
和require.ensure
。javascript
那么,这篇文档就来分析一下,webpack是如何实现code splitting的。java
PS:若是你对webpack如何实现commonjs和es module感兴趣,能够查看个人前两篇文章:webpack模块化原理-commonjs和webpack模块化原理-ES module。webpack
首先咱们依然建立一个简单入口模块index.js
和两个依赖模块foo.js
和bar.js
:es6
// index.js 'use strict'; import(/* webpackChunkName: "foo" */ './foo').then(foo => { console.log(foo()); }) import(/* webpackChunkName: "bar" */ './bar').then(bar => { console.log(bar()); })
// foo.js 'use strict'; exports.foo = function () { return 2; }
// bar.js 'use strict'; exports.bar = function () { return 1; }
webpack配置以下:web
var path = require("path"); module.exports = { entry: path.join(__dirname, 'index.js'), output: { path: path.join(__dirname, 'outs'), filename: 'index.js', chunkFilename: '[name].bundle.js' }, };
这是一个最简单的配置,指定了模块入口和打包文件输出路径,值得注意的是,此次还指定了分离模块的文件名[name].bundle.js
(不指定会有默认文件名)。json
在根目录下执行webpack
,获得通过webpack打包的代码以下(去掉了没必要要的注释):segmentfault
(function(modules) { // webpackBootstrap // install a JSONP callback for chunk loading var parentJsonpFunction = window["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()(); } }; // The module cache var installedModules = {}; // objects to store loaded and loading chunks var installedChunks = { 2: 0 }; // The require function 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; } // This file contains only the entry chunk. // The chunk loading function for additional chunks __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":"foo","1":"bar"}[chunkId]||chunkId) + ".bundle.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; }; // expose the modules object (__webpack_modules__) __webpack_require__.m = modules; // expose the module cache __webpack_require__.c = installedModules; // define getter function for harmony exports __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); } }; // getDefaultExport function for compatibility with non-harmony modules __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; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ __webpack_require__.p = ""; // on error function for async loading __webpack_require__.oe = function(err) { console.error(err); throw err; }; // Load entry module and return exports return __webpack_require__(__webpack_require__.s = 0); }) ([ (function(module, exports, __webpack_require__) { "use strict"; __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1)).then(foo => { console.log(foo()); }) __webpack_require__.e/* import() */(1).then(__webpack_require__.bind(null, 2)).then(bar => { console.log(bar()); }) }) ]);
编译后的代码,总体跟前两篇文章中使用commonjs和es6 module编写的代码编译后的结构差异不大,都是经过IFFE的方式启动代码,而后使用webpack实现的require
和exports
实现的模块化。数组
而对于code splitting的支持,区别在于这里使用__webpack_require__.e
实现动态加载模块和实现基于promise的模块导入。promise
因此首先分析__webpack_require__.e
函数的定义,这个函数实现了动态加载:缓存
__webpack_require__.e = function requireEnsure(chunkId) { // 一、缓存查找 var installedChunkData = installedChunks[chunkId]; if(installedChunkData === 0) { return new Promise(function(resolve) { resolve(); }); } if(installedChunkData) { return installedChunkData[2]; } // 二、缓存模块 var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; // 三、加载模块 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":"foo"}[chunkId]||chunkId) + ".bundle.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); // 五、返回promise return promise; };
代码大体逻辑以下:
installedChunks
中查找是否有缓存模块,若是缓存标识为0,则表示模块已加载过,直接返回promise
;若是缓存为数组,表示缓存正在加载中,则返回缓存的promise
对象promise
,并将promise
和resolve
、reject
缓存在installedChunks
中promise
,对应于import()
以上即是模块加载的过程,当资源加载完成,模块代码开始执行,那么咱们来看一下模块代码的结构:
webpackJsonp([0],[ /* 0 */, /* 1 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.foo = function () { return 2; } /***/ }) ]);
能够看到,模块代码不只被包在一个函数中(用来模拟模块做用域),外层还被当作参数传入webpackJsonp
中。那么这个webpackJsonp
函数的做用是什么呢?
其实这里的webpackJsonp
相似于jsonp中的callback,做用是做为模块加载和执行完成的回调,从而触发import
的resolve
。
具体细看webpackJsonp
代码来分析:
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { var moduleId, chunkId, i = 0, resolves = [], result; // 一、收集模块resolve for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } // 二、copy模块到modules for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); // 三、resolve import while(resolves.length) { resolves.shift()(); } };
代码大体逻辑以下:
chunkIds
收集对应模块的resolve
,这里的chunkIds
为数组是由于require.ensure
是能够实现异步加载多个模块的,因此须要兼容modules
中,提供其余CMD方案使用模块resolve
,完成整个异步加载webpack经过__webpack_require__.e
函数实现了动态加载,再经过webpackJsonp
函数实现异步加载回调,把模块内容以promise的方式暴露给调用方,从而实现了对code splitting的支持。