The Principle of Nodejs Modularization

I. Preface

The application of node is composed of modules. Node follows the module specification of commonjs, which is used to isolate the scope of each module and make each module execute in its own namespace.

The main contents of commonjs are as follows:

Modules must export external variables or interfaces through module.exports, and import the output of other modules into the scope of the current module through require().

commonjs module features:

1. All codes run in the current module scope and do not pollute the global scope.

2. Modules are loaded synchronously, according to the sequence of code.

3. Modules can be loaded many times, only run once at the first load, and then run the results will be cached. After loading, the results will be read directly from the cache. If you want the module to run again, you must clear the cache.

Let's start with a simple example.

Write a demo-exports.js

let name = 'saucxs';
let getName = function (name) {
    console.log(name)
};

module.exports = {
    name: name,
    getName: getName
}

Let's write another demo-require.js

let person = require('./demo-export')

console.log(person, '-------------')       // { name: 'saucxs', getName: [Function: getName] } 

console.log(person.name, '===========')   // saucxs
person.getName('gmw');                    // gmw

person.name = 'updateName'
console.log(person, '22222222')           //  { name: 'updateName', getName: [Function: getName] }

console.log(person.name, '3333333')       // updateName
person.getName('gmw')                     // gmw

node demp-require.js, as shown above.

module object

commonjs specification, each file is a module, each module has a module object, which points to the current module. Module objects have the following properties:

(1) id: current module ID.

(2) exports: Indicates the value that the current module exposes to the outside world.

(3) parent: An object that represents a module that calls the current module.

(4) children: An object that represents the module invoked by the current module.

(5) filename: the absolute path of the module

(6) paths: Look up the node_modules directory from the current module, then go to the parent directory in turn, look up the node_modules directory under the parent directory, and iterate until the node_modules directory under the root directory.

(7) loaded: A Boolean value indicating whether the current module is fully loaded.

Let's have a look at chestnuts.

module.js

module.exports = {
    name: 'saucxs',
    getName: function (name) {
        console.log(name)
    }
}

console.log(module)

node module.js

1,module.exports

We know that the module object has an exports attribute, which is used to expose variables, methods, or entire modules. When other files need require for this module, what they actually read is the exports attribute in the module object.

2. exports object

Now that you have module.exports to meet all the requirements, why do you have an exports object?

Now let's look at the relationship between the two.

(1) Exports object and module.exports are both reference type variables, pointing to the same memory address. In node, both of them are initially pointing to an empty object.

exports = module.exports = {}

(2) Secondly, exports objects are imported by means of parameters, which are directly assigned to the reference of parameters, but can not change the values outside the scope.

var module = {
    exports: {}
};

var exports = module.exports;

function change(exports) {
    /*Adding the attribute name to the parameter exports synchronizes with the external module.exports object*/
    exports.name = 'saucxs'
    /*Modifying the reference to wxports here does not affect module exports.*/
    exports = {
        age: 18
    }
    console.log(exports)  // {age: 18}
}

change(exports);
console.log(module.exports);   // {exports: {name: 'saucxs'}}

Analyse the above code:

Appointing exports directly will change the object application of exports in the current module. It shows that the current exports object has nothing to do with the external module.exports object, so changing the exports object will not affect module.exports.

Note: module.exports is designed to solve the problem of direct assignment of exports mentioned above, which will lead to the throwing of unsuccessful problems.

//These operations are legal.
exports.name = 'saucxs';
exports.getName = function(){
    console.log('saucxs')
};


//Equivalent to the following way
module.exports = {
    name: 'saucxs',
    getName: function(){
        console.log('saucxs')
    }
}

//Or more conventional writing
let name = 'saucxs';
let getName = function(){
    console.log('saucxs')
}
module.exports = {
    name: name,
    getName: getName
}

In this way, we can not assign the object or method to the exports attribute directly every time. It is more convenient to use the literal quantity of the object directly.

require METHOD

Requirement is the rule of module introduction. A module is thrown by exports or module.exports, module identifier is passed in by require method, and node is introduced into the module according to certain rules. We can use the methods and attributes defined in the module.

(1) The mechanism of introducing modules into node

1. Introducing modules into node requires three steps:

(1) Path analysis

(2) File Location

(3) Compilation and execution

2. In node, there are two kinds of modules:

(1) The modules provided by node, such as http module and fs module, are called core modules. The core module compiles binary files in the process of node source code compilation. When the node process starts, some core modules are directly loaded into memory. Therefore, this part of the module does not need to go through the above steps (2), (3) and gives priority to judgment in path analysis, so the loading speed is the fastest.

(2) The module written by users themselves is called file module. File module needs to be loaded on demand, and needs to go through the three steps mentioned above, which is slow.

3. Prefer loading from cache

Just as browsers cache static script files to improve page performance, Node caches introduced modules. Unlike browsers, Node caches objects after compilation and execution rather than static files. Let's take an example.

