CommonJs规范 详解

CommonJs可以做以下内容:
1、服务端应用程序
2、命令行工具
3、基于桌面的应用程序
4、混合开发

1. CommonJs规范

关于模块:

  • 每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
  • 在模块中使用global 定义全局变量,不需要导出,在别的文件中可以访问到。
  • 每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口
  • 通过 require加载模块,读取并执行一个js文件,然后返回该模块的exports对象
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序

2. module对象

node内部提供一个Module构建函数。所有的模块其实都是Module的实例。

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

  • module.id,模块的识别符,通常是带有绝对路径的模块文件名;
  • module.filename,模块的文件名,带有绝对路径;
  • module.loaded,返回一个boolean值,表示模块是否已经完成加载;
  • module.parent,返回一个对象,表示调用该模块的模块;
  • module.children,返回一个数组,表示该模块内用到的其他模块;
  • module.exports,表示模块对外输出的值;

下图中打印module的结果:
commonJs

2.1 module.exports属性和exports变量

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

为了方便,node为每一个模块提供了一个exports变量,指向module.exports。这等同在每个模块头部,有一条这样的命令。

var exports = module.exports;

所以我们在对外输出模块接口时,可以向exports对象中添加方法或者属性。下面这么写都是可以的。

module.exports.name = "zhangsan";
exports.name = 'lisi'
exports.age = 18;

但是注意,不可以直接将exports变量指向一个值,因为这样等于切断了exports和module.exports的联系。这意味着,如果一个模块的对外接口,是一个单一的值,这种情况就不能使用exports输出,只能使用module.exports输出。

exports = "lisi"; //错误的
exports = function(){return "lisi"}; //错误的

下面这种写法中,hello函数是无法对外输出的,因为module.exports被重新赋值了

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

如果你觉得exports 和 module.exports 之间的区别很难分清,一个简单的处理方法,就是放弃使用exports ,只使用 module.exports。

3. require

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

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

3.1 require的内部处理流程

当使用require加载模块时,在node内部,会做如下处理:

  1. 先检查Moducle._cache, 看里面有没有对应模块的缓存;
  2. 如果缓存没有的话,就创建一个新的module实例,并把它添加到缓存中,缓存的是它exports导出的值;
  3. 如果缓存中有的话,使用module.load()这个方法,去加载这个模块,读取文件内容后,使用module.compile()执行文件代码;
  4. 如果解析的过程中,出现异常,就从缓存中删除这个模块;
  5. 如果没有出现异常,最后返回这个模块的module.exports;

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

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

3.1 加载规则

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

  1. 如果参数字符串以“/”开头,表示加载的是一个位于绝对路径的模块文件。
  2. 如果参数字符串以“./”开头,表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。
  3. 如果参数字符串不以“/”或者“./”开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
  4. 如果参数字符串不以“/”或者“./”开头,而是一个路径,则会先找到该路径目录,然后再以它为参数找到后续路径。
  5. 如果指定的模块文件没有被发现,Node会尝试为文件添加.js、.json、.node后,再去搜索。.js文件会以文本格式的javascript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
  6. 如果想得到require命令加载的确切文件名,使用require.resolve()方法。

3.2 模块的缓存

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

3.2.1 模块缓存的案例1

require('./user.js');
require('./user.js').message = "hello";
require('./user.js').message
// "hello"

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

3.2.1 模块缓存的案例2

再来看下面这个案例:

// 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);

上面的例子中,当执行main.js时,加载了a.js,先输出x为a1,又加载了b.js,b.js中输出x为b1,又加载a.js,此时因为之前已经加载过a.js了,所以直接从缓存中读取,a.js的x为a1,所以先打印出"b.js a1";然后b.js输出x为b2,所以a.js中打印“a.js b2",然后a.js中又输出x为a2;所以main.js中打印”main.js a2",第二次再打印的时候,因为a.js和b.js都已经被缓存了,所以直接读取它们的exports的值,所以直接打印出“main.js b2"。
commonjs
所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写。

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

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

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

3.2.3 require.main

require方法有一个main属性,可以用来判断模块是直接执行,还是被调用执行。
直接执行的时候(node module.js),require.main属性指向模块本身。

require.main === module
// true

调用执行的时候(通过require加载该脚本执行),上面的表达式返回false。

4. 深入了解模块原理

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的函数式编程的特性,轻而易举地实现了模块的隔离。

但是,模块的输出module.exports怎么实现?

这个也很容易实现,Node可以先准备一个对象module:

// 准备module对象:
var module = {
    id: 'hello',
    exports: {}
};
var load = function (module) {
    // 读取的hello.js代码:
    function greet(name) {
        console.log('Hello, ' + name + '!');
    }
    
    module.exports = greet;
    // hello.js代码结束
    return module.exports;
};

var exported = load(module);
var exports= module.exports;
//exports="hellworld"; 错误
// 保存module:
save(module, exported);

可见,变量module是Node在加载js文件前准备的一个变量,并将其传入加载函数,我们在hello.js中可以直接使用变量module原因就在于它实际上是函数的一个参数:

module.exports = greet;

通过把参数module传递给load()函数,hello.js就顺利地把一个变量传递给了Node执行环境,Node会把module变量保存到某个地方。

由于Node保存了所有导入的module,当我们用require()获取module时,Node找到对应的module,把这个module的exports变量返回,这样,另一个模块就顺利拿到了模块的输出。

参考链接:https://javascript.ruanyifeng.com/nodejs/module.html

发布了130 篇原创文章 · 获赞 46 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Charissa2017/article/details/104928459