Webpack is one of the most important packaging tools to master at this stage. We know that WebPack builds a dependency graph recursively, which contains each module of the application, and then packages these modules into one or more bundles.

So what does webPack code look like? How are bundles connected together? How is the relationship between modules handled? What about dynamic import()?

This article takes a step-by-step look at the code behind WebPack

The preparatory work

Create a file and initialize it

mkdir learn-webpack-output
cd learn-webpack-output
npm init -y 
yarn add webpack webpack-cli -D
Copy the code

Create a new webpack.config.js file in the root directory. This is the default webPack configuration file

const path = require('path');

module.exports = {
  mode: 'development'.// Can be set to production
  // Execute the entry file
  entry: './src/index.js'.output: {
    // The output file name
    filename: 'bundle.js'.// Output files are placed in dist
    path: path.resolve(__dirname, './dist')},// To make it easier to view the output
  devtool: 'cheap-source-map'
}
Copy the code

Then we go back to package.json and add the command to start the Webpack configuration in NPM script

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1"."build": "webpack"
}
Copy the code

Create a new SRC folder and add the index.js file and sayHello file

// src/index.js
import sayHello from './sayHello';

console.log(sayHello, sayHello('Gopal'));
Copy the code
// src/sayHello.js
function sayHello(name) {
  return `Hello ${name}`;
}

export default sayHello;
Copy the code

After all preparations are complete, execute YARN Build

Analysis main process

Look at the output file, here does not put the specific code, a bit of space, you can click here to view

It’s an IIFE

Don’t panic, let’s break it down a little bit. In fact, the overall file is an IIFE — execute functions immediately.

(function(modules) { // webpackBootstrap
	// The module cache
	var installedModules = {};
	function __webpack_require__(moduleId) {
    / /... Omit details
	}
	// Import file
	return __webpack_require__(__webpack_require__.s = "./src/index.js"); ({})"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
  "./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__) {})});Copy the code

Modules is an object, the key of the object is the relative path of each JS module, and value is a function (we’ll call it module function below). IIFE will require the entry module first. / SRC /index.js:

// Import file
return __webpack_require__(__webpack_require__.s = "./src/index.js");
Copy the code

Sayhello.js “./ SRC/sayhello.js “. Sayhello.js (sayHello.js, sayhello.js, sayhello.js)

{
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) { 
    __webpack_require__.r(__webpack_exports__);
	  var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
    console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"].Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"]) ('Gopal')); })}Copy the code

Important implementation mechanism —__webpack_require__

The main function to require other modules here is __webpack_require__. Let’s focus on the __webpack_require__ function

  // Cache module use
  var installedModules = {};
  // The require function
  // Simulate module loading, webpack implementation of require
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    // Check whether the module is in the cache, if yes, it is directly fetched from the cache
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    // If not, create the module and put it in the cache, where the key value is the module Id, i.e. the file path mentioned above
    var module = installedModules[moduleId] = {
      i: moduleId, // Module ID
      l: false.// Whether the command has been executed
      exports: {}};// Execute the module function
    // Executes module functions and mounts them to module.exports. This points to the module. Exports
    modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);

    // Flag the module as loaded
    // Indicate that the module has been loaded
    module.l = true;

    // Return the exports of the module
    // module.exports exports are exported as arguments when modules are executed, and exports export interfaces that are exposed to the outside world, such as functions and variables
    return module.exports;
  }
Copy the code

First step, Webpack here to do a layer of optimization, through the object installedModules cache, check whether the module is in the cache, if there is directly from the cache, create and put in the cache, where the key value is the module Id, that is, the file path mentioned above

Module, module.exports, __webpack_require__ as arguments, and point the module’s function call object to module.exports, ensuring that the this pointer in the module always points to the current module.

The third step is to return the loaded module and the caller can call it directly.

So __webpack_require__ loads a module and returns the module.exports variable at the end

How does WebPack support ESM

As you may have noticed, my above writing is ESM writing, for some modular solutions, you can see my other article.

Let’s go back to the module function

{
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) { 
    __webpack_require__.r(__webpack_exports__);
	  var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
    console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"].Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"]) ('Gopal')); })}Copy the code

Let’s look at the __webpack_require__.r function

__webpack_require__.r = function(exports) {
 object.defineProperty(exports.'__esModule', { value: true });
};
Copy the code

Add an attribute __esModule with a value of true to __webpack_exports__

Look again at an implementation of __webpack_require__.n

// 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;
};
Copy the code

__webpack_require__.n determines whether a module is an ES module, identifies it as an ES module when __esModule is true, returns module.default, and returns module otherwise.

Finally __webpack_require__. D, the main job is to bind the above getter function to the getter of the a property in exports

// 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 }); }};Copy the code

__webpack_exports__[“default”], which is __webpack_require__. Webpack actually supports CommonJS and ES Module intermixing

 "./src/sayHello.js":
  / *! exports provided: default */
 (function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_require__.r(__webpack_exports__);
  function sayHello(name) {
    return `Hello ${name}`;
  }
  /* harmony default export */ __webpack_exports__["default"] = (sayHello);
 })
