本文系原创,转载请注明出处~ 了解Node模块的加载过程,有助于从宏观把握一个Js服务器程序是如何组织起来的。使用Node的vm模块,可以绕过require,直接运行Js代码
Node.js程序的入口
// file hello.js
(function (){
console.log('hello world!')
})()
在命令行下执行node hello.js
。Node在启动的时候,根据第二个参数 hello.js
加载js文件并运行。Node.js没有C++/Java那样的main函数,但hello.js犹如main函数那样,是服务端程序运行的总入口。
vm模块
(function(){
console.log('hello world~')
})
将以上代码保存到test.js main.js的内容为
const vm = require('vm');
const fs = require('fs');
const path = require('path');
var prt = path.resolve(__dirname, '.', 'test.js');
function stripBOM(content) {
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
return content;
}
var wrapper = stripBOM(fs.readFileSync(prt, 'utf8'));
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: prt,
lineOffset: 0,
displayErrors: true
});
compiledWrapper();
运行这个文件,得到hello world~
。使用vm模块就能直接运行js文件。在使用require
加载js文件的时候,Node.js内部也是如此读取,编译,运行我们的js文件。
Js文件与模块
用Node.js编写程序,一个js文件对应一个模块,在module.js文件中,其构造函数定义如下
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
node加载一个js文件时,会先new 一个 Module
var module = new Module(filename, parent);
然后读入Js代码并对文件进行头尾包装。如此这般之后,Js文件里面的代码变成这个匿名函数内部的语句。
(function (exports, require, module, __filename, __dirname) {
//原始文件内容
});
上述形式的代码实际上是一个函数字面量,说的直白点,就是一个定义匿名函数的表达式。Node使用V8编译并运行上面的代码,也就是对以上表达式求值,其值是一个函数对象。V8将结果返回给Node。
//右侧表达式的值是一个函数对象
var fn = (function(){
}) ;
Node得到返回的函数对象,使用apply方法,指定上下文,传入 module.exports,require方法, module,文件名以及路径作为参数,执行函数。在这一步,开始执行Js文件内部的代码。
var args = [this.exports, require, this, filename, dirname];
var result = compiledWrapper.apply(this.exports, args); //compiledWrapper 为函数对象
由源代码可知,Js文件运行的上下文环境是module.exports,因此在文件中,也可以直接使用this
导出对象。一旦一个文件加载之后,其对应的模块被缓存,其他文件又require
的时候,直接取缓存。
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
//...
module.exports 或者 exports 区别
从构造函数Module可以看出,他们指向同一个空对象。因此如下代码完全等效
module.exports.IA = 'ia';
exports.IA2 = 'ia';
exports.IAF = function(){
console.log('');
}
甚至可以直接使用this导出
this.IA3 = 'ia';
以上代码都不改变exports
的值,改变的是其指向的对象。但如果对exports
赋值,像这样
module.exports = {
print:function(){
console.log('')
}
}
则只能使用module.exports
导出,如果exports
前不加module
,module.exports
指向的对象仍然是{}
。
小心使用全局变量
把Js文件包装成一个匿名函数运行,使得Js文件内部定义的变量局限于函数内部,只有用exports
导出,外部才可访问。这使得不同文件中,相同的变量名互补干扰。如果定义变量时,前面不加var
,则变量成为全局变量。Node的执行环境存在一个全局的对象global
,它类似于浏览器里面的window
对象。如果代码这样写
function test(){
this.xxx = 'xxx';
yyy = 200;
}
test();
就会给global
增加两个属性。可以运行
console.log(global);
看一下这个全局对象包含哪些成员。
模块的分类
Node.js 的模块可以分三类
1.V8 自身支持的对象,例如Data
、Math
,这些是语言标准定义的对象,由V8直接提供
2.Node.js作为Js运行时,提供了丰富的API, 实现这些API的c++函数和Js代码,在Node初始化时被加载,这部分模块为NativeModule。c++扩展Js接口的部分过于底层。对于Js代码部分,加载使用NativeModule.require
,编译运行完缓存在NativeModule._cache
。相关的代码在bootstrap_node.js
中。bootstrap_node.js
是Node.js最先执行的一段js代码,文件头部有这样一段注释
//Hello, and welcome to hacking node.js!
//This file is invoked by node::LoadEnvironment in src/node.cc, and is
//responsible for bootstrapping the node.js core. As special caution is given
//to the performance of the startup process, many dependencies are invoked
//lazily.
这个文件中包含如下一段代码
var Module = NativeModule.require('module');
//...
preloadModules();
run(Module.runMain);
可见,主模块在这里被加载执行,Module.runMain
在module.js中
// bootstrap main module.
Module.runMain = function() {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
};
3.用户的js文件以及npm安装的模块,根据上面的讨论,这些js文件模块对应Module构造出的对象。require
函数由Module.prototype.require
定义,加载之后模块缓存在Module._cache
。