Node:深入模块机制

Node:深入模块机制

1. CommonJS规范

  • CommonJS(http://www.commonjs.org)规范的出现,其目标是为了构建JavaScript在包括Web服务器,桌面,命令行工具,及浏览器方面的生态系统。CommonJS其实不是一门新的语言,甚至都不能说它是一个新的解释器——实际上它只是一个概念或者是一个规范

  • 它具体弥补了前端JavaScript 的哪些空白呢?其实这也涉及了很多前端JavaScript 所没有涉及的东西,如二进制、编码、IO、文件、系统、断言测试、套接字、事件队列、Worker、控制台等等

  • CommonJS制定了解决这些问题的一些规范,而Node.js就是这些规范的一种实现。Node.js自身实现了require方法作为其引入模块的方法,同时NPM也基于CommonJS定义的包规范,实现了依赖管理和模块自动安装等功能。这里我们将深入一下Node.js的require机制和NPM基于包规范的应用。

2. 简单模块的定义和使用

先来看一个简单定义的模块计算圆的面积:

// circle.js

var PI = Math.PI;

exports.area = function(r){
    
    return PI * r * r;

}

// app.js

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

console.log(circle.area(2));	// 4PI

可以看到模块的调用十分地方便,只需要require需要调用的文件即可;Node.js讲模块的定义和调用都封装地及其简约,对用户十分友好;

3.exports和module.exports的区别

来看看一个小栗子:

// rocker.js
module.exports = 'ROCK N ROLL!';
exports.name = function() {
    
    
    console.log('Let them know that we still rock n roll');
};

//app.js
var rocker = require('./rocker.js');
rocker.name(); // TypeError: Object ROCK N ROLL! has no method 'name'
  • rocker模块完全忽略了exports.name,然后返回了一个字符串’ROCK IT!’

  • 通过上面的例子,你可能认识到你的模块不一定非得是模块实例(module instances)。你的模块可以是任何合法的JavaScript对象 - boolean,number,date,JSON, string,function,array和其他

  • 如果你没有明确的给module.exports设置任何值,那么exports中的属性会被赋给module.exports中,然后并返回它

正确用法是:

// rocker.js
module.exports = function(name, age) {
    
    
    this.name = name;
    this.age = age;
    this.about = function() {
    
    
        console.log(this.name +' is '+ this.age +' years old');
    };
};

//app.js
var Rocker = require('./rocker.js');
var rocker = new Rocker('yivi',18);
rocker.about();	//yivi is 18 years old
  • 如果你想要你的模块成为一个特别的对象类型,那么使用module.exports;如果你希望你的模块成为一个传统的模块实例(module instance),使用exports

把属性赋予module.exports的结果与把属性赋予给exports是一样的。看下面这个例子:

扫描二维码关注公众号,回复: 12701012 查看本文章
module.exports.name = function() {
    
    
    console.log('My name is Lemmy Kilmister');
};

//等同于

exports.name = function() {
    
    
    console.log('My name is Lemmy Kilmister');
};
  • module.exports是真实存在的东西,exports只是它的辅助方法。话虽如此,exports还是推荐的对象,除非你想把你模块的对象类型从传统的模块实例(module instance)修改为其他的

4.模块的载入策略

Node.js的模块分为两类:

  • 一类为原生(核心)模块,一类为文件模块原生模块在Node.js源代码编译的时候编译进了二进制执行文件,加载的速度最快
  • 另一类文件模块是动态加载的,加载速度比原生模块慢

但是Node.js对原生模块和文件模块都进行了缓存,于是在第二次require时,是不会有重复开销的。其中原生模块都被定义在lib这个目录下面,文件模块则不定性

  • 加载文件模块的工作,主要由原生模块module来实现和完成,该原生模块在启动时已经被加载,进程直接调用到runMain静态方法。

  • 实际上在文件模块中,又分为3类模块。这三类文件模块以后缀来区分,Node.js会根据后缀名来决定加载方法:

**.js:**通过fs模块同步读取js文件并编译执行。

**.node:**通过C/C++进行编写的Addon。通过dlopen方法进行加载。

**.json:**读取文件,调用JSON.parse解析加载。

Node.js在编译js文件的过程中会对js文件进行头尾包装,将其包装为一个模块,使其不污染全局,就像这样:

(function (exports, require, module, __filename, __dirname) {
    
    
    var circle = require('./circle.js');
    console.log('The area of a circle of radius 4 is ' + circle.area(4));
});
  • 这段代码会通过vm原生模块的runInThisContext方法执行(类似eval,只是具有明确上下文,不污染全局),返回为一个具体的function对象。最后传入module对象的exports,require方法,module,文件名,目录名作为实参并执行。
  • 这就是为什么require并没有定义在app.js文件中,但是这个方法却存在的原因。从Node.js的API文档中可以看到还有__filename__dirnamemoduleexports几个没有定义但是却存在的变量。其中__filename__dirname在查找文件路径的过程中分析得到后传入的。module变量是这个模块对象自身,exports是在module的构造函数中初始化的一个空对象({},而不是 null)。
  • 在这个主文件中,可以通过require方法去引入其余的模块。而其实这个require方法实际调用的就是load方法
  • load方法在载入、编译、缓存了module后,返回module的exports对象。这就是circle.js文件中只有定义在exports对象上的方法才能被外部调用的原因

以上所描述的模块载入机制均定义在lib/module.js中。

5.require方法中的文件查找策略

  1. 从文件模块缓存中加载
  2. 从原生模块中加载
  3. 从文件加载

require方法接收以下几种参数的传递:

http、fs、path等,原生模块。

/mod或…/mod,相对路径的文件模块。

/pathtomodule/mod,绝对路径的文件模块。

mod,非原生模块的文件模块。

对于每一个被加载的文件模块,创建这个模块对象的时候,这个模块便会有一个paths属性,其值根据当前文件的路径计算得到。

path的生成规则是:从当前文件目录开始查找node_modules目录;然后依次进入父目录,查找父目录下的node_modules目录;依次迭代, 直到根目录下的node_modules目录。

如果require绝对路径中的文件,查找时不会由内而外地遍历每一个node_modules目录,速度最快。

6.包结构

前面提到JavaScript缺少包结构。CommonJS致力于改变这种现状,于是定义了包的结构规范(http://wiki.commonjs.org/wiki/Packages/1.0)。而NPM的出现则是为了在CommonJS规范的基础上,实现解决包的安装卸载,依赖管理,版本管理等问题。require的查找机制明了之后,我们来看一下包的细节。

一个package.json文件应该存在于包顶级目录下。

二进制文件应该包含在bin目录下。

JavaScript代码应该包含在lib目录下。

文档应该在doc目录下。

单元测试应该在test目录下。

由上文的require的查找过程可以知道,Node.js在没有找到目标文件时,会将当前目录当作一个包来尝试加载,所以在package.json文件中最重要的一个字段就是main。而实际上,这一处是Node.js的扩展,标准定义中并不包含此字段, 对于require,只需要main属性即可。但是在除此之外包需要接受安装、卸载、依赖管理,版本管理等流程,所以CommonJS为package.json文件定义了如下一些必须的字段:

name : 包名,需要在NPM上是唯一的。不能带有空格。

description : 包简介。通常会显示在一些列表中。

version : 版本号。一个语义化的版本号(http://semver.org/),通常为x.y.z。该版本号十分重要,常常用于一些版本控制的场合。

keywords : 关键字数组。用于NPM中的分类搜索。

maintainers : 包维护者的数组。数组元素是一个包含name、email、web三个属性的JSON对象。

contributors : 包贡献者的数组。第一个就是包的作者本人。在开源社区,如果提交的patch被merge进master分支的话,就应当加上这个贡献patch的人。格式包含name和email。

bugs : 一个可以提交bug的URL地址。可以是邮件地址 (mailto:mailxx@domain),也可以是网页地址(http://url)。

licenses : 包所使用的许可证。

repositories : 托管源代码的地址数组。

dependencies : 当前包需要的依赖。这个属性十分重要,NPM会通过这个属性,帮你自动加载依赖的包。

scripts: 指明了在进行操作时运行哪个文件,或者执行拿条命令

7.Node.js模块与前端模块的异同

通常有一些模块可以适用于前后端,但是浏览器通过script标签加载的JavaScript文件时裸漏的,而Node.js在载入到最终的执行过程中,进行了包装,使得每个文件的变量都封装在一个闭包内,不会污染全局变量。

所以为了解决前后端一致性的问题,类库开发者需要将类库代码包装在一个闭包内

所以在设计前后端通用的JavaScript类库时,都有着以下类似的判断:

if (typeof exports !== "undefined") {
    
    
    exports.EventProxy = EventProxy;
} else {
    
    
    this.EventProxy = EventProxy;
}

即,如果exports对象存在,则将局部变量挂载在exports对象上,如果不存在,则挂载在全局对象上

猜你喜欢

转载自blog.csdn.net/yivisir/article/details/107772449