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