While working with WebPack, have you ever wondered why webPack-packed code runs directly in the browser? Why does Webpack support the latest ES6 syntax? Why is it possible to write the import ES6 module in Webpack and also support the Require CommonJS module?

The module specification

For modules, let’s take a look at the current mainstream Module specifications (AMD/CMD specifications have very little room to live since the ES6 Module and Webpack tools) :

  • CommonJS
  • UMD
  • ES6 Module

CommonJS

Before ES6, JS did not have its own module specification, so the community developed the CommonJS specification. The module system used by NodeJS is based on the CommonJS specification.

/ / CommonJS export
module.exports = { age: 1.a: 'hello'.foo:function(){}}/ / CommonJS import
const foo = require('./foo.js')
Copy the code

UMD

According to the current running environment, if it is Node environment, it is to use CommonJS specification, if it is not AMD environment, and finally export global variables. This allows the code to run in both Node and browser environments. At present, most libraries are packaged into UMD specification, Webpack also supports UMD packaging, configuration API is output.libraryTarget. A detailed example can be found in the author’s packaged NPM toolkit: cache-manage-js

(function (global, factory) {
    typeof exports === 'object' && typeof module! = ='undefined' ? module.exports = factory() :
    typeof define === 'function'&& define.amd ? define(factory) : (global.libName = factory()); } (this, (function () { 'use strict'; })));Copy the code

ES6 Module

The ES6 module is designed to be as static as possible, so that the module dependencies, as well as the input and output variables, can be determined at compile time. Specific ideas and syntax can be seen in another article: ES6- module details

// Export the ES6 module
export default { age: 1.a: 'hello'.foo:function(){}}// Es6 module import
import foo from './foo'
Copy the code

Webpack module packaging

Given that there are so many module specifications, how does WebPack parse different modules?

Webpack identifies Module dependencies in the entry file according to the entry file in webpack.config.js. Whether the Module dependencies are written in CommonJS or ES6 Module specification, WebPack automatically analyzes them, converts them, compiles the code, and packages them into the final file. The module implementation in the final file is based on webPack’s own implementation of WebPack_require (ES5 code), so the packaged file can run in the browser.

This also means that in a Webapck environment, you can write code using ES6 module syntax alone (which is usually what we do), CommonJS module syntax, or even a mixture of the two. Since webpack2 started, built-in support for ES6, CommonJS, AMD modular statements, Webpack will analyze the syntax of various modules, and do conversion compilation.

Let’s take a look at the packaged source file as an example. The source code is in webpack-module-example

// webpack.config.js
const path = require('path');

module.exports = {
    mode: 'development'.// JavaScript executes the entry file
  entry: './src/main.js'.output: {
    // Merge all dependent modules into a bundle.js file
    filename: 'bundle.js'.// Put the output files in the dist directory
    path: path.resolve(__dirname, './dist'),}};Copy the code
// src/add
export default function(a, b) {
    let { name } = { name: 'hello world,'} // ES6 syntax is deliberately used here
    return name + a + b
}

// src/main.js
import Add from './add'
console.log(Add, Add(1.2))
Copy the code

The condensed bundle.js file is as follows:

// Modules is an array of modules, each element of which is stored {module path: module export code function}
(function(modules) {
// Module cache function, the loaded module can not be re-read, improve performance
var installedModules = {};

// Key function, load module code
// Looks a bit like the CommonJS module for Node, but here is es5 code that runs in the browser
function __webpack_require__(moduleId) {
  // Cache check, if any, directly from cache
  if(installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // Create an empty module and stuff it into the cache
  var module = installedModules[moduleId] = {
    i: moduleId,
    l: false.// Whether the tag has been loaded
    exports: {} // The initial module is empty
  };

  // Mount the contents of the module to module.exports
  modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
  module.l = true; // mark as loaded

  // Return the loaded module, which can be called directly by the caller
  return module.exports;
}

// r function under the __webpack_require__ object
Exports defines an __esModule to true, indicating that it is a module object
__webpack_require__.r = function(exports) {
  Object.defineProperty(exports, '__esModule', { value: true });
};

// Start the entry module main.js
return __webpack_require__(__webpack_require__.s = "./src/main.js"); ({})/ / add module
  "./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {
    // Define __esModule to true on module.exports
    __webpack_require__.r(__webpack_exports__);
    // Assign the add module content directly to the module.exports.default object
    __webpack_exports__["default"] = (function(a, b) {
      let { name } = { name: 'hello world,'}
      return name + a + b
    });
  }),

  // Entry module
  "./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__)
    // Get the add module definition
    // _add__WEBPACK_IMPORTED_MODULE_0__ = module.exports, similar to require
    var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/add.js");
    // add module contents: _add__WEBPACK_IMPORTED_MODULE_0__["default"]
    console.log(_add__WEBPACK_IMPORTED_MODULE_0__["default"].Object(_add__WEBPACK_IMPORTED_MODULE_0__["default"]) (1.2))})});Copy the code

