Say something

Recently, I am working on a project strongly related to engineering – micro front end, which involves the coexisting problem of base project and sub-project loading. Previously, Webpack had been stuck at the configuration level, often referred to as the entry level. This project promoted, I had to step over the threshold, to look inside a little more.

This article focuses on how Webpack files run in the browser after they are built, which will give us a better understanding of how Webpack is built.

The code in this article is basically the core part. If you want to see the entire code and Webpack configuration, you can focus on the project and copy it yourself and run it at the demo address

Before reading this article, you need to know the basic concepts of Webpack and know the difference between Chunk and Module

In this article, we will take a step-by-step look at how the Webpack file is packaged and how the code works, through the following three steps:

  • Single file packaging, starting from IIFE;
  • Between multiple files, how to determine the loading status of dependencies;
  • What is the black magic behind on-demand loading?

Start with the simplest: how does a single file work

The simplest packaging scenario is to package an HTML file that only references a JS file, and the project will run. For example, 🌰 :

// index.js import sayHello from './utils/hello'; import { util } from './utils/util'; console.log('hello word:', sayHello()); console.log('hello util:', util); Utils /util.js export const util = 'hello utils'; // Utills /hello.js import {util} from './util'; console.log('hello util:', util); const hello = 'Hello'; export default function sayHello() { console.log('the output is:'); return hello; };

Entry-level code is simply an entry file that relies on two modules: util and hello, and then the module hello, which relies on util, and finally runs the HTML file, and you can see the Console print on the console. What does the packaged code look like? Look at the following. Some of the intrusive code has been removed, and only the core has been commented, but it is still long and requires patience:

