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 understand
CommonJS
Specification; - For other environments (e.gQuickJs) provides module loading function for reuse
npm
Module 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:
- Each file is a module with its own scope. Module by
exports
ormodule.exports
Expose methods, properties, or objects. - Modules can be passed by other modules
require
References, if the same module is referenced multiple times, caching is used instead of reloading. - 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:
- Start with a global cache
cache
Does the target module already exist? The search is based on the full path to the module file. - If not, instantiate a new one using the full path to the module file
Module
Object while pushing into the parent modulechildren
In the. - Will be created in step 2
module
Objects incache
In the. - Call the one created in step 2
module
The object’scompile
Method, at which point the module code is actually parsed and executed. - return
module.exports
That 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:
- Read code text content using file IO.
- To provide
json
Format file support. - use
new Function
Generate a method. - will
module
,module.exports
,require
,__dirname
,__filename
As an argument, execute the method - will
loaded
Marked 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.