The core code above allows the packaged code to run directly in the browser because webpack emulates module loading via the __webpack_require__ function (similar to node’s require syntax), which mounts the defined module contents to module.exports. The __webpack_require__ function also optimizes module caching to prevent module reloads and improve performance.

Let’s take a look at the source of Webpack:

// webpack/lib/MainTemplate.js

// Main file template
// The final file generated by Webpack is called chunk. Chunk contains several logical modules, namely modules
this.hooks.render.tap( "MainTemplate",
(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
  const source = new ConcatSource();
  source.add("/******/ (function(modules) { // webpackBootstrap\n");
  __webpack_require__ is in the bootstrapSource
  source.add(new PrefixSource("/ * * * * * * /", bootstrapSource));
  source.add("/******/ })\n");
  source.add(
    "/************************************************************************/\n"
  );
  source.add("/ / * * * * * * (");
  source.add(
    // All dependent modules write arrays
    this.hooks.modules.call(
      new RawSource(""),
      chunk,
      hash,
      moduleTemplate,
      dependencyTemplates
    )
  );
  source.add(")");
  return source;
}
Copy the code

Webpack ES6 syntax support

Careful readers may notice that the packaged add module code above still contains ES6 syntax, which is not supported in low-end browsers. This is because there is no corresponding loader to parse JS code, webPack treats all resources as modules, and different resources are converted by different Loaders.

This is handled using babel-Loader and its plugin @babel/preset-env to turn ES6 code into ES5 code that can run in the browser.

// webpack.config.js
module.exports = { ... .module: {
    rules: [{// For file resources with the js suffix, use Babel
        test: /\.m? js$/.exclude: /(node_modules|bower_components)/.use: {
          loader: 'babel-loader'.options: {
            presets: ['@babel/preset-env'}}}]}};Copy the code
// Es6 syntax processed by Babel
__webpack_exports__["default"] = (function (a, b) {
  var _name = {    name: 'hello world,'  }, name = _name.name;
  return name + a + b;
});
Copy the code

The Webpack module loads asynchronously

Webpack packages all modules into the main file, so modules are loaded synchronously. But loading on demand (also known as lazy loading) is also one of the most commonly used optimization techniques in application development. Loading on demand, generally speaking, means that the code is executed to the asynchronous module (the module content is in another JS file), the corresponding asynchronous module code is immediately loaded through the network request, and then the following process is continued. So how does WebPack determine which code is an asynchronous module when executing it? How does WebPack load asynchronous modules?

Webpack has a require.ensure API syntax to mark asynchronous loading modules, and the latest Webpack4 recommends using the new import() API (with the @babel/plugin-syntax-dynamic-import plug-in). Since require. Ensure executes the following process via a callback function, and import() returns a promise, this means that the latest ES8 async/await syntax makes it possible to execute an asynchronous process as if writing synchronous code.

Now let’s take a look at webPack’s source code to see how it implements asynchronous module loading. Modify import file main.js to introduce async module:

// main.js
import Add from './add'
console.log(Add, Add(1.2), 123)

// Load as needed
// Ensure: require. Ensure
// require.ensure([], function(require){
// var asyncModule = require('./async')
// console.log(asyncModule.default, 234)
// })

// Approach 2: webpack4 new import syntax
// add @babel/plugin-syntax-dynamic-import plugin
let asyncModuleWarp = async() = >await import('./async')
console.log(asyncModuleWarp().default, 234)
Copy the code
// async.js
export default function() {
    return 'hello, aysnc module'
}
Copy the code

The above code package generates two chunk files, the main file main.bundle.js and the asynchronous module file 0.bundle.js. Similarly, in order to facilitate readers to quickly understand, simplify and retain the main process code.

// 0.bundle.js

// Async module
// Window ["webpackJsonp"] is a bridge between multiple chunk files
// window["webpackJsonp"].push = primary chunk file. webpackJsonpCallback
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  [0].// The async module identifies chunkId, which can determine whether the async code is successfully loaded
  {module path: module contents}
  {
  "./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {
      __webpack_require__.r(__webpack_exports__);
      __webpack_exports__["default"] = (function () {
        return 'hello, aysnc module'; }); }}));Copy the code

