As a front-end developer, you have to deal with Node.js on a daily basis. Node follows the Commonjs specification, the core of which is to load dependent modules via require. We are often used to using various libraries provided by the community, but we know very little about the philosophy behind module references. This article through the source code reading, analysis of the commonJS specification in the work principle behind require.
Where does require come from?
It is well known that some “global” variables can be used directly in node JS modules/files, such as require, module, __dirname, __filename, exports.
These variables or methods are not “global”, but local variables that are provided as a wrapper in the CommonJS module load.
module.exports = function () {
console.log(__dirname);
}
Copy the code
After compile, you have variables such as module, __dirname and so on that can be used directly.
(function (exports, require, module, __filename, __dirname) {
module.exports = function () {
console.log(__dirname); }})Copy the code
Exports = exports; require = undefined; exports = exports; require = undefined;
// Assigning directly to exports does not work
(function (exports, module) {
exports = function () {
}
})(m.exports, m)
return m.exports;
Copy the code
Direct assignment only modifies the value of partial face-exports. Module. exports is not assigned.
Require’s lookup process
As the documentation clearly describes, the search process for the simplified require module is as follows:
In the Y path, require(X) 1. If X is a built-in module (HTTP, fs, path, etc.), return to the built-in module directly without performing 2. If X'/'Set Y to the root directory of the file system. 3'/'.'/'.'.. / 'Start a. Load the file as a file (Y + X) and try to load the file as the extensions [X, x.js, x.son, x.ode]. If the file exists, return the file without further execution. B. Load the file as a folder (Y + X). If the file exists, return the file and do not continue executing the file. Try to parse package.json main field b. Try loading the index file in the path (index.js, index.json, index.node) 4. Search for NODE_MODULE and return module A. Start at path Y and work your way up, trying to load (path +)'node_modules/'+ X) b. GLOBAL_FOLDERS node_modules throw"Not Found" Error
Copy the code
For example, in/Users/helkyle/projects/learning module/foo js in the require (” bar “) will be from/Users/helkyle/projects/learning module/start up step by step a lookup Bar module (not with ‘./’, ‘/’, ‘.. /’ start).
'/Users/helkyle/projects/learning-module/node_modules'.'/Users/helkyle/projects/node_modules'.'/Users/helkyle/node_modules'.'/Users/node_modules'.'/node_modules'
Copy the code
Note that when using the NPM link function, require in the linked module will be looked up by the absolute path of the linked module in the file system, not the path of the main Module.
For example, suppose you have two modules.
/usr/lib/foo
/usr/lib/bar
Copy the code
/usr/lib/foo/node_modules/bar /usr/lib/bar /usr/lib/bar In this case the bar module requires (‘quux’) lookup path is /usr/lib/bar/node_modules/ instead of /usr/lib/foo/node_modules
The hole I stepped in before
The Cache mechanism
As you can see in practice, the Node Module require process actually has a cache. Require the same module twice and get the same result.
// a.js
module.exports = {
foo: 1};// b.js
const a1 = require('./a.js');
a1.foo = 2;
const a2 = require('./a.js');
console.log(a2.foo); / / 2
console.log(a1 === a2); // true
Copy the code
Execute node b.js and see that the second require A. js gets the same module reference as the first require.
From the source, require is the encapsulation of module common methods.
function makeRequireFunction(mod, redirects) {
const Module = mod.constructor;
let require;
// Simplify other code
require = function require(path) {
return mod.require(path);
};
function resolve(request, options) {
validateString(request, 'request');
return Module._resolveFilename(request, mod, false, options);
}
require.resolve = resolve;
function paths(request) {
validateString(request, 'request');
return Module._resolveLookupPaths(request, mod);
}
resolve.paths = paths;
require.main = process.mainModule;
require.extensions = Module._extensions;
require.cache = Module._cache;
return require;
}
Copy the code
Trace the code to see that require() ends up calling the module._load method:
// Ignore the code and see what happens to the load process.
Module._load = function(request, parent, isMain) {
// Call _resolveFilename to get the module's absolute path
const filename = Module._resolveFilename(request, parent, isMain);
const cachedModule = Module._cache[filename];
if(cachedModule ! = =undefined) {
// If there is a cache, return the cached exports object directly
return cachedModule.exports;
}
// The built-in module returns directly
const mod = loadNativeModule(filename, request, experimentalModules);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// Create a new Module object
const module = new Module(filename, parent);
// Main module special handling
if (isMain) {
process.mainModule = module;
module.id = '. ';
}
/ / the cache module
Module._cache[filename] = module;
// Returns the Module exports object
return module.exports;
};
Copy the code
The module cache will be cached in the module._cache property as the absolute path of the module after the first load. The module will return the cached result directly when require again to improve efficiency. Print require.cache on the console and see.
// b.js
require('./a.js');
require('./a.js');
console.log(require.cache);
Copy the code
There are two keys in the cache, which are the absolute path of a.js and B. js files in the system. Value is the module object after the corresponding module load. So the next time the require (‘/a. s’). The result is the require cache [‘/Users/helkyle/projects/learning – the module/a. s’]. Exports and the require for the first time points to is the same The Object.
{
'/Users/helkyle/projects/learning-module/b.js':
Module {
id: '. '.exports: {},
parent: null.filename: '/Users/helkyle/projects/learning-module/b.js'.loaded: false.children: [[Object]],paths:
[ '/Users/helkyle/projects/learning-module/node_modules'.'/Users/helkyle/projects/node_modules'.'/Users/helkyle/node_modules'.'/Users/node_modules'.'/node_modules']},'/Users/helkyle/projects/learning-module/a.js':
Module {
id: '/Users/helkyle/projects/learning-module/a.js'.exports: { foo: 1 },
parent:
Module {
id: '. '.exports: {},
parent: null.filename: '/Users/helkyle/projects/learning-module/b.js'.loaded: false.children: [Array].paths: [Array]},filename: '/Users/helkyle/projects/learning-module/a.js'.loaded: true.children: [].paths: [
'/Users/helkyle/projects/learning-module/node_modules'.'/Users/helkyle/projects/node_modules'.'/Users/helkyle/node_modules'.'/Users/node_modules'.'/node_modules']}}Copy the code
Apply – Implement the Mock Module effect of Jest
Jest is Facebook’s open source front-end testing library, which provides many very powerful and useful features. The Mock Module is one of the most eye-catching features. Mock (modulePath). Jest will automatically load the mock version of the Module when you run the test code.
For example, there is an apis file in the project that provides the docking back-end API.
// /projects/foo/apis.js
module.export = {
getUsers: (a)= > fetch('api/users')};Copy the code
You don’t want it to actually connect to back-end requests during running tests. At this point, mock files are created in the same directory as the apis file according to the Jest documentation
// /projects/foo/__mock__/apis.js
module.exports = {
getUsers: () => [
{
id: "1",
name: "Helkyle"
},
{
id: "2",
name: "Chinuketsu"}}]Copy the code
Mock (‘./apis. Js ‘) in the test file.
jest.mock('./apis.js');
const apis = require('./apis.js');
apis.getUsers()
.then((users) => {
console.log(users);
// [ { id: '1', name: 'Helkyle' }, { id: '2', name: 'Chinuketsu'}]})Copy the code
Now that we know the basics of require, we’ll implement a similar feature, rewriting the statement that loads api.js to load __mock__/api.js.
Using the require. The cache
Because of the caching mechanism, writing to the target cache ahead of time, require again will get the result we expect.
// Require the mock apis file ahead of time to generate the cache.
require('./__mock__/apis.js');
// Write to cache the file path to require
const originalPath = require.resolve('./apis.js');
require.cache[originalPath] = require.cache[require.resolve('./__mock__/apis.js')];
// The result will be the cached version
const apis = require('./apis.js');
apis.getUsers()
.then((users) = > {
console.log(users);
// [ { id: '1', name: 'Helkyle' }, { id: '2', name: 'Chinuketsu' } ]
})
Copy the code
The magic to change the module. _load
The mock Module needs to be required in advance based on require.cache. As mentioned at 👆, since modules are ultimately loaded through module._load, interception at this location completes the mock on demand.
const Module = require('module');
const originalLoad = Module._load;
Module._load = function (path, ... rest) {
if (path === './apis.js') {
path = './__mock__/apis.js';
}
return originalLoad.apply(Module, [path, ...rest]);
}
const apis = require('./apis.js');
apis.getUsers()
.then((users) = > {
console.log(users);
})
Copy the code
Note: The above content is for reference only. Jest has its own module loading mechanism, which differs from commonJS. For example, in Jest the require Module does not write require.cache.
Require when the program starts
A review of the Node documentation shows that there is also a –require in the Command Line section, which is used to preload a particular module before executing the business code.
For example, write a setup file to mount it, Assert, and so on to a global object.
// setup.js
global.it = async function test(title, callback) {
try {
await callback();
console.log(` ✓${title}`);
} catch (error) {
console.error(` ✕${title}`);
console.error(error);
}
}
global.assert = require('assert');
Copy the code
Add the –require parameter to the startup code. By introducing global.assert, global.it, you can use assert directly in your code, not in your test files.
node --require './setup.js' foo.test.js
Copy the code
// foo.test.js
// Don't require require('assert');
function sum (a, b) {
return a + b;
}
// no --require it is not defined
it('add two numbers', () => {
assert(sum(2.3) = = =5);
})
Copy the code
reading
- Jest – Manual Mock
- Require () source code interpretation