Copy the code

Now that we have a rough idea of how webPack files work, let’s take a look at a particular scenario for code separation – dynamic import

Dynamic import

Code separation is one of the most compelling features of WebPack. This feature enables you to separate code into different bundles and load these files on demand or in parallel. Code separation can be used to obtain smaller bundles and control resource load priorities, which, when used properly, can greatly affect load times.

Common code segmentation methods are as follows:

  • Entry starting point: Manually detach code using the Entry configuration.
  • To prevent duplication, use Entry Dependencies or SplitChunksPlugin to delete and separate chunks.
  • Dynamic import: Separation of code through inline function calls to modules.

In this article we’ll focus on dynamic imports. We’ll create a new file under SRC, another.js

function Another() {
  return 'Hi, I am Another Module';
}

export { Another };
Copy the code

Modified index. Js

import sayHello from './sayHello';

console.log(sayHello, sayHello('Gopal'));

// For demonstration purposes, load dynamically only when conditions are available
if (true) {
  import('./Another.js').then(res= > console.log(res))
}
Copy the code

If we look at the packaged content, we ignore the.map file, and we see an additional 0.bundle.js file, which we call a dynamically loaded chunk, and bundle.js, which we call the main chunk

For the output code, the main chunk is shown here, and the dynamically loaded chunk is shown here. The following is an analysis of the two codes

Main chunk analysis

Let’s start with the main chunk

More content, let’s take a closer look:

The first thing we notice is that the place we import dynamically compiles to the following, which looks like an asynchronous loading function

if (true) {
  __webpack_require__.e(/ *! import() */ 0).then(__webpack_require__.bind(null./ *! ./Another.js */ "./src/Another.js")).then(res= > console.log(res))
}
Copy the code

So let’s look at the implementation of __webpack_require__.e

__webpack_require__.eDynamic loading using JSONP

// The loaded chunk cache
var installedChunks = {
  "main": 0
};
// ...
__webpack_require__.e = function requireEnsure(chunkId) {
  // Promises queue, wait for asynchronous chunks to load before performing a callback
  var promises = [];

  // JSONP chunk loading for javascript
  var installedChunkData = installedChunks[chunkId];
  // 0 indicates installed
  if(installedChunkData ! = =0) { // 0 means "already installed".

    // a Promise means "currently loading".
    // The target chunk is loading, push the promise to Promises array
    if(installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      // setup Promise in chunk cache
      // Use Promise to load the target chunk asynchronously
      var promise = new Promise(function(resolve, reject) {
        / / set installedChunks [chunkId]
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      // I sets three states of chunk loading and stores them in installedChunks to prevent chunk reloading
      // nstalledChunks[chunkId] = [resolve, reject, promise]
      promises.push(installedChunkData[2] = promise);
      // start chunk loading
      / / using json
      var head = document.getElementsByTagName('head') [0];
      var script = document.createElement('script');

      script.charset = 'utf-8';
      script.timeout = 120;

      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      // Get the address of the target chunk. __webpack_require__.p indicates the set publicPath, which is empty by default
      script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
      // call the method when the request times out, which is 120 seconds
      var timeout = setTimeout(function(){
        onScriptComplete({ type: 'timeout'.target: script });
      }, 120000);
      script.onerror = script.onload = onScriptComplete;
      // Set the loading complete or error callback
      function onScriptComplete(event) {
        // avoid mem leaks in IE.
        // Prevent IE memory leaks
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        // The webpackJsonpCallback function is loaded
        if(chunk ! = =0) {
          if(chunk) {
            var errorType = event && (event.type === 'load' ? 'missing' : event.type);
            var realSrc = event && event.target && event.target.src;
            var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ':' + realSrc + ') ');
            error.type = errorType;
            error.request = realSrc;
            chunk[1](error);
          }
          installedChunks[chunkId] = undefined; }}; head.appendChild(script); }}return Promise.all(promises);
};
Copy the code
  • You can see that import() is converted to simulate JSONP to load dynamically loaded chunk files

  • Set three states of chunk loading and cache them in installedChunks to prevent chunk reloading. These state changes are mentioned in the webpackJsonpCallback

    / / set installedChunks [chunkId]
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
    Copy the code
    • installedChunks[chunkId]for0, on behalf of thechunkIt’s loaded
    • installedChunks[chunkId]forundefined, on behalf of thechunkThe loading failed, timed out, or was never loaded
    • installedChunks[chunkId]forPromiseObject representing thechunkBeing loaded

After __webpack_require__.e, we know that JSONP is used to dynamically import the chunk file and process it according to the resulting state. So how do we know the state after the import? What about chunks loaded asynchronously

Asynchronous Chunk

// window["webpackJsonp"] is actually an array to which an element is added. This element is also an array in which the first element is chunkId and the second object is the same as the argument passed into IIFE
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0] and {/ * * * / "./src/Another.js":
  / * * * / (function(module, __webpack_exports__, __webpack_require__) {
  
  "use strict";
  __webpack_require__.r(__webpack_exports__);
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Another".function() { return Another; });
  function Another() {
    return 'Hi, I am Another Module';
  }
  / * * * /}}));//# sourceMappingURL=0.bundle.js.map
