之前提到,Node.js的核心模块会在编译Node的时候以二进制的方式被编译进来。核心模块分为c/c++和Javascript两部分。c/c++存在Node的src目录下,Javascript存在lib目录下。
2.3.1 Javascript核心模块编译过程
Node采用了V8附带的js2c.py工具 ,将内置的Javascript代码(src/node.js和lib/*.js)转换成c++数组,生成node_natives.h文件,在这个过程中Javascript代码以字符串的形式存储在Node命名空间中,是不可执行文件。在Node启动时,Javascript代码直接加载进内存中。
2.3.2 c/c++核心模块的编译过程
核心模块中,有一些模块是完全由c/c++实现,另一些模块的核心文件是由c/c++实现,另一部分是由Javascript封装。Node这种复合模式可以在开发速度和性能之间找到平衡点。
完全由c/c++编写的统一称为内建模块,它们不会被用户直接调用。Node的buffer,crypto,evals,fs,os等模块都是部分通过c/c++编写。
内建模块的优势在于:首先它们是由c++开发,性能优于脚本语言;其次,在进行文件编译时,它们被编译进二进制文件。一旦Node开始执行,它们被加载进内存中,无须再次做标识符,文件定位,编译等过程,直接就可执行。
Node所有模块类型中,存在着一种依赖关系,文件模块(Javascript)依赖核心模块,而核心模块(Javascript)依赖内建模块(c/c++)。
这里有个问题,内建模块如何把变量或者方法倒出,从而核心模块可以访问到呢?
Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助加载内建模块。在加载内建模块时,先创建一个exports空对象,调用get_builtin_module()方法取出内建模块对象,通过执行register_func()填充exports对象,最后将exports对象按模块名缓存并返回给调用方完成导出。
该方法不仅可以倒出方法,还可以导出其他内容。
2.3.3 编写核心模块
以c/c++模块为例,演示如何编写内建模块。先编写一个极其简单的javascript版本原型,这个方法返回一个HelloWorld字符串:
exports.sayHello = function(){
return 'hello world';
}
编写内置模块分为两部分,编写头文件和编写c/c++文件。
1.将如下代码保存为node_hello.h,存放到Node的src目录下:
#ifndef NODE_HELLO_H_
#define NODE_HELLO_H_
#include<v8.h>
namespace node {
// 预定义方法
v8::Handle<v8::Value> SayHello(const v8::Arguments& args);
}
#endif
2.编写node_hello.cc,并存储到src目录下:
#include <node.h>
#include <node_hello.h>
#include <v8.h>
namespace node {
// 实现预定义方法
Handle<Value> SayHello(const Arguments& args){
HandleScopt scope;
return scope.Close(String::New("hello world!"));
}
}
// 给传入的目标对象添加sayHello方法
void Init_Hello(Handle<Object> target){
target->Set(String::NewSymbol("sayHello"),FunctionTemplate::New(SayHello)->GetFunction());
}
// 调用NODE_MODULE()将注册方法定义到内存中
NODE_MODULE(node_hello,node::Init_Hello)
以上两部完成内建模块的编写,但是真正要让Node认为它是内建模块,还需要更改src/node_extension.h,在NODE_EXT_LIST_END前添加NODE_EXT_LIST_ITEM(node_hello),以将node_hello模块添加进node_module_list数组中。
另外还需要使得编译的代码编译进执行文件。同时还需要更改Node的项目生成文件node.gyp,并在’target_name’:'node’节点的sources中添加新编写的两个文件。
编译和安装后,直接node执行如下代码:
var hello = process.binding('hello');
hello.sayHello();