preface

What is CommonJS?

Node.js applications adopt the CommonJS module specification.

Each file is a module, with its own independent scope, variables, methods, etc., invisible to other modules. The CommonJS specification states that within each module, the module variable represents the current module. This variable is an object whose exports property (module.exports) is the interface to the outside world. Loading a module loads the module.exports property of that module. The require method is used to load modules.

CommonJS module features:

All code runs in the module scope and does not pollute the global scope.

Modules can be loaded multiple times, but only run once on the first load, and then the results are cached and read directly from the cache when they are loaded later. For the module to run again, the cache must be cleared.

The order in which modules are loaded, in the order they appear in the code.

How to use it?

Suppose we now have an A.js file and we want to use some of the methods and variables of a.js in main.js, running on nodejs. Then we can use the CommonJS specification to have a file export methods/variables. Then use the require function to introduce variables/functions.

Example:

// exports = 'exports '; // exports =' exports '; // main.js let STR = require('./a'); // If a.js is imported, it will automatically add console.log(STR) to you in the preset order; // Output: 'This is the variable of a.js'Copy the code

Write a require function by hand

preface

Let’s start writing a simplified version of the require function that supports the following functions:

  1. Import a JS file that complies with the CommonJS specification.
  2. Support for automatic file suffixes (JS and JSON files temporarily supported)

Start now!

Define a REQ method

Let’s start with a custom req method, separate from the global require function.

The req method takes a parameter named ID, which is the path to the file to load.

// main.js

function req(id){}

let a = req('./a')
console.log(a)
Copy the code

2. Create a Module class

Create a new module class that will handle the file loading process.

