Node模块详解(二)

介绍

上篇文章介绍了模块机制中的文件模块(直通链接),本篇文章继续接着介绍node模块机制中剩余的模块类型:

  • 核心模块
  • 扩展模块

注:本文是学习《深入浅出Node.js》一书的学习笔记,所以参考源码与书上版本一致。(版本:v0.10.13-release)

核心模块

什么是核心模块

区别于文件模块等第三方导入模块,核心模块就是直接包含在node源码中,为用户提供服务的模块

当然这些模块也需要导入的,想了解都有哪些模块,直接进入官网,找到Docs文档,里面介绍的模块都是核心模块(链接:核心模块介绍)。

简单列举几个比较常用的核心模块:

  • fs:提供对文件的读写等操作
  • http:提供了搭建本地服务器的API
  • path:提供文件路径操作的API
  • os:操作系统相关信息的API
  • url:提供了操作URL信息的API

核心模块导入流程

核心模块的使用官网已经非常详细了,我也没什么好介绍的,今天的重点是核心模块在Node源码中是以什么形式存在的,以及核心模块的导入流程。

Node源码中的核心模块

核心模块一般分为两部分:

  • C++编写的内建模块(核心实现)
  • JavaScript编写的封装模块

其中C++部分的代码存放在src/目录下,而JavaScript的代码存放在lib/下。

一般我们调用的都是JS编写的封装模块,在对Node源码不是特别了解的情况下,不建议直接调用C++编写的内建模块(调用方法也就不告诉你了)。

导入流程

导入流程主要分为如下几个步骤:

  1. js代码转存
  2. 运行时从内存中取出内容
  3. 编译执行
  4. 对象缓存和返回

看不懂没关系,下面详细介绍。

js代码转存

node编译过程中,并不会对js部分的代码进行编译操作,而是调用tools/js2c.py工具将js代码转换成C++中的字符串数组,存放在node_natives.h文件中,手动执行后的情况如下:

js2c

看代码就知道,这一步其实做了个很简单的事情,把js文件内容console.log("hello");变成ASCII码放在$filename_native数组中,这样node源码编译时,js文件的内容也就以字符串的形式编译进了可执行文件中。

取出文件

知道内容被存到node_natives.h文件中,那么取出的代码就很好找了,首先找到调用文件的地方,运气很好,只有一个文件node_javascript.cc,而且确实是获取js文件的代码,函数很简单,直接贴源码了

1
2
3
4
5
6
7
8
9
10
11
void DefineJavaScript(v8::Handle<v8::Object> target) {
HandleScope scope;

for (int i = 0; natives[i].name; i++) {
if (natives[i].source != node_native) {
Local<String> name = String::New(natives[i].name);
Handle<String> source = BUILTIN_ASCII_ARRAY(natives[i].source, natives[i].source_len);
target->Set(name, source);
}
}
}

一共几行代码,功能就是把想要的js文件内容取出,这里有个条件,是除了node_native模块,这里其实就是node.js文件,与其他js文件不同的是它在src/下,这个文件很重要,这里先不说明,我们继续往下走。

以前版本的代码就是简单,这里我们查使用函数DefineJavaScript()的地方,发现也只有一个地方,就是我们的node.cc文件(题外话,这个文件就是定义着我们的全局变量process的地方),而使用的函数为Binding(),以下为源码内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static Handle<Value> Binding(const Arguments& args) {
Local<String> module = args[0]->ToString();
String::Utf8Value module_v(module);
...
if ((modp = get_builtin_module(*module_v)) != NULL) {
...
} else if (!strcmp(*module_v, "constants")) {
...
} else if (!strcmp(*module_v, "natives")) {
// 重点关注内容
exports = Object::New();
DefineJavaScript(exports);
binding_cache->Set(module, exports);
} else {
return ThrowException(Exception::Error(String::New("No such module")));
}
return scope.Close(exports);
}