(function(modules) {// WebPackBootstrap // InstalledModules = {}; // function __webpack_require__(moduleId) {// Install the module. If (instAlledModules [moduleId]) {return instAlledModules [moduleId].exports; Var module = installedModules[moduleId] = {I: moduleId, l: false, exports: {}}; var module = installedModules[moduleId]; // mount exports to the exports object; // mount exports to the exports object; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Identifies that the module has loaded module.l = true; // Return the exports of the module return module.exports; } // expose entry input module; __webpack_require__.m = modules; // Expose modules that have already been loaded; __webpack_require__.c = installedModules; // eg: export const hello = 'hello world'; // Returns: exProts. Hello = 'Hello World '; __webpack_require__.d = function (exports, name, getter) { if (! __webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); }}; // __webpack_public_path__ __webpack_require__.p = ''; Return __webpack_require__(__webpack_require__. S = "./ SRC /index.js"); })({ "./webpack/src/index.js": /*! no exports provided */ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var _utils_hello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/hello */ "./webpack/src/utils/hello.js"); var _utils_util__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils/util */ "./webpack/src/utils/util.js"); console.log('hello word:', Object(_utils_hello__WEBPACK_IMPORTED_MODULE_0__["default"])()); console.log('hello util:', _utils_util__WEBPACK_IMPORTED_MODULE_1__["util"]); }), "./webpack/src/utils/hello.js": (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "default", function() { return sayHello; }); var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./util */ "./webpack/src/utils/util.js"); console.log('hello util:', _util__WEBPACK_IMPORTED_MODULE_0__["util"]); var hello = 'Hello'; function sayHello() { console.log('the output is:'); return hello; } }), "./webpack/src/utils/util.js": (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "util", function() { return util; }); var util = 'hello utils'; })});

At first glance, the packaging result is a IIFE (Instant Execute Function), which is Webpack’s startup code and contains variable method declarations. The input is an object that describes the file we wrote in our code. The path to the file is the opposite key. Value is the code defined in the file, but the code is wrapped in a function:

/** * exports module * __webpack_exports__; exports module * __webpack_require__; /** * exports module * __webpack_require__; **/ function(module, __webpack_exports__, __webpack_require__) {// function(module, __webpack_exports__, __webpack_require__) {// function(module, __webpack_exports__, __webpack_require__);

The principle of loading, in the above code has been made comments, patience, a minute to understand, or add a picture, in VScode with Drawio plug-in to draw, feel:

In addition to the above loading process, one more detail is how Webpack can tell whether the dependency package is ESM or CommonJS module, or look at the packaging code. __webpack_require__. R (__webpack_exports__); / / __webpack_exports__ (__webpack_exports__); / / __webpack_exports__

R = function (exports) {if (typeof Symbol!); // Exports a module whose type is __esModule, __webpack_require__. == 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } // Defines the __esModule attribute on the MOdule. The __webpack_require__. N method uses // For the ES6 MOdule, import a from 'a'; Get: a[default]; // For CMD, import a from 'a'; Returns the entire module object.defineProperty (exports, '__esModule', {value: true}); }; // esModule gets the default in the module, N = function (module) {var getter = module&&module. __esModule? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; // Why define a property on this method? // use import m from 'm', then call m.func(); // The final execution of the module m is: m.a.func(); __webpack_require__.d(getter, 'a', getter); return getter; };

The most common: how to implement the introduction of multiple files

After going through the easy ones, now let’s look at one of the most common ones, which is introducing splitChunks, multi-chunk builds, and what happens to the execution process. We often put some external dependencies into a JS package, the project’s own resources into a JS package;

Again, let’s look at the pre-package code:

// index.js + import moment from 'moment'; + import cookie from 'js-cookie'; import sayHello from './utils/hello'; import { util } from './utils/util'; console.log('hello word:', sayHello()); console.log('hello util:', util); + console.log('time', moment().format('YYYY-MM-DD')); + cookie.set('page', 'index'); Utils /util.js + import moment from 'moment'; export const util = 'hello utils'; export function format() { return moment().format('YYYY-MM-DD'); } // The associated module: utils/hello.js // is the same as above

As can be seen from the above code, we have introduced two external JS packages, Moment and JS-Cookie, and used a subcontracting mechanism to pack the packages that depend on node_modules into a single one. Here is a screenshot of the HTML file after multi-chunk packaging:

Let’s see what the async.js package looks like:

// Pseudocode Hides my moment and js - cookie code details (window [" webpackJsonp "] = window (" webpackJsonp ") | | []), push ([[" async "] and { "./node_modules/js-cookie/src/js.cookie.js": (function(module, exports, __webpack_require__) {}), "./node_modules/moment/moment.js": (function(module, exports, __webpack_require__) {}) })

[[“async”],{}]. In advance, the first element of the array is the chunk name contained in the file. The second element is the same as the input in the first section of the simple file packaging. Is the module name and the packaged module code;

Look at the change in index.js again:

(function(modules) {// new function webpackJsonPCallback (data) {return checkDeferredModules(); }; Function checkDeferredModules() {} var instAlledModules = {}; // undefined = chunk not loaded, null = chunk preloaded/ preloaded: // Promise = chunk loading 0 = chunk loaded var installedChunks = { "index": 0 }; var deferredModules = []; // on error function for async loading __webpack_require__.oe = function(err) { console.error(err); throw err; }; / / load the key 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; // start from the entry file - return __webpack_require__(__webpack_require__. S = "./ SRC /index.js"); // DeferredModules.push (["./webpack/ SRC /index.js","async"]); // Check the executable entry + return checkDeferredModules(); }) ({// ellipse; })

From the above code, we can see that we have done a lot of work to support the multi-chunk execution, Webpack’s bootstrap, I will list it here:

  • A newcheckDeferredModules, which is used to rely on Chunk to check if it is ready;
  • newwebpackJsonpGlobal array for communication between files and module storage; The communication is throughIntercept pushThe operation has been completed;
  • Add WebpackJsonpCallback as an interceptPush the agentOperation is also the core of the whole implementation;
  • To modify theEntrance to the fileExecution mode, depending on deferredModules implementation;

Here are a lot of articles, let’s crack one by one:

### WebpackJSONP push intercept

// Check if the window[" webpackJSONP "] array is declared. If not, declare one; var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; Var oldjsonPFunction = jsonParray.push. Bind (jsonParray); Indow ["webpackJsonp"]. Push = webpackJsonpCallback; JsonParray = jsonParray.slice (); jsonParray = jsonParray.slice (); jsonParray.slice (); // This is the first step in the process of iterating through the array. // This is the case where the chunk dependency runs out first, but the intercepting agent has not yet taken effect; So manually traverse once, let the loaded module go through the agent operation again; for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); // This operation is an assignment statement and is meaningless; var parentJsonpFunction = oldJsonpFunction;

WebpackJSONPCallback will decrypt it later.

What does the proxy WebpackJsonpCallback do

function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var executeModules = data[2]; // add "moreModules" to the modules object, var moduleId, chunkId, i = 0, resolves = []; for(; i < chunkIds.length; i++) { chunkId = chunkIds[i]; // InstAlledchunks [chunKid] = 0; } for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, ModuleId)) {// Add modules from other chunks to the main chunk; modules[moduleId] = moreModules[moduleId]; If (parentJsonPfunction) parentJsonPfunction (data); While (resolves. Length) {/ / in the next section to tell} / / it is of little use here deferredModules. Push. Apply (deferredModules, executeModules | | []); // run deferred modules when all chunks ready return checkDeferredModules(); };

Remember the format of the data for push:

window["webpackJsonp"].push([["async"], moreModules])

When you intercept push, you actually do three things:

  • Add the second variable in the array moreModules to the modules input of the index.js function that executes immediately.
  • Make the load state of the chunk complete;
  • Then checkDeferredModules to see if there are any modules waiting for the dependency to execute after it has been loaded.

What does CheckDeferredModules do

function checkDeferredModules() { var result; for(var i = 0; i < deferredModules.length; i++) { var deferredModule = deferredModules[i]; var fulfilled = true; for(var j = 1; j < deferredModule.length; J++) {// DepId is the Dependent chunk ID, and for the entry './webpack/ SRC /index.js' deferredModule, depId is' async '. Var depId = DeferredModule [j]; if(installedChunks[depId] ! == 0) fulfilled = false; } which is fulfilled {// Fulfill the fulfilled item; deferredModules.splice(i--, 1); // webpack/ SRC /index.js result = __webpack_require__(__webpack_require__. S = deferredModule[0]); } } return result; }

Remember that the implementation of the entry file is replaced with: deferredModules. Push ([“./webpack/ SRC /index.js”,”async”]) and then checkdeferredModules. This function checks which chunk is installed, but some modules are executed and depend on some chunk. The module is not executed until the dependent chunk is loaded. The./webpack/ SRC /index.js module executes the chunk that depends on async.

A small summary

At this point, it seems that the chunk-bundled file execution process is clear. If you can figure out how to use HTML in two ways that will not cause the file to fail, then you really understand:

<! --> <script type="text/ JavaScript "SRC ="async.bundle_9b9adb70.js"></script> <script type="text/ JavaScript" src="index.4f7fc812.js"></script> <! >< script type="text/ JavaScript "SRC ="index.4f7fc812.js"></script> <script type="text/ JavaScript" src="async.bundle_9b9adb70.js"></script>

Loading on demand: dynamic loading process analysis

After multi-package loading is sorted out, I will look at on-demand loading, which is not so complicated, because many implementations are completed on the basis of multi-package loading. In order to make the theory clearer, I added two places of on-demand loading with the same rhythm:

// The entry file, index.js, lists only the new code let count = 0; const clickButton = document.createElement('button'); const name = document.createTextNode("CLICK ME"); clickButton.appendChild(name); document.body.appendChild(clickButton); clickButton.addEventListener('click', () => { count++; import('./utils/math').then(modules => { console.log('modules', modules); }); if (count > 2) { import('./utils/fire').then(({ default: fire }) => { fire(); }); } }) // utils/fire export default function fire() { console.log('you are fired'); } // utils/math export default function add(a, b) { return a + b; }

The code is simple, just add a button to the page, when the button is clicked, load the utils/math module as needed, and print the output module; When the number of hits is greater than two, load the utils/fire module as needed and call the exposed fire function. Js: 0. Bundle_29180b93.js and 1. Bundle_42bc336c.js: 0. Bundle_29180b93.js and 1. Bundle_42bc336c.js: 0. Bundle_29180b93.js

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],{
"./webpack/src/utils/math.js":
  (function(module, __webpack_exports__, __webpack_require__) {})
}]);

The format is identical to the Async Chunk format above.

Then let’s look at what’s added to index.js when it’s packaged:

(function(modules) {// script URL calculation method. If the following two hashes are familiar, yes, the hash value of the two on-demand files is passed in 0, Function jsonpScriptSrc(chunkId) {return __webpack_require__. P + "" + ({} [chunkId] | | chunkId) + "bundle_" + {" 0 ":" 29180 b93 ", "1" : "42 bc336c} [chunkId] +" js "} / / on-demand loaded script method __webpack_require__. E = function requireEnsure(chunKid) {// More on this later}; })({ "./webpack/src/index.js": Function (module, __webpack_exports__, __webpack_require__) {// List only the code that loads utils/fire.js on demand __webpack_require__. E (/*! import() */ 0) .then(__webpack_require__.bind(null, "./webpack/src/utils/fire.js")) .then(function (_ref) { var fire = _ref["default"]; fire(); }); }})

In the previous section, we added very little code, mainly covering two methods, jsonpScriptSrc and requireEnsure. The former is well documented in the comments, and the latter is simply creating a script tag dynamically, loading the desired JS file dynamically, and returning a Promise. Take a look at the code:

__webpack_require__.e = function requireEnsure(chunkId) { var promises = []; var installedChunkData = installedChunks[chunkId]; If (installedChunkData! == 0) {// A Promise means "currently loading"; // A Promise means "currently loading"; // A Promise means "currently loading"; if(installedChunkData) { promises.push(installedChunkData[2]); } else {// Setup Promise in the chunk cache: New Promise = new Promise(function(resolve,)); reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); Promise. push(instAlledChunkData [2] = promise. push(instAlledChunkData [2] = promise. push(instAlledChunkData [2] = promise); Var script = document.createElement('script'); 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 (); var error = new error (); OnScriptComplete = function (event) {// Error handling}; var timeout = setTimeout(function(){ onScriptComplete({ type: 'timeout', target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; / / script is added to the body the document. The head. The appendChild (script). } } return Promise.all(promises); };

The code implementation of RequireEnsure is relatively unexceptional. It’s all routine operations, but it’s clever to use Promises instead of the usual onLoad callbacks. Whether the module has been installed, or use the WebpackJSONP push agent above to complete.

Now to add the code that I said I would save for the next section:

function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var executeModules = data[2]; var moduleId, chunkId, i = 0, resolves = []; for(; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(Object.prototype.hasOwnProperty.call(installedChunks, ChunKid) &&instAlledchunks [chunKid]) {// InstAlledChunks [chunKid] is an array of [resolve, reject, promise] elements. The resolve callback is taken here; resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; // MoreModules injection ignores while(resolves.length) {// Resolve here, then Promise. All completes resolves.shift()(); }}}

So what the code above does is, it still uses this proxy to resolve the newly generated promise when the chunk load is completed, so that the on-demand load then can continue to execute, a very tortuous publish and subscribe.

conclusion

From there, the process of executing Webpack packaged code is analyzed, which is not easy to understand, but easier to understand with a little more patience. After all, the depth of Wbepack is hidden in Webpack’s own plug-in system, the code is basically ES5 level, but with some clever methods, such as push interceptor proxies.

If anything is not clear, the recommended Clone project, package your own analysis code: Demo address: Webpack project

(https://github.com/closertb/c….