preface

There are two other articles in this series

Article 1: Do you still have many questions about modularity ❓❓❓

Part two: CommonJS & AMD & ES6

This is the third article, this article will talk about the implementation of JS Module system, including Node.js, Webpack, and browser ES Module implementation.

Node.js

What does it take to implement a modular system? First we can take a look at the implementation in Node.js, which was one of the first to support JS modular development, although it is different from the browser environment, we can also learn from it.

The module implementation of Node can be divided into the following two blocks:

  1. Analysis of the positioning
  2. Compile implementation

The first part, “analysis location,” is to locate the correct file location through the module identity passed in by require.

const utils = require('./utils')
Copy the code

There are many small details involved, such as identifiers that might be:

  • Internal modules provided by Node, such as HTTP, FS
  • Relative path or absolute path
  • Introduced NPM packages such as Express and Axios

In addition, module identifiers in code are often simplified, such as./utils, which may be.js,.json, or even.utils/index.js since it has no extension.

These details are not our developers’ concern, but are a must for modular systems in order to locate files correctly.

The second part is “Compilation and execution”. After the file is located, Node first compiles the file. Js,.node, and.json files are supported in Node, and their loading methods are also different. Each successfully compiled module is kept in the cache with the file path as an index to improve secondary import performance.

This section is about compiling JS files. As you probably already know, Node wraps JS files. For example, a module file like this:

 const math = require('math'); 
 exports.area = function (radius) { 
    return Math.PI * radius * radius; 
 };
Copy the code

When encapsulated, it becomes:

(function (exports.require.module, __filename, __dirname) { 
   const math = require('math'); 
      exports.area = function (radius) { 
   return Math.PI * radius * radius; 
   }; 
});
Copy the code

This allows scope isolation between each module file and allows exports, require variables to be used within each module.

Webpack

If you look at some of the modular implementations in the front end space, you’ll see a lot of similarities.

First, Webpack. We can easily read the packaged code by setting mode=development and devtool=false in webpack.config.js.

// webpack.config.js
const path = require('path');
module.exports = {
  entry: path.join(__dirname, 'main.js'),
  output: {
    path: path.join(__dirname, 'output'),
    filename: 'index.js',},mode: 'development'.devtool: false};Copy the code

CommonJS Module

First we configure an entry file main.js and a module that relies on bar.js, and then run Webpack (WebPack 5.51.1, webpack-CLI 4.8.0) to pack.

// main.js
let bar = require('./bar');
function foo() {
  return bar.bar();
}
​
//bar.js
exports.bar = function () {
  return 1;
};
Copy the code

Everything that wraps the result is as follows (unnecessary comments are omitted) :

(() = > {
  // webpackBootstrap
  var __webpack_modules__ = {
    './bar.js': (__unused_webpack_module, exports) = > {
      exports.bar = function () {
        return 1; }; }};// The module cache
  var __webpack_module_cache__ = {};
​
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if(cachedModule ! = =undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {},});// Execute the module function
    __webpack_modules__[moduleId](module.module.exports, __webpack_require__);
​
    // Return the exports of the module
    return module.exports;
  }
​
  var __webpack_exports__ = {};
  (() = > {
    // ./main.js
    let bar = __webpack_require__('./bar.js');
    function foo() {
      returnbar.bar(); }}) (); }) ();Copy the code

We can understand it completely if we look at these two parts.

  1. __webpack_modules__
var __webpack_modules__ = {
  './bar.js': (__unused_webpack_module, exports) = > {
    exports.bar = function () {
      return 1; }; }};Copy the code

__webpack_modules__ is an object in which the module file is stored with the module path as the key value, and the module code we originally wrote is also wrapped in a function. __unused_webpack_module and exports are passed in.

Note: This is very similar to what Node.js does. The first argument is actually module, but it is not used in the module, so it is marked __unused_webpack_module; The third argument should be require, which is also not used here, but can be seen in main.js below, which is __webpack_require__.

  1. __webpack_require__