去掉无关的代码,留下我们的重要关注内容,从这部分代码可以得出,调用process.Binding('natives')就可以取出内存中的js代码,而这个引用就在我们刚刚谈到的node.js中,

1
NativeModule._source = process.binding('natives');

重点出来了,之前我们讲过,文件模块操作的对象为Module,那么我们核心模块的对象就是NativeModule

编译&缓存

当我们对一个核心模块引用时,实际上就是运行了下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
NativeModule.require = function(id) {
if (id == 'native_module') {
return NativeModule;
}

var cached = NativeModule.getCached(id);
if (cached) {
return cached.exports;
}

if (!NativeModule.exists(id)) {
throw new Error('No such native module ' + id);
}

process.moduleLoadList.push('NativeModule ' + id);

var nativeModule = new NativeModule(id);

nativeModule.cache();
nativeModule.compile();

return nativeModule.exports;
};
// 从缓存中取出
NativeModule.getCached = function(id) {
return NativeModule._cache[id];
}
// 判断是否存在
NativeModule.exists = function(id) {
return NativeModule._source.hasOwnProperty(id);
}
// 从内存中得到js代码
NativeModule.getSource = function(id) {
return NativeModule._source[id];
}
// 封装
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
// 编译
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);

var fn = runInThisContext(source, this.filename, true);
fn(this.exports, NativeModule.require, this, this.filename);

this.loaded = true;
};
// 保存到缓存中
NativeModule.prototype.cache = function() {
NativeModule._cache[this.id] = this;
};

一下子放这么多代码实在是不应该,但是这部分代码简单而道出了核心模块导入的本质,实在不忍心割掉一点点,每个函数都对功能添加了注释,请务必仔细研读。

另外谈一点,核心模块的js代码因为是从内存中取出的,相比于从磁盘取出的文件模块加载速度肯定要快上很多。

核心模块至此就结束了,流程简单来说就是:编译node时将核心js代码变成c++中的字符串数组,引用的时候从内存中取出并进行编译,然后缓存放到NativeModule._cache中并返回编译后的对象,如果你看过文件模块的介绍,应该并不难掌握。

扩展模块

终于讲完了核心模块,希望你已经掌握,下面来讲一种特殊的文件模块,扩展模块。

什么是扩展模块

亘古不变,我们先来说下是什么。

扩展模块是用C/C++代码预编译后得到的.node文件,代码中调用process.dlopen()进行加载执行。显然这种方式性能要高于js编写的文件模块,而且在需要大量的位运算操作的场景中,C/C++的性能要优于JS。

JavaScript的位运算参照Java的位运算实现,但是Java位运算是在int型数字的基础上进行的,而JavaScript中只有double型的数据类型,在进行位运算的过程中,需要将double型转换为int型,然后再进行。所以,在JavaScript层面上做位运算的效率不高。(节选自书中)

这里你可能要说,那大家都去写扩展模块算了。世界上没有完美的东西,扩展模块也有他的缺点,当然,C/C++语言的学习和开发难度肯定是第一点,不然脚本语言也不会盛行于世,其次就是不再支持跨平台,对于*nix和windows平台的差异如下:

区别

这里也为我们展现了扩展模块的编译和导入流程。

所以在需要跨平台的应用上,应该对扩展模块的使用要小心,

实现一个自己的扩展模块

是的,这里应该有一个实现,不过这部分我并没有掌握清楚,书中的例子因为版本古老,编译失败了,所以这里也就不展示了,给自己留个坑,也希望感兴趣的小伙伴可以研究实现一个自己的扩展模块,加油,你们是最胖的!!!

总结

模块深入学习打开了我对node源码解读的开端,尽管代码是老的版本,但依旧能从中学到很多,这一系列文章是我读《深入浅出Node.js》一书的学习笔记,也希望自己能够成功读完,并将学习后的知识分享给大家。

-------------本文结束,感谢您的阅读-------------