An overview of the

What’s the use?

I recently saw the Promise implementation of the “beggar edition”, so I want to implement a “beggar edition” of the CommonJS specification for module loading. It is hoped that:

  • Thoroughly understandCommonJSSpecification;
  • For other environments (e.gQuickJs) provides module loading function for reusenpmModule package on;
  • Add an interview question (manual black question mark face) to other front-end interviewers;

specification

CommonJS specification I believe everyone is familiar with, node.js is just because of the implementation of CommonJS specification, it has the ability to load modules, and on this basis, the emergence of a vigorous ecology. In short:

  1. Each file is a module with its own scope. Module byexportsormodule.exportsExpose methods, properties, or objects.
  2. Modules can be passed by other modulesrequireReferences, if the same module is referenced multiple times, caching is used instead of reloading.
  3. Modules are loaded on demand, unused modules are not loaded.

If you are not familiar with the CommonJS specification, it is recommended to read the CommonJS Modules and Node.js Modules documentation first.

The core to realize

Initialize the module

First, we initialize a custom Module object that wraps the file’s corresponding Module.

class Module {
    constructor(id) {
        this.id = id;
        this.filename = id;
        this.loaded = false;
        this.exports = {};
        this.children = [];
        this.parent = null;

        this.require = makeRequire.call(this); }}Copy the code

This. exports and this.require are used to support module loading.

This. exports holds the module object that was parsed out of the file (you could say that it was module.exports that you defined in the module file). It is empty when initialized, which also explains why the value of the target module attribute is not retrieved at compile time in circular require cases. Here’s a little chestnut:

// a.js
const b = require('./b');

exports.value = 'a';

console.log(b.value);   // a.value: undefined
console.log(b.valueFn()); // a.value: a
Copy the code
// b.js
const a = require('./a');

exports.value = `a.value: ${a.value}`;  // compile phase, a.value === undefined

exports.valueFn = function valueFn() {
    return `a.value: ${a.value}`;       // Run phase, a.value === a
};
Copy the code

This.require is the method used to load modules (the require you use in your module code), which allows you to load other submodules that the module depends on.

To achieve the require

So let’s look at the implementation of require.

We know that when we require a module using a relative path, it is relative to the __dirname of the current module, which is why we need to define a separate require method for each module.

const cache = {};

function makeRequire() {
    const context = this;
    const dirname = path.dirname(context.filename);
    
    function resolve(request) {}

    function require(id) {
        const filename = resolve(id);

        let module;
        if (cache[filename]) {
            module = cache[filename];
            if(! ~context.children.indexOf(module)) {
                context.children.push(module); }}else {
            module = new Module(filename);
            (module.parent = context).children.push(module);
            (cache[filename] = module).compile();
        }

        return module.exports;
    }

    require.cache = cache;
    require.resolve = resolve;

    return require;
}
Copy the code

Note the order of execution here:

  1. Start with a global cachecacheDoes the target module already exist? The search is based on the full path to the module file.
  2. If not, instantiate a new one using the full path to the module fileModuleObject while pushing into the parent modulechildrenIn the.
  3. Will be created in step 2moduleObjects incacheIn the.
  4. Call the one created in step 2moduleThe object’scompileMethod, at which point the module code is actually parsed and executed.
  5. returnmodule.exportsThat is, the methods, properties, or objects we expose in the module.

The order of 3 and 4 is important, as if the two steps were reversed, they would result in circular require.

File path resolution

There is a require.resolve method in the above code that resolves the module’s full file path. This approach has helped us find thousands of modules without having to write the entire path each time.

In the official Node.js documentation, this lookup process is described in complete pseudocode:

require(X) from module at path Y
1. If X is a core module,
   a. return the core module
   b. STOP
2. If X begins with '/'
   a. set Y to be the filesystem root
3. If X begins with '/' or '/' or '.. / '
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
   c. THROW "not found"
4. LOAD_NODE_MODULES(X, dirname(Y))
5. THROW "not found"
Copy the code

Corresponding implementation code:

const coreModules = { os, }; // and other core modules
const extensions = [' '.'.js'.'.json'.'.node'];
const NODE_MODULES = 'node_modules';
const REGEXP_RELATIVE = / ^ \. {0, 2} \ / /;

function resolve(request) {
    if (coreModules[request]) {
        return request;
    }

    let filename;
    if (REGEXP_RELATIVE.test(request)) {
        let absPath = path.resolve(dirname, request);
        filename = loadAsFile(absPath) || loadAsDirectory(absPath);
    } else {
        filename = loadNodeModules(request, dirname);
    }

    if(! filename) {throw new Error(`Can not find module '${request}'`);
    }

    return filename;
}
Copy the code

If you’re interested in how to look up from a directory, file, or node_modules, see the complete code below. These procedures are also implemented according to pseudocode in the official Node.js documentation.

Compile the module

Finally, we need to compile the code in the file into code that is actually executable in the JS environment.

function compile() {
    const __filename = this.filename;
    const __dirname = path.dirname(__filename);

    let code = fs.readFile(__filename);
    if (path.extname(__filename).toLowerCase() === '.json') {
        code = 'module.exports=' + code;
    }
    const wrapper = new Function('exports'.'require'.'module'.'__filename'.'__dirname', code);
    wrapper.call(this.this.exports, this.require, this, __filename, __dirname);

    this.loaded = true;
}
Copy the code

In the compile method, we mainly do:

  1. Read code text content using file IO.
  2. To providejsonFormat file support.
  3. usenew FunctionGenerate a method.
  4. willmodule,module.exports,require,__dirname,__filenameAs an argument, execute the method
  5. willloadedMarked astrue.

The complete code

This is a complete implementation of a CommonJS module loader that runs on top of the QuickJs engine. The QuickJs engine implements ES6 module loading, but does not provide CommonJS module loading.

Of course, if you do encounter this problem during the interview, it is recommended to use the node.js source code implementation version.