The modularization of webpack not only supports commonjs and ES modules, but also realizes the dynamic loading of modules through code splitting. according to Official documents of wepack , there are two ways to implement dynamic loading: import and require ensure.
Then, this document will analyze how webpack implements code splitting.
PS: if you are interested in how webpack implements commonjs and es module, you can check my first two articles: webpack modular principle commonjs and webpack modularization principle - ES module.
prepare
First, we still create a simple entry module index JS and two dependent modules foo JS and bar js:
// 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; }
The configuration of webpack is as follows:
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' }, };
This is the simplest configuration. It specifies the module entry and the output path of the packaged file. It is worth noting that this time, it also specifies the file name [name] of the separated module bundle. JS (no default file name will be specified).
Execute webpack in the root directory to get the following code packaged by webpack (remove unnecessary comments):
(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()); }) }) ]);
analysis
The compiled code, as a whole, has little difference from the code written using commonjs and es6 module in the previous two articles. They all start the code through IFFE, and then use the require ments and exports implemented by webpack to realize modularization.
For code splitting support, the difference is that it is used here__ webpack_require__.e realize the dynamic loading module and the module import based on promise.
So first analyze__ webpack_ require__. Definition of e function, which realizes dynamic loading:
__webpack_require__.e = function requireEnsure(chunkId) { // 1. Cache lookup var installedChunkData = installedChunks[chunkId]; if(installedChunkData === 0) { return new Promise(function(resolve) { resolve(); }); } if(installedChunkData) { return installedChunkData[2]; } // 2. Cache module var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; // 3. Load module 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"; // 4. Exception handling 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); // 5. Return promise return promise; };
The general logic of the code is as follows:
- Cache lookup: find whether there is a cache module from the cache installedChunks. If the cache ID is 0, it means that the module has been loaded, and return promise directly; If the cache is an array, indicating that the cache is loading, the cache promise object is returned
- If there is no cache, create a promise and cache the promise, resolve and reject in installedChunks
- Build a script tag, append it to the head tag, and src points to the loaded module script resources to dynamically load js scripts
- Add the script tags onload and onerror events. If the timeout or module loading fails, reject will be called to return the module loading failure exception
- If the module is loaded successfully, the current module promise is returned, corresponding to import()
The above is the module loading process. When the resource loading is completed and the module code starts to execute, let's take a look at the structure of the module code:
webpackJsonp([0],[ /* 0 */, /* 1 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; exports.foo = function () { return 2; } /***/ }) ]);
It can be seen that the module code is not only wrapped in a function (used to simulate the module scope), but also passed into webpackJsonp as a parameter. What is the function of this webpackJsonp function?
In fact, the webpackJsonp here is similar to the callback in jsonp. It is used as a callback for module loading and execution, so as to trigger the resolve of import.
Take a closer look at the webpackJsonp code to analyze:
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { var moduleId, chunkId, i = 0, resolves = [], result; // 1. Collection module resolve for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } // 2. copy module to modules for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); // 3,resolve import while(resolves.length) { resolves.shift()(); } };
The general logic of the code is as follows:
- Collect the resolve of the corresponding module according to chunkIds. The chunkIds here is an array because it is required Ensure can load multiple modules asynchronously, so it needs to be compatible
- Add the dynamic module to the modules of IFFE to provide modules for other CMD schemes
- Directly call resolve to complete the whole asynchronous loading
summary
Webpack pass__ webpack_require__.e function realizes dynamic loading, and then realizes asynchronous loading callback through webpackJsonp function to expose the module content to the caller in the way of promise, so as to realize the support for code splitting.