Nodejs模块机制

# 1. CommonJS 规范
# 1.1 模块引用
模块上下文提供require()方法来引入外部模块
//test.js
//引入一个模块到当前上下文中
const math = require('math');
math.add(1, 2);
2
3
4
# 1.2 模块定义
模块上下文提供exports对象用于导入导出当前模块的方法或变量,且它是唯一的导出出口。模块中存在一个module对象,它代表模块自身,exports是module的属性。一个文件就是一个模块。将方法作为属性挂载在exports上。
//math.js
exports.add = function () {
let sum = 0, i = 0, args = arguments, l = args.length;
while(i < l) {
sum += args[i++];
}
return sum;
}
2
3
4
5
6
7
8
# 1.3 模块标识
传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径或者绝对路径,可以没有文件后缀名.js.
# 2. Node的模块实现
在Node中引入模块,需经历四个步骤:
- 路径分析
- 文件定位
- 编译执行
- 加入内存
# 2.1 路径分析
Node.js中模块可以通过文件路径或名字获取模块的引用。 模块的引用会映射到一个js文件路径。 在Node中模块分为两类:
- Node提供的模块,核心模块(内置模块),公开了一些常用的API给开发者,且它们在Node进程开始的时候就预加载了
- 用户编写的模块,文件模块,如通过NPM安装的第三方模块或本地模块,每个模块都会暴露一个公开的API,供开发者导入使用。 核心模块是node源码在编译过程中编译成二进制执行文件。在node启动时就被加载进内存中,所以核心模块引入时省去了文件定位和编译执行两个步骤,并且在路径分析中优先判断。 因此,核心模块的加载是最快的,文件模块则是在运行时动态加载,速度比核心模块慢。
node模块的载入及缓存机制:
- 载入内置模块
- 载入文件模块
- 载入文件目录模块
- 载入node_modules里的模块
- 自动缓存已载入模块
# 载入内置模块
- 引用时直接用名字而非文件路径
- 第三方的模块和内置模块同名时,会覆盖第三方同名模块
# 载入文件模块
- 分为绝对路径和相对路径
- 可忽略扩展名
.js
# 载入文件目录模块
require一个目录文件。
node首先会在文件夹里试图找到包定义文件package.json,若没找到,就找默认住文件index.js,若index.js也不存在,将加载失败。
# 载入node_modules里的模块
如果当前目录的node_modules里没找到,node会从父目录的node_modules里搜索,这样递归下去直到根目录
# 自动缓存已载入模块
对于已加载的模块Node会缓存下来,而不必每次都重新搜索载入
# 优先从缓存加载
和浏览器会缓存静态js文件一样,node也会对引入的模块进行缓存。不同的是,浏览器仅仅缓存文件,而node缓存的是编译和执行后的对象(缓存内存)。
require()对相同模块的多次加载一律采用缓存优先的方式,核心模块缓存检查先于文件模块的缓存检查。
对于每一个被加载的文件模块,创建这个模块对象的时候,这个模块便会有一个paths属性,其值根据当前文件的路径 计算得到。
举个例子:
// modulepath.js
console.log(module.paths);
2
我们将其放到任意一个目录中执行node modulepath.js命令,将得到以下的输出结果
[ '/home/ikeepstudying/research/node_modules',
'/home/ikeepstudying/node_modules',
'/home/node_modules',
'/node_modules' ]
2
3
4
# 2.2 文件定位
# 文件扩展名分析
调用require()方法时若参数没有文件扩展名,Node会按.js、.json、.node的顺序补足扩展名,依次尝试。
在尝试过程中,需要调用fs模块阻塞式地判断文件是否存在。因为Node的执行是单线程的,这是一个会引起性能问题的地方。如果是.node或者.json文件可以加上扩展名加快一点速度。另一个诀窍是:同步配合缓存。
# 目录分析和包
# 2.3 模块编译
每个模块文件模块都是一个对象,它的定义如下:
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 = [];
}
2
3
4
5
6
7
8
9
10
11
对于不同扩展名,其载入方法也有所不同:
.js通过fs模块同步读取文件后编译执行。.node这是C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件.json同过fs模块同步读取文件后,用JSON.pares()解析返回结果 每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上。
# json 文件的编译
//Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = NativeModule.require('fs').readFileSync(filename, 'utf-8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch(err) {
err.message = filename + ':' + err.message;
throw err;
}
}
2
3
4
5
6
7
8
9
10
Module._extensions会被赋值给require()的extensions属性,所以可以用:console.log(require.extensions);输出系统中已有的扩展加载方式。 当然也可以自己增加一些特殊的加载:
require.extensions['.txt'] = function(){
//code
};。
2
3
# js模块的编译
在编译的过程中,Node对获取的javascript文件内容进行了头尾包装,将文件内容包装在一个function中:
(function (exports, require, module, __filename, __dirname) {
var math = require(‘math‘);
exports.area = function(radius) {
return Math.PI * radius * radius;
}
})
2
3
4
5
6
包装之后的代码会通过vm原生模块的runInThisContext()方法执行(具有明确上下文,不污染全局),返回一个具体的function对象,最后传参执行,执行后返回module.exports.
# 核心模块编译
核心模块分为C/C++编写和JavaScript编写的两个部分,其中C/C++文件放在Node项目的src目录下,JavaScript文件放在lib目录下。 1.转存为C/C++代码 Node采用了V8附带的js2c.py工具,将所有内置的JavaScript代码转换成C++里的数组,生成node_natives.h头文件:
namespace node {
const char node_native[] = { 47, 47, ..};
const char dgram_native[] = { 47, 47, ..};
const char console_native = { 47, 47, ..};
const char buffer_native = { 47, 47, ..};
const char querystring_native = { 47, 47, ..};
const char punycode_native = { 47, 47, ..};
...
struct _native {
const char* name;
const char* source;
size_t source_len;
}
static const struct _native natives[] = {
{ "node", node_native, sizeof(node_native)-1},
{ "dgram", dgram_native, sizeof(dgram_native)-1},
...
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在这个过程中,JavaScript代码以字符串形式存储在node命名空间中,是不可直接执行的。在启动Node进程时,js代码直接加载到内存中。在加载的过程中,js核心模块经历标识符分析后直接定位到内存中。 2.编译js核心模块 lib目录下的模块文件也在引入过程中经历了头尾包装的过程,然后才执行和导出了exports对象。与文件模块的区别在于:获取源代码的方式(核心模块从内存加载)和缓存执行结果的位置。 js核心模块源文件通过process.binding('natives')取出,编译成功的模块缓存到NativeModule._cache上。代码如下:
function NativeModule() {
this.filename = id + '.js';
this.id = id;
this.exports = {};
this.loaded = fales;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
2
3
4
5
6
7
8
# 3. import 和 require的区别
import是ES6的模块规范,require是commonjs的模块规范。import是静态(编译时)加载模块,require是(运行时)动态加载
静态加载时代码在编译的时候已经执行了,动态加载是编译后在代码运行的时候再执行。 举个例子:
import { name } from 'name.js'
// name.js文件
export let name = 'jinux'
export let age = 20
2
3
4
5
main.js文件里引入了name.js文件导出的变量,在代码编译阶段执行后的代码如下:
let name = 'jinux'
var obj = require('obj.js');
// obj.js文件
var obj = {
name: 'jinux',
age: 20
}
module.export obj;
2
3
4
5
6
7
8
require是在运行阶段,需要把obj对象整个加载进内存,之后用到哪个变量就用哪个,这里再对比一下import,import是静态加载,如果只引入了name,age是不会引入的,所以是按需引入,性能更好一点。
# 4. nodejs清除require缓存
开发nodejs应用时会面临一个问题,就是修改了配置数据后,必须重启服务器才能看到修改后的结果,即如何在修改文件后,自动重启服务器。
// server.js
const port = process.env.port || 1337;
app.listen(port);
console.log("server start in " + port);
exports.app = app;
2
3
4
5
在app.js中引用server.js,如果我们在server.js中启动了服务器,我们停止服务器可在app.js中调用
app.app.close()
但是当我们重新引入server.js的时候发现并不是用的最新的server.js文件,原因是require的缓存机制,在第一次调用的时候缓存下来了。怎么办?
下面的代码解决了这个问题
// app.js
delete require.cache[require.resolve('./server.js')];
app = require('./server.js');
2
3