In-depth understanding of CommonJS

reference

  • Node.js Design Patterns

IIFE

Before JS was modularized, data could only be encapsulated through block-level scopes, the most common of which was the use of IIFE.

const myModule = (() = > {
  const privateFoo = () = > {};
  const privateBar = [];
  const exported = {
    publicFoo: () = > {},
    publicBar: () = >{}};returnexported; }) ();// once the parenthesis here are parsed, the function will be invoked

console.log(myModule);
console.log(myModule.privateFoo, myModule.privateBar);

Copy the code
{ publicFoo: [Function: publicFoo],
  publicBar: [Function: publicBar] }
undefined undefined
Copy the code

CommonJS modules

  • throughrequireFunction to import
  • throughexportormodule.exportsexport
  • Synchronous loading

Module is loaded

Before we talk about imports and exports, let’s discuss how the imported and exported code is loaded.

function loadModule(filename, module.require) {
  const wrappedSrc = `(function (module, exports, require) { 
      ${fs.readFileSync(filename, "utf8")}
      })(module, module.exports, require)`;
  eval(wrappedSrc);
}

Copy the code

This is just an overview of the loading process. Many boundary and security issues are not considered, such as:

Here we are simply using eval for our JS code, which actually has a lot of security issues, so the actual code should be implemented using VM.

  1. The only difference between IFFE and IFFE is that we provide the following three arguments to the execution function
    • module
    • exports: We looked at the transmission and found that it was actually equal tomodule.exports
    • require

In fact, there are two additional arguments in the original code: __filename and __dirname, which is why we can use them when we write the code

  1. Read the file usingfs.readFileSync, is the use ofsynchronousRead.

!!!!!!!!! Synchronous loading!! This is the biggest feature, and we’ll talk more about it later.

Method the require

From The above code, our original JS file will be executed wrapped in a function called The Module Wrapper in The document. The file can then be executed with the parameters passed in by the function.

// 1.js
console.log(1)
Copy the code

Assuming the file is loaded, the last code to be executed is the following

(function (module.exports.require) {
  // Read the code in the 1.js file loaded from the file
  console.log(1)
})(module.module.exports, require);

Copy the code

So, if we are a loaded file, we can use the following two keywords

  • require: Uses this function to load a local file to import new module content
  • exportsandmodule.exports: Exports the values in the current module

Let’s start with a common module code

// load another dependency
const dependency = require('./anotherModule')
// a private function
function log() {
  console.log(`Well done ${dependency.username}`)}// the API to be exported for public use
module.exports.run = () = > {
  log()
}
Copy the code

Just like in IIFE with block-level scope encapsulation, through the Module Wrapper, any value in a module is private unless it is mounted on module.exports.

The require implementation

function require(moduleName) {
  console.log(`Require invoked for module: ${moduleName}`);
  const id = require.resolve(moduleName);
  if (require.cache[id]) {
    return require.cache[id].exports;
  }
  // module metadata
  const module = {
    exports: {},
    id,
  };
  // Update the cache
  require.cache[id] = module;
  // load the module
  loadModule(id, module.require);
  // return exported variables
  return module.exports;
}
require.cache = {};
require.resolve = (moduleName) = > {
  /* resolve a full module id from the moduleName */
};

Copy the code

Complete the path name &&id && resulve

function require(moduleName) {
  / /...
  const id = require.resolve(moduleName); 
  / /...
}
/ /...
require.resolve = (moduleName) = > {
  /* resolve a full module id from the moduleName */
};

Copy the code

When we get moduleName, we will complete the path of the module through require.resolve to get an absolute path. There’s a particular logic to path completion that I’ll talk about later.

The cache module &&module&& cache

function require(moduleName) {
  // ...
  const module = {
    exports: {},
    id,
  };
  require.cache[id] = module;
  // ...
}
require.cache = {};
// ...

Copy the code

Create a module object with two properties

  • Exports: empty object
  • Id: Passes beforeresulveThe resolved path

Mount the Module to reure. Cache and then query it with its ID as the key.

Since there is the concept of caching, we can determine whether the module is created before creating it. If there is, we can not skip the process of creating the module

function require(moduleName) {
  // ...
  if (require.cache[id]) {
    return require.cache[id].exports;
  }
  // Create a module
  const module = {
    exports: {},
    id,
  };
  // ...
}
// ...

Copy the code

Load the module &&loadModule

function require(moduleName) {
  const id = require.resolve(moduleName);
  const module = {
    exports: {},
    id,
  };
  // Load the module
  loadModule(id, module.require);
}

function loadModule(filename, module.require) {
  const wrappedSrc = `(function (module, exports, require) { 
          ${fs.readFileSync(filename, "utf8")}
          })(module, module.exports, require)`;
  eval(wrappedSrc);
}

Copy the code

LoadModule is what we implemented earlier, putting the code together

function require(moduleName) {
  const id = require.resolve(moduleName);
  const module = {
    exports: {},
    id,
  };
  // Load the module
  (function (filename, module.require) {(function (module.exports.require) {
      fs.readFileSync(filename, "utf8"); }) (module.module.exports, require);
  })(id, module.require);
}

