preface

Q: What’s the difference between module exports in CommonJS and ES Modules?

  • CommonJSExporting is a copy of the value;ES ModulesWhat is exported is a “reference” to the value.

I’m sure a lot of you know this answer off the top of your head. Okay, go ahead.

Q: Is the CommonJS exported value a shallow copy or a deep copy?

Q: Can you emulate the export mechanism for ES Modules?

Webpack is a packaging tool that makes the ES Modules and CommonJS workflows instantly clear.

The preparatory work

Initialize the project and install the beta version of WebPack 5, which has many optimizations compared to WebPack 4: better support for ES Modules, and streamlined packaged code.

$ mkdir demo && cd demo
$ yarn init -y
$ yarn add webpack@next webpack-cli
# or yarn add [email protected] webpack-cli
Copy the code

As early as Webpack4, the concept of no configuration has been introduced. There is no need to provide the webpack.config.js file, which by default uses SRC /index.js as the entry file and generates the packaged main.js file to be placed in the dist folder.

Make sure you have the following directory structure:

├ ─ ─ dist │ └ ─ ─ index. The HTML ├ ─ ─ the SRC │ └ ─ ─ index. The js ├ ─ ─ package. The json └ ─ ─ yarn. The lockCopy the code

To introduce packaged main.js in index.html:

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="main.js"></script>
  </body>
</html>
Copy the code

Add a command script to package.json:

"scripts": {
  "start": "webpack"
},
Copy the code

Running no-configuration packaging:

$ yarn start
Copy the code

The terminal will prompt:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
Copy the code

Webpack requires the user to provide the Mode option when packaging to indicate whether the packaged resource is for development or production, allowing WebPack to use its built-in optimizations accordingly, which default to Production.

We set it to None to avoid interference from the default behavior so that we can better analyze the source code.

Modify the package. The json:

"scripts": {
  "start": "webpack --mode=none"
},
Copy the code

Re-run, webpack generates a packaged main.js file in the dist directory. Since the entry file is empty, the source code for main.js is a single IIFE (execute now function), which seems simple, but is extremely important.