function Module(id) { this.id = id; This.exports = {} // this. Exports = {} // this. Exports = {} // this.Copy the code

3. Obtain the absolute file path

As we mentioned earlier, the require function supports passing in a path. The path can be a relative path or an absolute path, or the file name extension can be omitted.

We add a method called “_resolveFilename” to the Module class to parse the file path passed in by the user and get an absolute path.

Module._resolveFilename = function (id) {}Copy the code

Continue adding a “Extennsions” property, which is an object. Key is the file extension, and value is the processing method of different files corresponding to the extension.

The native require function supports four types of files:

  1. Js file
  2. Json file
  3. The node file
  4. MJS file

Due to space, we only support two extensions here:.js and.json.

We add two properties, each with a function value, to the Extensions object. It is convenient to classify different file types.

// main.js 
Module.extensions['.js'] = function (module) {}
Module.extensions['.json'] = function (module) {}
Copy the code

Next, we import nodeJS ‘native “path” and “fs” modules to get absolute file paths and file operations.

Let’s handle the module. _resolveFilename method to make it work.

Module._resolveFilename = function (id) {// Convert relative path to absolute path let absPath = path.resolve(id); If (fs.existssync (absPath)){return absPath; if(fs.existssync (absPath)){return absPath; Json let extenisons = object.keys (module.extensions); for (let i = 0; i < extenisons.length; i++) { let ext = extenisons[i]; Let currentPath = absPath + ext; Let exits = fs.existssync (currentPath); If (exits){return currentPath}} throw new Error(' file does not exist ')}Copy the code

In this case, we support taking a parameter named ID, which will be the path from the user.

First we use path.resolve() to get the absolute path to the file. Fs.existssync is then used to determine whether the file exists. If none exists, we try to add a file suffix.

We’ll go through the currently supported file extension objects and try to concatenate paths. If the spliced file exists, return the file path. No exception was thrown.

So we can get the full file path in the req method:

Function req(id){// Get the absolute path from the relative path let filename = module._resolvefilename (id); }Copy the code

4. Load the module — JS implementation

This is where we start, loading the common.js module.

Start by creating an instance of Module. Pass in a file path and return a new module instance.

We then define a tryModuleLoad function, passing in our newly created module instance.

Function tryModuleLoad(module) {// let ext = path.extName (module.id); Module.extensions[ext](Module)} function req(id){// Obtain absolute path from relative path let filename = module._resolvefilename (id); let module = new Module(filename); // new a new module tryModuleLoad(module); }Copy the code

The **tryModuleLoad function ** gets the Module and uses the path.extname function to get the file extension, which is then handed to different functions for processing.

Next, we deal with the JS file loading.

The first step is to pass in an instance of the Module object.

Get the absolute path to the file using the ID attribute in the Module object. After getting the absolute path to the file, use the FS module to read the file contents. The read code is UTF8.

Module. Extensions ['.js'] = function (Module) {// 1) Read let script = fs.readfilesync (module.id, 'utf8'); }Copy the code

Second, forge a self-executing function.

Let’s start with a new Wrapper array. The 0th item in the array is the beginning of the self-executing function and the last item is the end.

let wrapper = [
    '(function (exports, require, module, __dirname, __filename) {\r\n',
    '\r\n})'
];
Copy the code

This self-executing function takes 5 arguments: exports object, require function, Module object, dirName path, fileame file name.

We spliced the content of the file to be loaded with the self-executing function template to assemble a complete executable JS text:

Module. Extensions ['.js'] = function (Module) {// 1) Read let script = fs.readfilesync (module.id, 'utf8'); // 2) let content = wrapper[0] + script + wrapper[1]; }Copy the code

Step 3: Create a sandbox execution environment

Here we use the “VM” module in NodeJS. This module creates a NodeJS virtual machine and provides a separate sandbox environment.

For details, see the official introduction of vm module

We use the VM module’s runInThisContext function, which creates a sandbox with a global attribute. The usage is to pass in a JS text content. We pass in the text we just concatenated, returning a fn function:

const vm = require('vm'); Module. Extensions ['.js'] = function (Module) {// 1) Read let script = fs.readfilesync (module.id, 'utf8'); // 2) let content = wrapper[0] + script + wrapper[1]; // 3) Create sandbox environment, return js function let fn = vm.runInThisContext(content); }Copy the code

Step 4: Execute the sandbox environment to obtain the exported object.

Because we need the file directory path above, so we first get the directory path. The dirname method of the path module is used.

We then use the call method, passing in the argument and executing it immediately.

The first argument to the call method is the this object inside the function. The rest of the arguments are required by the function.

Module. Extensions ['.js'] = function (Module) {// 1) Read let script = fs.readfilesync (module.id, 'utf8'); // 2) let content = wrapper[0] + script + wrapper[1]; // 3) execute the string function (node API) let fn = vm.runInThisContext(content); // Let __dirname = path.dirname(module.id); // Let __dirname = path.dirname(module.id); Call (module.exports, module.exports, req, module, __dirname, module.id)}Copy the code

In this way, we pass in the Module object, and then internally hang the exported value to the Module’s export property.

Step 5: Return the exported value

Since our handler is a non-pure function, it is ok to return the Export object of the Module instance directly.

Function req(id){// There is no asynchronous API method // Get absolute path from relative path let filename = module._resolvefilename (id); tryModuleLoad(module); // module.exports = {} return module.exports; }Copy the code

Thus, we implement a simple require function.

let str = req('./a'); // str = req('./a'); console.log(str); Exports = "exports" // exports = "exports"Copy the code

5. Load the module — JSON file implementation

Json files are simpler to implement. Parse the json file using FS and convert it to A JS object using Json.parse.

Module.extensions['.json'] = function (module) {
    let script = fs.readFileSync(module.id, 'utf8');
    module.exports = JSON.parse(script)
}
Copy the code

6. Optimize

Commonjs will cache the modules we want to load. When we read again, we read our module from the cache instead of calling fs and VM modules again to get the exported content.

We create a new _cache attribute on the Module object. This property is an object where key is the file name and value is the content cache of the exported file.

When we load a module, we first check the _cache property to see if it has been cached. If so, return the cached contents directly. If not, try to get the exported content and hang it on the cache object.

Module._cache = {} function req(id){// Obtain absolute path from relative path let filename = module. _resolveFilename(id); let cache = Module._cache[filename]; Return cache.exports} let module = new module (filename); Module._cache[filename] = Module // Input into the cache object // load the relevant Module (that is, assign the Module's exports value) tryModuleLoad(Module); // module.exports = {} return module.exports; }Copy the code

Complete implementation

const path = require('path'); const fs = require('fs'); const vm = require('vm'); function Module(id) { this.id = id; // The id of the current module is this.exports = {}; } module.extensions = {}; Module.js] = function (Module) {// 1) Read let script = fs.readfilesync (module.id,  'utf8'); // 2) let content = wrapper[0] + script + wrapper[1]; // 3) execute the string function (node API) let fn = vm.runInThisContext(content); // Let __dirname = path.dirname(module.id); // Let __dirname = path.dirname(module.id); Module. exports, module.exports, req, module, __dirname, Module.id)} // If the file is json module.extensions ['.json'] = function (module) {let script = fs.readfilesync (module.id, 'utf8'); Module.exports = json.parse (script)} module.exports = json.parse (script)} module.exports = resolvefilename = function (id) {module.exports = json.parse (script) absPath = path.resolve(id); If (fs.existssync (absPath)){return absPath; Json let extenisons = object.keys (module.extensions); for (let i = 0; i < extenisons.length; i++) { let ext = extenisons[i]; Let currentPath = absPath + ext; Let exits = fs.existssync (currentPath); If (exits){return currentPath}} throw new Error(' file does not exist ')} let wrapper = ['(function (exports, require, exit) module, __dirname, __filename) {\r\n', '\r\n})' ]; Function tryModuleLoad(module) {let ext = path.extName (module.id); Module.extensions[ext](Module)} module. _cache = {} function req(id){// There is no asynchronous API method // Get absolute path from relative path let filename = Module._resolveFilename(id); let cache = Module._cache[filename]; Return cache.exports} let module = new module (filename); // create a Module module. _cache[filename] = Module; TryModuleLoad (module); // Load the relevant module (e.g. exports of this module); // module.exports = {} return module.exports; } let str = req('./a'); console.log(str);Copy the code

End summary

Thus, we implement a simplified version of the CommonJS Require function by hand.

Let’s review the implementation flow of require:

  1. Get the absolute path to the file to load. Try adding a suffix if you don’t have one
  2. Try to read the export from the cache. If the cache has one, return the cache contents. No, next step
  3. Create a new module instance and enter the cache object
  4. Attempting to load a module
  5. According to the file type, classification processing
  6. If it is a JS file, read the file content, splice the self-executing function text, use the VM module to create a sandbox instance to load the function text, obtain the exported content, and return the content
  7. If it is a JSON file, read the contents of the file, use the json. parse function to convert it into a JS object, and return the contents
  8. Gets the export return value.

Thank you readers for your support. Welcome to like comment forwarding collection