As we know above, the source code of the asynchronous module is stored in the packaged file of the asynchronous module. In order to distinguish different asynchronous modules, the corresponding identifier of the asynchronous module is also saved: chunkId. The above code actively calls the window[“webpackJsonp”].push function, which is the key function that connects the asynchronous module to the main module. This function is defined in the main file. Window [“webpackJsonp”]. Push = webpackJsonpCallback

// main.bundle.js

(function(modules) {
// The callback function after getting the asynchronous chunk code
// The key function to connect two module files
function webpackJsonpCallback(data) {
  var chunkIds = data[0]; //data[0] stores the chunkId corresponding to the asynchronous module
  var moreModules = data[1]; // data[1] stores the asynchronous module code

  // The async module is loaded successfully
  var moduleId, chunkId, i = 0, resolves = [];
  for(; i < chunkIds.length; i++) { chunkId = chunkIds[i];if(installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    installedChunks[chunkId] = 0;
  }

  // Store all asynchronous module code in modules
  // All the asynchronous code has been synchronously loaded into the main module
  for(moduleId in moreModules) {
    modules[moduleId] = moreModules[moduleId];
  }

  Resolve () = installedChunks[chunkId][0](
  while(resolves.length) { resolves.shift()(); }};// Record which chunks have been loaded
var installedChunks = {
  "main": 0
};

// __webpack_require__ is still the synchronous read module code
function __webpack_require__(moduleId) {... }// Load the asynchronous module
__webpack_require__.e = function requireEnsure(chunkId) {
  / / create a promise
  // Save resolve in installedChunks[chunkId] and wait for the code to load before executing resolve() to return the promise
  var promise = new Promise(function(resolve, reject) {
    installedChunks[chunkId] = [resolve, reject];
  });

  // Load chunk code asynchronously by inserting script tags into the head header
  var script = document.createElement('script');
  script.charset = 'utf-8';
  script.timeout = 120;
  script.src = __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".bundle.js"
  var onScriptComplete = function (event) {
    var chunk = installedChunks[chunkId];
  };
  script.onerror = script.onload = onScriptComplete;
  document.head.appendChild(script);

  return promise;
};

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] | | [];// Window ["webpackJsonp"].push = webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;

// entry execution
return __webpack_require__(__webpack_require__.s = "./src/main.js"); ({})"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {... }),"./src/main.js": (function(module, exports, __webpack_require__) {
  // Synchronization mode
  var Add = __webpack_require__("./src/add.js").default;
  console.log(Add, Add(1.2), 123);

  // Asynchronous
  var asyncModuleWarp =function () {
    var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() {
      return regeneratorRuntime.wrap(function _callee$(_context) {
        // When asynchronous code is executed, the __webpack_require__.e method is executed
        // __webpack_require__.e returns a promise that the asynchronous code has been loaded into the main module
        // Next, load the module directly as if it were synchronized
        return __webpack_require__.e(0)
              .then(__webpack_require__.bind(null."./src/async.js"))
      }, _callee);
    }));

    return function asyncModuleWarp() {
      return _ref.apply(this.arguments); }; } ();console.log(asyncModuleWarp().default, 234)})});Copy the code

As can be seen from the above source code, the asynchronous loading of WebPack implementation modules is a bit like the jSONP process. In the main JS file, the module information is asynchronously loaded by building script tags in the head. The webpackJsonpCallback function is then used to synchronize the source code of the asynchronous module to the main file, so that the asynchronous module can behave like the synchronous module. Source code specific implementation process:

  1. When an asynchronous module is encountered, use__webpack_require__.eFunction to load asynchronous code in. This function dynamically adds a script tag to the HTML head, SRC pointing to the file stored in the specified asynchronous module.
  2. The loaded asynchronous module file is executedwebpackJsonpCallbackFunction to load the asynchronous module into the main file.
  3. So it can be used directly as a synchronization module__webpack_require__("./src/async.js")Load asynchronous modules.

The async resolve() module is only resolved () when the primary module is loaded.

conclusion

  1. Webpack implements ES /CommonJS modules based on its own implementation of Webpack_require, so the code can run in the browser.
  2. Support for ES6, CommonJS, and AMD modular statements has been built in since webpack2. But without the conversion of the new ES6 syntax to ES5 code, that work is left to Babel and its plug-ins.
  3. You can use both ES6 modules and CommonJS modules in Webpack. Because module.exports is like export default, ES6 modules are easily compatible with CommonJS: import XXX from ‘commonjs-module’. Default: require(‘es-module’).default
  4. The implementation process of Webpack asynchronous loading module is basically the same as jSONP.

Refer to the article

  • Front-end modular: CommonJS,AMD,CMD,ES6
  • Dive into CommonJs and ES6 Modules
  • What does Webpack code into
  • Webpack source code analysis
  • Webpack Code Splitting