(() = > {
  // webpackBootstrap}) ();Copy the code

We know that in both CommonJS and ES Modules, a file is a module, and the Modules’ scopes are isolated from each other and do not pollute the global scope. This is where IIFE comes in. It wraps the entire JS code of a file into a closure function that not only functions themselves, but also ensures that the scope between functions does not pollute each other, and there is no direct access to internal variables outside the closure function unless they are explicitly exported.

var name = "webpack";

(() = > {
  var name = "parcel";
  var age = 18;
  console.log(name); // parcel}) ();console.log(name); // webpack
console.log(age); // ReferenceError: age is not defined
Copy the code

Copy VS reference

Next, enter the practice part, involving the source code reading, let us in-depth understanding of CommonJS and ES Modules differences.

CommonJS

New SRC/counter. Js:

let num = 1;

function increase() {
  return num++;
}

module.exports = { num, increase };
Copy the code

Modified index. Js:

const { num, increase } = require("./counter");

console.log(num);
increase();
console.log(num);
Copy the code

If you read the previous statement, no doubt, print 1, 1.

So according to? We looked at main.js, which had the answer we were looking for, after removing the useless comments:

(() = > {
  var __webpack_modules__ = [
    ,
    module= > {
      let num = 1;

      function increase() {
        return num++;
      }

      module.exports = { num, increase }; },];var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if (__webpack_module_cache__[moduleId]) {
      return __webpack_module_cache__[moduleId].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 module.exports;
  }

  (() = > {
    const { num, increase } = __webpack_require__(1);

    console.log(num);
    increase();
    console.log(num); }) (); }) ();Copy the code

It can be simplified as:

(() = > {
  var__webpack_modules__ = [...] ;var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {... } (() = > {
    const { num, increase } = __webpack_require__(1);

    console.log(num);
    increase();
    console.log(num); }) (); }) ();Copy the code

The outermost layer is an IIFE, executed immediately.

__webpack_modules__, which is an array with an empty first entry and an arrow function passing in the module argument, contains all the code in counter.js.

__webpack_module_cache__ Caches modules that have been loaded.

function __webpack_require__(moduleId) {… } is similar to require(), which checks __webpack_module_cache__ to see if the module has already been loaded, and returns the contents of the cache if it has. Otherwise, create a new module: {exports: {}}, set the cache, execute the module function, and return module.exports

Finally, an IIFE wraps the code in index.js and executes __webpack_require__(1), exporting num and increase for index.js.

Module. exports = {num, increase}; , is equivalent to the following:

module.exports = {
  num: num,
  increase: increase,
};
Copy the code

Module. exports[‘num’] is a basic type. If it is assigned to n1, module.exports[‘num’] already points to n2, which is also 1. But num and num are no longer relevant.

let num = 1;
// mun is equivalent to module.exports['num']
mun = num;

num = 999;
console.log(mun); / / 1
Copy the code

Module.exports [‘increase’] is a function of reference type. Increase is only a pointer. When it is assigned to module.exports[‘increase’], only Pointers are copied. Its memory address still points to the same piece of data. Module. exports[‘increase’] essentially increases by name.

Due to the nature of lexical scope, the num variable changed by increase() in counter.js is already bound when the function is declared, always bound to the memory address pointing to n1 num.

JavaScript uses lexical scope, which specifies that when a variable is accessed from within a function, the lookup variable is looked up from where the function is declared to the outer scope, rather than up from where the function was called

var x = 10;

function foo() {
  console.log(x);
}

function bar(f) {
  var x = 20;
  f();
}

bar(foo); // 10 instead of 20
Copy the code

Calling increase() does not affect the memory address pointing to num in n2, which is why 1, 1 is printed.

ES Modules

Modify counter. Js and index.js respectively, this time using ES Modules.

let num = 1;

function increase() {
  return num++;
}

export { num, increase };
Copy the code
import { num, increase } from "./counter";

console.log(num);
increase();
console.log(num);
Copy the code

Obviously, print 1, 2.

As usual, look at main.js and delete unnecessary comments as follows:

(() = > {
  "use strict";
  var __webpack_modules__ = [
    ,
    (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
      __webpack_require__.d(__webpack_exports__, {
        num: () = > /* binding */ num,
        increase: () = > /* binding */ increase,
      });
      let num = 1;

      function increase() {
        returnnum++; }},];var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {} // The same function will not be expanded

  /* webpack/runtime/define property getters */
  (() = > {
    __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); }) (); (() = > {
    var _counter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

    console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
    (0, _counter__WEBPACK_IMPORTED_MODULE_0__.increase)();
    console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num); }) (); }) ();Copy the code

After simplification, it is roughly as follows:

(() = > {
  "use strict";
  var__webpack_modules__ = [...] ;var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {... } (() = > {
    __webpack_require__.d = (exports, definition) = >{... }; }) (); (() = > {
    __webpack_require__.o = (obj, prop) = >{... }}) (); (() = > {
    var _counter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

    console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
    (0, _counter__WEBPACK_IMPORTED_MODULE_0__.increase)();
    console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num); }) (); }) ();Copy the code

Start by looking at two utility functions: __webpack_require__.o and __webpack_require__.d.

__webpack_require__. O encapsulates the Object. The prototype. HasOwnProperty. Call (obj, prop) operation.

__webpack_require__.d passes Object. DefineProperty (exports, key, {enumerable: true, get: Definition [key]}) to set getters for different properties of exports objects

We then see the familiar __webpack_modules__, which has the same form as in the previous section. The main thing is this code:

__webpack_require__.d(__webpack_exports__, {
  num: () = > /* binding */ num,
  increase: () = > /* binding */ increase,
});
Copy the code

Unlike CommonJS, ES Modules does not assign a value to module.exports directly. Instead, it assigns the value to the arrow function returned by module.exports. Where num() and increase() are executed, the num variable and increase function are returned in counter.js.

When the last IIFE is encountered, __webpack_require__(1) is called, returns module.exports and assigns the value to _counter__WEBPACK_IMPORTED_MODULE_0__. All subsequent property fetches use the dot operator, This triggers the get operation for the corresponding property, and the function is executed to return the value in counter.js.

So print 1, 2.

As an added bonus, the browser will throw an error if you try to reassign the num variable or increase function from import directly in index.js:

Uncaught TypeError: Cannot set property num of #<Object> which has only a getter
Copy the code

Remember webPack did a getter on exports objects earlier? The access descriptor for the exports object is set. Setters are not set. They default to undefined.

In “use strict” mode, assigning an attribute to an exports object will result in an error.

Webpack is designed this way to conform to the ES Modules specification, which means that the exported value of a module is read-only and you can’t change it directly in index.js. You can only change it indirectly by increasing () the counter.

In ES Modules, a constant cannot be reassigned even if it is declared using var or let.

Uncaught TypeError: Assignment to constant variable.
Copy the code

Having said that, and knowing how lexical scopes work, you can implement a “beggar’s version” of ES Modules:

function my_require() {
  var module = {
    exports: {}};let counter = 1;

  function add() {
    return counter++;
  }

  module.exports = { counter: () = > counter, add };
  return module.exports;
}

var obj = my_require();

console.log(obj.counter()); / / 1
obj.add();
console.log(obj.counter()); / / 2
Copy the code

conclusion

More to see the source code, there will be a lot of harvest, this is a process of thinking.

In the early days, browsers didn’t support either ES Modules or CommonJS.

ES Modules were written into the spec back in ES2015, but at that time it was CommonJS.

With native support from browser vendors and the introduction of Dynamic Import in ES2020, Snowpack is a promising option for those who are interested in it. Snowpack can export third-party libraries directly for browsers to run, eliminating the need for webpack packaging.