[Turn] Modularization - Common Specification and Node Module Implementation

Node is not completely implemented in accordance with the CommonJS specification, but makes certain trade-offs for the module specification, and also adds a few features that it needs. This article will introduce the module implementation of NodeJS in detail

introduce

  Nodejs is different from javascript, the top-level object in javascript is window, and the top-level object in node is global

  [Note] In fact, there is also a global object in javascript, but it does not access externally, but uses the window object to point to the global object.

  In javascript, through var a = 100; you can get 100 through window.a

  But in nodejs, it cannot be accessed through global.a, and the result is undefined

  This is because var a = 100; the variable a in this statement is just the variable a in the module scope, not the a under the global object

  In nodejs, a file is a module, and each module has its own scope. A variable declared with var is not global, but belongs to the current module

  If you want to declare the variable under the global scope like this

 

Overview

  Modules in Node are divided into two categories: one is the module provided by Node, called the core module ; the other is the module written by the user, called the file module

  The core module part is compiled into the binary executable file during the compilation process of the Node source code. When the Node process starts, some core modules are directly loaded into the memory, so when this part of the core module is introduced, the two steps of file location and compilation and execution can be omitted, and priority is given in the path analysis, so its loading speed is the fastest

  The file module is dynamically loaded at runtime, requiring complete path analysis, file location, compilation and execution process, and the speed is slower than the core module

  Next, we expand the detailed module loading process

 

module loading

  In javascript, the script tag can be used to load modules, but in nodejs, how to load another module in one module?

  Use the require() method to import

【Cache Loading】

  Before introducing the identifier analysis of the require() method, you need to know that, just as the front-end browser caches static script files to improve performance, Node caches imported modules to reduce the overhead of secondary introduction. The difference is that browsers only cache files , while Node caches objects after compilation and execution

  Regardless of whether it is a core module or a file module, the require() method will always use the cache-first method for the secondary loading of the same module , which is the first priority . The difference is that the cache check of the core module precedes the cache check of the file module

【Identifier Analysis】

  The require() method accepts an identifier as a parameter. In the Node implementation, it is based on such an identifier that module lookups are performed. Module identifiers are mainly divided into the following categories in Node: [1] Core modules, such as http, fs, path, etc.; [2] Relative path file modules starting with . or ..; [3] Absolute paths starting with / File module; [4] Non-path file module, such as custom connect module

  According to the different formats of the parameters, the requirecommand goes to different paths to find the module file

  1. If the parameter string starts with "/", it means that a module file located in an absolute path is loaded. For example, require('/home/marco/foo.js')will load/home/marco/foo.js

  2、If the parameter string starts with "./", it means that a module file is loaded at a relative path (compared to the location of the currently executing script). For example, require('./circle')will load the current script in the same directorycircle.js

  3、如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)

  [注意]如果是当前路径下的文件模块,一定要以./开头,否则nodejs会试图去加载核心模块,或node_modules内的模块 

复制代码
//a.js
console.log('aaa');

//b.js
require('./a');//'aaa'
require('a');//报错
复制代码

【文件扩展名分析】

  require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node会先查找是否存在没有后缀的该文件,如果没有,再按 .js、.json、.node的次序补足扩展名,依次尝试

  在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node和.json文件,在传递给require()的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解Node单线程中阻塞式调用的缺陷

【目录分析和包】

  在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时Node会将目录当做一个包来处理

  在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤

  而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node

  如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常

 

访问变量

  如何在一个模块中访问另外一个模块中定义的变量呢? 

【global】

  最容易想到的方法,把一个模块定义的变量复制到全局环境global中,然后另一个模块访问全局环境即可

复制代码
//a.js
var a = 100;
global.a = a;

//b.js
require('./a');
console.log(global.a);//100
复制代码

  这种方法虽然简单,但由于会污染全局环境,不推荐使用

【module】

  而常用的方法是使用nodejs提供的模块对象Module,该对象保存了当前模块相关的一些信息