var __webpack_module_cache__ = {};
​
function __webpack_require__(moduleId) {
  // Check if module is in cache
  var cachedModule = __webpack_module_cache__[moduleId];
  if(cachedModule ! = =undefined) {
    return cachedModule.exports;
  }
  // Create a new module (and put it into the cache)
  var module = (__webpack_module_cache__[moduleId] = {
    exports: {},});// Execute the module function
  __webpack_modules__[moduleId](module.module.exports, __webpack_require__);
​
  // Return the exports of the module
  return module.exports;
}
Copy the code

It’s actually the equivalent of the require function that we’re using, but we’ve changed the name here. __webpack_module_cache__ is first fetched from __webpack_module_cache__ and returned if it exists. If it does not, create a new module object, place it in the cache, and execute the corresponding module. Pass this module object and module.exports into the module function.

At this point, you should be able to understand how Webpack enables browsers to “run CommonJS code,” which is really very similar to Node.js 😺.

ES Module

How does Webpack implement ES Module export as a reference rather than a copy?

Again, prepare two files:

// bar.js
export let counter = 1;
​
​
// main.js
import { counter } from './bar';
console.log(counter);
Copy the code

The result of packing is as follows:

(() = > {
  // webpackBootstrap
  'use strict';
  var __webpack_modules__ = {
    './bar.js': (__unused_webpack_module, __webpack_exports__, __webpack_require__,) = > {
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, {
        counter: () = > counter,
      });
      let counter = 1; }};// The module cache
  var __webpack_module_cache__ = {};
​
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if(cachedModule ! = =undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = (__webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {},});// Execute the module function
    __webpack_modules__[moduleId](module.module.exports, __webpack_require__);
​
    // Return the exports of the module
    return module.exports;
  }
  /* webpack/runtime/define property getters */
  (() = > {
    // define getter functions for harmony exports
    __webpack_require__.d = (exports, definition) = > {
      for (var key in definition) {
        if( __webpack_require__.o(definition, key) && ! __webpack_require__.o(exports, key)
        ) {
          Object.defineProperty(exports, key, {
            enumerable: true.get: definition[key], }); }}}; }) ();/* webpack/runtime/hasOwnProperty shorthand */
  (() = > {
    __webpack_require__.o = (obj, prop) = >
      Object.prototype.hasOwnProperty.call(obj, prop); }) ();/* webpack/runtime/make namespace object */
  (() = > {
    // define __esModule on exports
    __webpack_require__.r = (exports) = > {
      if (typeof Symbol! = ='undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports.Symbol.toStringTag, {
          value: 'Module'}); }Object.defineProperty(exports.'__esModule', { value: true}); }; }) ();var __webpack_exports__ = {};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() = > {
    // ./main.js
​
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */
    var _bar__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./bar.js');
​
    console.log(_bar__WEBPACK_IMPORTED_MODULE_0__.counter); }) (); }) ();Copy the code

We’ve extracted the key parts:

var __webpack_modules__ = {
  './bar.js': (__unused_webpack_module, __webpack_exports__, __webpack_require__,) = > {
    __webpack_require__.r(__webpack_exports__);
    __webpack_require__.d(__webpack_exports__, { // 1. Note here 🌟
      counter: () = > counter,
    });
    let counter = 1; }};/* webpack/runtime/define property getters */
(() = > {
  // define getter functions for harmony exports
  __webpack_require__.d = (exports, definition) = > {
    for (var key in definition) {
      if( __webpack_require__.o(definition, key) && ! __webpack_require__.o(exports, key)
      ) {
        Object.defineProperty(exports, key, { // 2. And here 🌟
          enumerable: true.get: definition[key], }); }}}; }) ();Copy the code

Originally, here do a layer of proxy 😺, before and after packaging results comparison:

/ / before packaging
export let counter = 1;
​
/ / after packaging
let counter = 1;
Object.defineProperty(export.'counter', {
   enumerable: true.get: () = > counter
})
Copy the code

Browser ES Module implementation

Content is coming ~