欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Node模块,CommonJS规范详解

程序员文章站 2022-12-09 17:53:18
在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很...

在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多语言都采用这种组织代码的方式。在node环境中,一个.js文件就称之为一个模块(module),有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。node 应用由模块组成,采用 commonjs 模块规范。1、使用模块有什么好处?最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括node内置的模块和来自第三方的模块。使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。

// example.js
var x = 5;
var addx = function (value) { 
 return value + x;
};

上面代码中,变量x和函数addx,是当前文件example.js私有的,其他文件不可见。如果想在多个文件分享变量,必须定义为global对象的属性。

global.warning = true;

2、模块的基本使用commonjs规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。'use strict';var s = 'hello';function greet(name) { console.log(s + ', ' + name + '!');}module.exports = greet;函数greet()是我们在hello模块中定义的,然后通过module.exports接口把函数greet作为模块的输出暴露出去,这样其他模块就可以使用greet函数了。require方法用于加载模块。我们再编写一个main.js文件,调用hello模块的greet函数:'use strict';// 引入hello模块:var greet = require('./hello');var s = 'michael';greet(s); // hello, michael!注意到引入hello模块用node提供的require函数:var greet = require('./hello');引入的模块作为变量保存在greet变量中,那greet变量到底是什么东西?其实变量greet就是在hello.js中我们用module.exports = greet;输出的greet函数。所以,main.js就成功地引用了hello.js模块中定义的greet()函数,接下来就可以直接使用它了。3、module对象node内部提供一个module构建函数。所有模块都是module的实例。

function module(id, parent) {  
    this.id = id; 
    this.exports = {};  
    this.parent = parent;  
     // ...

每个模块内部,都有一个module对象,代表当前模块。它有以下属性。

module.id模块的识别符,通常是带有绝对路径的模块文件名。

module.filename模块的文件名,带有绝对路径。

module.loaded返回一个布尔值,表示模块是否已经完成加载。

module.parent返回一个对象,表示调用该模块的模块。

module.children返回一个数组,表示该模块要用到的其他模块。

module.exports表示模块对外输出的值。例如:

// example.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);

输出的module:

{ id: '.',
  exports: { '$': [function] },
  parent: null,
  filename: '/path/to/example.js',
  loaded: false,
  children:
   [ { id: '/path/to/node_modules/jquery/dist/jquery.js',
       exports: [function],
       parent: [circular],
       filename: '/path/to/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [object] } ],
  paths:
   [ '/home/user/deleted/node_modules',
     '/home/user/node_modules',
     '/home/node_modules',
     '/node_modules' ]
}

如果在命令行下调用某个模块,比如node something.js,那么module.parent就是null。如果是在脚本之中调用,比如require('./something.js'),那么module.parent就是调用它的模块。利用这一点,可以判断当前模块是否为入口脚本。

if (!module.parent) {
    // ran with `node something.js`
    app.listen(8088, function() {
        console.log('app listening on port 8088');
    })
} else {
    // used with `require('/.something.js')`
    module.exports = app;
}

module.exports属性module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。exports变量为了方便,node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

所以,在对外输出模块接口时,也可以向exports对象添加方法。

exports.area = function (r) {
  return math.pi * r * r;
};

exports.circumference = function (r) {
  return 2 * math.pi * r;
};

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

exports = function(x) {console.log(x)};

上面这样的写法是无效的,因为exports不再指向module.exports了。下面的写法也是无效的。

exports.hello = function() {
  return 'hello';
};

module.exports = 'hello world';

上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。4、require命令4.1 加载规则(详见node加载模块顺序)4.2 模块的缓存第一次加载某个模块时,node会缓存该模块。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个module对象,以后再加载该模块,就直接从缓存取出对应的module对象,然后获取module.exports属性。

{  //module对象
   id: '...',  
   exports: { ... }, 
   loaded: true,  ...
}

即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,commonjs 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除缓存。所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写。

// 删除指定模块的缓存
delete require.cache[modulename];

// 删除所有模块的缓存
object.keys(require.cache).foreach(function(key) {
  delete require.cache[key];
})

注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。4.3 require.mainrequire方法有一个main属性,可以用来判断模块是直接执行,还是被调用执行。直接执行的时候(node module.js),require.main属性指向模块本身。

require.main === module  // true

调用执行的时候(通过require加载该脚本执行),上面的表达式返回false。5、模块的加载机制commonjs模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

// lib.js
var counter = 3;
function inccounter() {
  counter++;
}
module.exports = {
  counter: counter,
  inccounter: inccounter,
};
// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.inccounter();
console.log(mod.counter); // 3

上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了(调用mod.inccounter()方法时,操作的是lib.js模块中的变量counter,而不是导出的变量counter)。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

// lib.js
var counter = 3;
function inccounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  inccounter: inccounter,
};

上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。

上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。

5.1 require的内部处理流程require命令是commonjs规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用node的内部命令module._load。

module._load = function(request, parent, ismain) {};

在这个函数中需要执行以下几步:1. 检查 module._cache,是否缓存之中有指定模块2. 如果缓存之中没有,就创建一个新的module实例3. 将它保存到缓存4. 使用 module.load() 加载指定的模块文件,读取文件内容之后,使用 module.compile() 执行文件代码5. 如果加载/解析过程报错,就从缓存删除该模块6. 返回该模块的 module.exports

采用module.compile()执行指定模块的脚本,逻辑如下。

module.prototype._compile = function(content, filename) {};

在该函数中需要执行以下几步:1. 生成一个require函数,指向module.require;2. 加载其他辅助方法到require;3. 一旦require函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。该函数的参数包括require、module、exports,以及其他一些参数。

(function (exports, require, module, __filename, __dirname) {
  // your code injected here!

// 执行读取的hello.js代码:
    function greet(name) {
        console.log('hello, ' + name + '!');
    }
    module.exports = greet;
    // hello.js代码执行结束

});

4. 执行该函数上面的第1步和第2步,require函数及其辅助方法主要如下。

require(): 加载外部模块

require.resolve():将模块名解析到一个绝对路径

require.main:指向主模块

require.cache:指向所有缓存的模块

require.extensions:根据文件的后缀名,调用不同的执行函数module._compile方法是同步执行的,所以module._load要等它执行完成,才会向用户返回module.exports的值。5.2 模块的循环加载commonjs 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。我们来看一段代码:

// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

当第一次require('a.js')时,执行a.js脚本。先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。a.js已经执行的部分,只有一行。exports.done = false;因此,对于b.js来说,它从a.js只输入一个变量done,值为false。然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。

最后输出的结果:

$ node main.js

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行

exports.done = true;

5.3 node.js是如何实现模块node实现“模块”功能的奥妙在于javascript是一种函数式编程语言,它支持闭包。如果我们把一段javascript代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。请注意我们编写的hello.js代码是这样的:

var s = 'hello';
var name = 'world';
console.log(s + ' ' + name + '!');

node.js加载了hello.js后,它可以把代码包装一下,变成这样执行:

(function () {    
   // 读取的hello.js代码:    
   var s = 'hello';    
   var name = 'world';    
   console.log(s + ' ' + name + '!');    
   // hello.js代码结束
})();

这样一来,原来的全局变量s现在变成了匿名函数内部的局部变量。如果node.js继续加载其他模块,这些模块中定义的“全局”变量s也互不干扰。所以,node利用javascript的函数式编程的特性,轻而易举地实现了模块的隔离。