复制代码
function Module(id, parent) {
    this.id = id;
    this.exports = {};
    this.parent = parent;
    if (parent && parent.children) {
        parent.children.push(this);
    }
    this.filename = null;
    this.loaded = false;
    this.children = [];
}
复制代码
复制代码
module.id 模块的识别符,通常是带有绝对路径的模块文件名。
module.filename 模块的文件名,带有绝对路径。
module.loaded 返回一个布尔值,表示模块是否已经完成加载。
module.parent 返回一个对象,表示调用该模块的模块。
module.children 返回一个数组,表示该模块要用到的其他模块。
module.exports 表示模块对外输出的值。
复制代码

【exports】

  module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量

复制代码
//a.js
var a = 100;
module.exports.a = a;

//b.js
var result = require('./a');
console.log(result);//'{ a: 100 }'
复制代码

  为了方便,Node为每个模块提供一个exports变量,指向module.exports。造成的结果是,在对外输出模块接口时,可以向exports对象添加方法

console.log(module.exports === exports);//true

  [注意]不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系

 

模块编译

  编译和执行是模块实现的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同,具体如下所示

  js文件——通过fs模块同步读取文件后编译执行

  node文件——这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件

  json文件——通过fs模块同步读取文件后,用JSON.parse()解析返回结果

  其余扩展名文件——它们都被当做.js文件载入

  每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能

  根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件的调用如下:

复制代码
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
    var content = NativeModule.require('fs').readFileSync(filename, 'utf8'); 
    try {
        module.exports = JSON.parse(stripBOM(content));
    } catch (err) {
        err.message = filename + ': ' + err.message;
        throw err;
    }
};
复制代码

  其中,Module._extensions会被赋值给require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。编写如下代码测试一下:

console.log(require.extensions);

  得到的执行结果如下:

{ '.js': [Function], '.json': [Function], '.node': [Function] }

  在确定文件的扩展名之后,Node将调用具体的编译方式来将文件执行后返回给调用者

【JavaScript模块的编译】

  回到CommonJS模块规范,我们知道每个模块文件中存在着require、exports、module这3个变量,但是它们在模块文件中并没有定义,那么从何而来呢?甚至在Node的API文档中,我们知道每个模块中还有filename、dirname这两个变量的存在,它们又是从何而来的呢?如果我们把直接定义模块的过程放诸在浏览器端,会存在污染全局变量的情况

  事实上,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了(function(exports, require, module, filename, dirname) {\n,在尾部添加了\n});

  一个正常的JavaScript文件会被包装成如下的样子

复制代码
(function (exports, require, module,  filename,  dirname) {
    var math = require('math');
    exports.area = function (radius) {
        return Math.PI * radius * radius;
    };
});
复制代码

  这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行

  这就是这些变量并没有定义在每个模块文件中却存在的原因。在执行之后,模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用

  至此,require、exports、module的流程已经完整,这就是Node对CommonJS模块规范的实现

【C/C++模块的编译】

  Node调用 process.dlopen()方法 进行加载和执行。在Node的架构下,dlopen()方法在Windows和*nix平台下分别有不同的实现,通过libuv兼容层进行了封装

  实际上,.node的模块文件并不需要编译,因为它是编写C/C++模块之后编译生成的,所以这里只有加载和执行的过程。在执行的过程中,模块的exports对象与.node模块产生联系,然后返回给调用者

  C/C++模块给Node使用者带来的优势主要是执行效率方面的,劣势则是C/C++模块的编写门槛比JavaScript高

【JSON文件的编译】

  .json文件的编译是3种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用

  JSON文件在用作项目的配置文件时比较有用。如果你定义了一个JSON文件作为配置,那就不必调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响

 

CommonJS

  在介绍完Node的模块实现之后,回过头来再学习下CommonJS规范,相对容易理解

  CommonJS规范的提出,主要是为了弥补当前javascript没有标准的缺陷,使其具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段

  CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分

【模块引用】

var math = require('math');

  在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中

【模块定义】

  在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:

复制代码
// math.js
exports.add = function () {
    var sum = 0, i = 0,args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};
复制代码

  在另一个文件中,我们通过require()方法引入模块后,就能调用定义的属性或方法了

// program.js
var math = require('math');
exports.increment = function (val) {
    return math.add(val, 1);
};

【模块标识】

  模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js

  模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324866675&siteId=291194637