Copy the code

The main thing to do is to stuff an element into an array window[‘webpackJsonp’], which is also an array, where the first element of the array is chunkId and the second object is similar to the IIFE argument passed in to the main chunk. The key is where does this window[‘webpackJsonp’] come in? Let’s go back to the main chunk. Return __webpack_require__(__webpack_require__. S = “./ SRC /index.js”); There’s still a bit to go before the entrance

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] | | [];// Save the original array.prototype. push method
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// Change the implementation of the push method to webpackJsonpCallback
// So the window['webpackJsonp']. Push that we execute in the asynchronous chunk is actually the webpackJsonpCallback function.
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
// Execute the webpackJsonpCallback method on the elements already in the array
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
Copy the code

JsonpArray is the window[“webpackJsonp”], which executes the webpackJsonpCallback when the push method is executed. This function is called when the push operation is complete

jsonpArray.push = webpackJsonpCallback;
Copy the code

WebpackJsonpCallback – a callback after loading dynamic chunk

Let’s look at the webpackJsonpCallback function, where the input parameter is the dynamically loaded chunk window[‘webpackJsonp’] push parameter.

var installedChunks = {
  "main": 0
};	

function webpackJsonpCallback(data) {
  // the first argument in window["webpackJsonp"] -- [0]
  var chunkIds = data[0];
  // See the second parameter in push window["webpackJsonp"] in the packaged chunk module for details of the corresponding module
  var moreModules = data[1];

  // add "moreModules" to the modules object,
  // then flag all "chunkIds" as loaded and fire callback
  var moduleId, chunkId, i = 0, resolves = [];
  for(; i < chunkIds.length; i++) { chunkId = chunkIds[i];[resolve, reject, promise] [resolve, reject, promise]
    // This can look at the state set in __webpack_require__
    // Represents the chunk being executed, which is added to the CONVERger array
    if(installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    // mark execution as completed
    installedChunks[chunkId] = 0;
  }
  // Add modules of the asynchronous chunk to the modules array of the main chunk one by one
  for(moduleId in moreModules) {
    if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; }}// parentJsonpFunction: the primitive array push method adds data to the window["webpackJsonp"] array.
  if(parentJsonpFunction) parentJsonpFunction(data);
  // When the while loop ends, the return value of __webpack_require__.e promises to resolve
  / / resolove execution
  while(resolves.length) { resolves.shift()(); }};Copy the code

When we are going to load the json asynchronous chunk is completed, will be to carry out the window (” webpackJsonp “) | | []). Push, namely webpackJsonpCallback. There are mainly the following steps

  • Go through the chunkIds to load, find the chunks that are not finished executing, and join in the convergences
for(; i < chunkIds.length; i++) { chunkId = chunkIds[i];[resolve, reject, promise] [resolve, reject, promise]
  // This can look at the state set in __webpack_require__
  // Represents the chunk being executed, which is added to the CONVERger array
  if(installedChunks[chunkId]) {
    resolves.push(installedChunks[chunkId][0]);
  }
  // mark execution as completed
  installedChunks[chunkId] = 0;
}
Copy the code
  • The unexecuted state is non-zero and is set to zero upon completion

  • InstalledChunks [chunkId][0] is actually resolve in the Promise constructor

    // __webpack_require__.e 
    var promise = new Promise(function(resolve, reject) {
    	installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    Copy the code
  • Add modules from the asynchronous chunk to the modules array of the main chunk one by one

  • The original array push method adds data to the window[“webpackJsonp”] array

  • The various convertor methods are performed to tell the state of the callback function in __webpack_require__.e

Only when this method completes will we know if JSONP is successful, that is, script.onload/ onError will be executed after webpackJsonpCallback. Onload/onError is used to check whether the chunks in installedChunks are set to 0 for completion of webpackJsonpCallback

Dynamic import summary

The general process is shown in the figure below

conclusion

This article analyzes the main webPack packaging process and the output code in the case of dynamic loading, summarized below

  • The overall file is oneIIFE— Execute the function immediately
  • webpackLoaded files are cached to optimize performance
  • Mainly through__webpack_require__ To simulate theimportA module and returns the module at the endexportThe variables of
  • webpackHow to supportES Module
  • Dynamic loadingimport()The main implementation is usingJSONPDynamically load the module and passwebpackJsonpCallbackDetermine the loading result

reference

  • Analyze the webpack packed file
  • Webpack packages product code analysis
  • “Webpack series” – Route lazy loading principle