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的支持。