Copy the code

With so many functions embedded, all it really does is execute the loaded file, pass in the moudle object we created in the beginning, and let the code we execute modify the moudle, such as the code we wrote earlier

// load another dependency
const dependency = require('./anotherModule')
// a private function
function log() {
  console.log(`Well done ${dependency.username}`)}// the API to be exported for public use
module.exports.run = () = > {
  log()
}
Copy the code

Finally, run hangs in moudle.exports, which we created earlier with the following code

const module = {
  exports: {},
  id,
};
Copy the code

Export data &&module.exports

function require(moduleName) {
  loadModule(id, module.require);
  Module.exports has been loaded with code that hangs on exposed properties
  return module.exports;
}
require.cache = {};
Copy the code

In addition to exporting, we said earlier that these modules are cached on require.

moudle.exportsandexportsUse mode and difference

We know that these two variables are passed in as function arguments, so when we use moudle and exports, we can imagine putting the code of the current file into the following code:

const initModule = {
  exports: {
    defatName: "wcdaren",}};// const ret = reuire('./ XXX ')
const ret = (function fn(module.exports) {
  // Put the code from the JS file here
  return module.exports;
})(initModule, initModule.exports);


console.log(`==============>ret`);
console.log(ret);

Copy the code

Loading strategyresolve

See the Resolve load policy documentation for details

function require(moduleName) {
  // ...
  const id = require.resolve(moduleName);
  // ...
}
// ...
require.resolve = (moduleName) = > {
  /* resolve a full module id from the moduleName */
};

Copy the code

Resolve is the property method of require. What it does is complete the path in which the reference was passed to get an absolute path string.

In actual projects, the one we use most often is

  • Import your own written file module
  • The importnode_modulesPackage module in
  • Import the core modules provided by Node

If the loading strategy is only based on the above three conditions, we can simply summarize the loading strategy order as follows:

  1. To determine whether it is a core module, look it up in the list of modules provided by Node itself and return it if it is
  2. Judgment ismoduleNameWhether or not to/or. /At the beginning, if yes, the unified conversion to the absolute path to load and return
  3. If the first two steps are not found, consider it a package module and go to the nearest onenode_moudlesTo find a

The only thing to notice here is the extension issue, assuming we load it this way

const dependency = require('./anotherModule')
Copy the code

Discard the path and set the file name anotherModule to X. Then Node makes the following judgments when loading

X is loaded directly if it is a file, otherwise it is loaded in the following format

  1. X.js
  2. X.json
  3. X.node
  4. X/index.js
  5. X/index.json
  6. X/index.node

If it is a package module, the above extension will be performed in the package.json file with the value of the main property

In practice, that’s all you need to know.

It is better to look at the document by yourself.

The cache

function require(moduleName) {
  // ...
  if (require.cache[id]) {
    return require.cache[id].exports;
  }

  const module = {
    exports: {},
    id,
  };
  // ...
  // load the module
  loadModule(id, module.require);
  // ...
  return module.exports;
}
// ...

Copy the code

From our implementation of require, we can see that once the module has been loaded, when it loads again, we only return the data returned after the first execution. The result is that the module will only be executed once.

// a.js 
console.log(` = = = = = = = = = = = = = = > 111 `)
console.log(111)

Copy the code
// main.js
require("./a");
require("./a");
require("./a");
require("./a");
require("./a");

Copy the code

Eventually the console will only play once

= = = = = = = = = = = = = = > 111, 111Copy the code

A circular reference

Cycles

Here’s an example of a circular reference:

a.js:

console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
Copy the code

b.js:

console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
Copy the code

main.js:

console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
console.log('main ending');
Copy the code

Printed results:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
Copy the code

Title: test
Note right of main.js: console.log('main starting')
main.js->a.js: require('./a.js')
Note right of a.js: console.log('a starting');
Note right of a.js: exports.done = false;
a.js -> b.js:  require('./b.js')
Note right of b.js: console.log('b starting');
Note right of b.js: exports.done = false;
b.js -> a.js: require('./a.js')
a.js->b.js:,
Note right of b.js:exports.done = true;
Note right of b.js:console.log('b done');
b.js->a.js:,
Note right of a.js: exports.done = true;
Note right of a.js: console.log('a done');
a.js->main.js:,
main.js->b.js:require('./b.js');
b.js->main.js:,
Note right of main.js:console.log('main ending');


Copy the code

When main.js loads a.js, then a.js in turn loads b.js. At that point, b.js tries to load a.js. In order to prevent an infinite loop, an unfinished copy of the a.js exports object is returned to the b.js module. b.js then finishes loading, and its exports object is provided to the a.js module.

From the documentation and flowcharts, we know that when a circular reference occurs, the code that was not fully executed is returned, and the module.exports object created when it was first loaded is returned, resulting in the imported module code being incomplete. When the loading position of this module is moved, the result is a lot of uncertainty. So we should avoid this kind of circular reference in real projects, such as

  • A is dependent on B
  • B depends on A

So that’s the case where there’s duplicate code in A and B, and we should extract that code and put it in C, and there’s

  • A depends on C
  • B depends on C