CommonJS规范

目录

1. 概述
2. module 对象
  2.1 module.exports 属性
  2.2 exports 变量
3. AMD 规范与 CommonJS 规范的兼容性
4. require 命令
  4.1 基本用法
  4.2 加载规则
  4.3 目录的加载规则
  4.4 模块的缓存
  4.5 模块的循环加载
  4.6 require.main
5. 模块的加载机制
  5.1 require 内部的处理流程
6. 参考链接

1. 概述


Node 应用由模块组成,采用 CommonJS 模块规范。

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

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

上面代码中,变量 x 和函数 addX,是当前文件example.js私有的,其他文件不可见。

如果想在多个文件分享变量,必须定义为global对象的属性。

global.APP = 'mApp';

上面代码的warning变量,可以被所有文件读取。当然,这样写法是不推荐的。

CommonJS 规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

var x = 5;
var addX = function (value) {
  return value + x;
};

// 写法一
// module.exports.x = x;
// module.exports.addX = addX;

// 写法二
module.exports = {
  x: x,
  addX: addX
};

上面代码通过module.exports输出变量x和函数addX

require方法用于加载模块。

var example = require('./example');

console.log(example.x); // 5
console.log(example.addX(2)); // 7

require方法的详细解释参见《Require命令》一节。

CommonJS 模块的特点如下:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

2. 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 表示模块对外输出的值。

下面是一个示例文件,最后一行输出module变量。

// example.js
var math = require('./math');

console.log(math.add(2, 3)); // 5
console.log(module);

执行这个文件,命令行会输出如下信息。

Module {
  id: '.',
  path: 'T:\Labs\Lab2\模块化\demo5',
  exports: {},
  parent: null,
  filename: 'T:\Labs\Lab2\模块化\demo5\example.js', 
  loaded: false,
  children: [
    Module {
      id: 'T:\Labs\Lab2\模块化\demo5\math.js',      
      path: 'T:\Labs\Lab2\模块化\demo5',
      exports: [Object],
      parent: [Circular],
      filename: 'T:\Labs\Lab2\模块化\demo5\math.js',
      loaded: true,
      children: [],
      paths: [Array]
    }
  ],
  paths: [
    'T:\Labs\Lab2\模块化\demo5\node_modules',       
    'T:\Labs\Lab2\模块化\node_modules',
    'T:\Labs\Lab2\node_modules',
    'T:\Labs\node_modules',
    'T:\node_modules'
  ]
}

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

if (!module.parent) {
    // ran with `node something.js`(项目中,一般入口文件才会直接用 node 命令去执行)
    app.listen(8088, function() {
        console.log('app listening on port 8088');
    })
} else {
    // used with `require('/.something.js')`
    module.exports = app;
}

2.1 module.exports 属性

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

var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();

setTimeout(function() {
  module.exports.emit('ready');
}, 1000);

上面模块会在加载后1秒后,发出ready事件。其他文件监听该事件,可以写成下面这样。

var a = require('./a');
a.on('ready', function() {
  console.log('module a is ready');
});

2.2 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变量指向一个值,因为这样等于切断了exportsmodule.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输出。

  • 无效的赋值
// a.js
exports = '单一的值'; // 无效
// b.js
var a = require('./a');

console.log(a); // {}
  • 有效的赋值
// a.js
module.exports = '单一的值'; // 有效
// b.js
var a = require('./a');

console.log(a); // 单一的值

3. AMD 规范与 CommonJS 规范的兼容性

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是异步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用异步模式,因此浏览器端一般采用AMD规范。

AMD规范使用define方法定义模块,下面就是一个例子:

define(['package/lib'], function(lib){
  function foo(){
    lib.log('hello world!');
  }

  return {
    foo: foo
  };
});

AMD规范允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:

define(function (require, exports, module){
  var someModule = require("someModule");
  var anotherModule = require("anotherModule");

  someModule.doTehAwesome();
  anotherModule.doMoarAwesome();

  exports.asplode = function (){
    someModule.doTehAwesome();
    anotherModule.doMoarAwesome();
  };
});

4. require 命令

4.1 基本用法

Node使用CommonJS模块规范,内置的require命令用于加载模块文件。

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

// example.js
var invisible = function () {
  console.log("invisible");
}

exports.message = "hi";

exports.say = function () {
  console.log(message);
}

运行下面的命令,可以输出exports对象。

var example = require('./example.js');

console.log(example);
// {
//   message: "hi",
//   say: [Function]
// }