requireA.js

console.log('Modular requireA Start loading...')
exports = function() {
    console.log('Hi')
}
console.log('Modular requireA Loaded')
init.js

var mod1 = require('./requireA')
var mod2 = require('./requireA')
console.log(mod1 === mod2)

Execute node init.js

Although requireA was introduced twice, the code in the module was actually executed only once. And mod1 and mod2 point to the same module.

4. module._load source code

Module._load = function(request, parent, isMain) {

  //  Computing absolute paths
  var filename = Module._resolveFilename(request, parent);

  //  Step 1: If there is a cache, take out the cache
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // Step 2: Is it a built-in module?
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // Step 3: Generate module instances and store them in the cache
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // Step 4: Loading modules
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // Step 5: exports properties of the output module
  return module.exports;
};

The corresponding process is as follows:

(2) Path analysis and document location

1. Path Analysis

Module identifier analysis:

(1) Core modules, such as http and fs modules.

(2) Relative path file module starting with. or.. /

(3) Absolute path module starting with/

(4) Non-path file module.

Analysis:

(1) Core module: Priority is second only to cache, and loading speed is the fastest. If the custom module has the same name as the core module, the load will fail. If you want to succeed, you must change the name of the custom module or change the path.

(2) File modules in the form of paths: identifiers starting with. or. / are treated as file modules. In the loading process, require method converts the path into a real path, and the loading speed is second only to the core module.

(3) Custom module in non-path form: This is a special file module, which may be in the form of a file or a package. The strategy of finding such modules is similar to the js scope chain, and node tries the path in the module path one by one until the target file is found.

Note: This is the search strategy when node locates the specific files of the file module, which is embodied in an array of paths.

You can output Module objects in the REPL environment and view the above arrays by viewing their path attributes. The paths arrays at the beginning of this article are as follows:

2. File Location

(1) Analysis of File Expansion Names

The identifier for require() analysis can contain no extensions, and the node will complement the extensions in the order of. js,. Node,. json, and try them in turn.

(2) Target analysis and package

If in the step of extension analysis, the file can not be found but the corresponding directory can be found, then node will treat the directory as a package. Next step analysis will be carried out to find the file name specified by the main attribute of package.json in the current directory. If the search is unsuccessful, index.js,index.node,index.json will be found in turn.

If no files are located in the process of directory analysis, the custom module will go to the next module path and continue searching until all module paths have been traversed and the exception that failed to find is thrown.

(3) Reference source code

The method Module._findPath is called inside the Module._load method, which is used to return the absolute path of the module. The source code is as follows:

Module._findPath = function(request, paths) {

  // List all possible suffix names:.js,.json,.node
  var exts = Object.keys(Module._extensions);

  // If it's an absolute path, it's not searched.
  if (request.charAt(0) === '/') {
    paths = [''];
  }

  // Catalog slashes with suffixes
  var trailingSlash = (request.slice(-1) === '/');

  // Step 1: If the current path is already in the cache, return it directly to the cache
  var cacheKey = JSON.stringify({request: request, paths: paths});
  if (Module._pathCache[cacheKey]) {
    return Module._pathCache[cacheKey];
  }

  // Step 2: Travel through all paths in turn
  for (var i = 0, PL = paths.length; i < PL; i++) {
    var basePath = path.resolve(paths[i], request);
    var filename;

    if (!trailingSlash) {
      // Step 3: Does the module file exist?
      filename = tryFile(basePath);

      if (!filename && !trailingSlash) {
        // Step 4: Does the module file exist with a suffix name?
        filename = tryExtensions(basePath, exts);
      }
    }

    // Step 5: Is there a package.json in the directory? 
    if (!filename) {
      filename = tryPackage(basePath, exts);
    }

    if (!filename) {
      // Step 6: Is there a directory name + index + suffix name? 
      filename = tryExtensions(path.resolve(basePath, 'index'), exts);
    }

    // Step 7: Save the found file path into the return cache, and then return
    if (filename) {
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
 }
    
  // Step 8: No file found, return false 
  return false;
};

(3) Clear the cache

According to the above module introduction mechanism, we know that when we first introduce a module, require's cache mechanism will add the modules we introduce into memory to improve the performance of secondary loading. However, if we modify the code of the introduced module, when we re-introduce the module, we will find that it is not our latest code, which is a troublesome thing. How to solve it?

require(): Load external modules

require.resolve(): Resolve the module name to an absolute path

Request. main: Point to the main module

require.cache: Modules that point to all caches

Requirements. extensions: Call different execution functions based on the suffix name of the file

Solution:

//Delete the cache for the specified module
delete require.cache[require.resolve('/*Cached module name*/')]

// Delete caches for all modules
Object.keys(require.cache).forEach(function(key) {
     delete require.cache[key];
})

Then we can require in the required module again.

Keywords: node.js JSON Attribute

Added by jonshutt on Wed, 24 Jul 2019 07:10:27 +0300