如果模块输出的是一个函数(或一个单一的值),那就不能定义在exports对象上面,而要定义在module.exports变量上面。

module.exports = function () {
  console.log("hello world")
}

require('./example2.js')()

上面代码中,require命令调用自身,等于是执行module.exports,因此会输出 hello world。

4.2 加载规则

require命令用于加载文件,后缀名默认为.js

var foo = require('foo');
//  等同于
var foo = require('foo.js');

根据参数的不同格式,require命令去不同路径寻找模块文件。

1)如果参数字符串以/开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js

2)如果参数字符串以./开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js

3)如果参数字符串以../开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('../circle/a')将加载当前脚本父级同级目录circle 下的 a.js

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

举例来说,脚本/home/user/projects/foo.js执行了require('bar.js')命令,Node会依次搜索以下文件:

C:UsersxxxAppDataLocalMicrosoftTypeScript4.2
ode_modules@types
odear.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js

这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。

5)如果参数字符串不以.//开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。

6)如果指定的模块文件没有发现,Node会尝试为文件名添加.js.json.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。

7)如果想得到require命令加载的确切文件名,使用require.resolve()方法。

4.3 目录的加载规则

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。

在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子:

// package.json
{ 
  "name" : "some-library",
  "main" : "./lib/some-library.js" 
}

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

4.4 模块的缓存

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

// a.js  
console.log('a.js');
// b.js
require('./a');
require('./a').message = 'add property';
console.log(require('./a').message);

输出:

a.js // 一次
add property // 有值

上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。但是第三次加载的时候,这个message属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存。

如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次require这个模块的时候,重新执行一下输出的函数。

// a.js,输出函数
module.exports = function() {
  console.log('a.js');
}
// b.js
require('./a')(); // 执行函数
require('./a')(); // 执行函数
require('./a')(); // 执行函数

输出:

a.js
a.js
a.js

所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写:

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

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

注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。

4.5 模块的循环加载

如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。

// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';

// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';

// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);

上面代码是三个JavaScript文件。其中,a.js加载了b.js,而b.js又加载a.js。这时,Node返回a.js的不完整版本,所以执行结果如下。

$ node main.js
b.js  a1
a.js  b2
main.js  a2
main.js  b2

修改main.js,再次加载a.js和b.js。

// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);

执行上面代码,结果如下:

$ node main.js
b.js  a1
a.js  b2
main.js  a2
main.js  b2
// 这里不再执行 a.js 和 b.js 内部的 console.log 语句
main.js  a2
main.js  b2

上面代码中,第二次加载a.js和b.js时,会直接从缓存读取exports属性,所以a.js和b.js内部的console.log语句都不会执行了。

4.6 require.main

require方法有一个main属性,可以用来判断模块是直接执行,还是被调用执行。

直接执行的时候(node module.js),require.main属性指向模块本身。

// a.js
console.log(require.main === 	module); // false

// main.js
require(./a);
console.log(require.main === 	module); // true

执行上面代码,结果如下:

$ node main.js
false
true

5. 模块的加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。

下面是一个模块文件lib.js

// lib.js
var counter = 3;
function incCounter() {
  counter++;
  return counter;
}

module.exports = {
  counter,
  incCounter
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter

然后,加载上面的模块。

// main.js
let {counter, incCounter} = require('./lib');

console.log(counter); // 3
console.log(incCounter()); // 4
console.log(counter); // 3

上面代码说明,counter输出以后,lib.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
};

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

Module.prototype._compile = function(content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};

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

  • require(): 加载外部模块
  • require.resolve():将模块名解析到一个绝对路径
  • require.main:指向主模块
  • require.cache:指向所有缓存的模块
  • require.extensions:根据文件的后缀名,调用不同的执行函数

一旦require函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。该函数的参数包括requiremoduleexports,以及其他一些参数。

(function (exports, require, module, __filename, __dirname) {
  // YOUR CODE INJECTED HERE!
});

Module._compile方法是同步执行的,所以Module._load要等它执行完成,才会向用户返回module.exports的值。

6. 参考链接

[1] Addy Osmani, Writing Modular JavaScript With AMD, CommonJS & ES Harmony

[2] Pony Foo, A Gentle Browserify Walkthrough

[3] Nico Reed, What is require?

[4] Fred K. Schott, The Node.js Way - How require() Actually Works

原文链接

https://javascript.ruanyifeng.com/nodejs/module.html

原文地址:https://www.cnblogs.com/uakora/p/14601793.html