nodejs模块采用Commonjs规范,弥补Javascript开发大型应用没有标准的缺陷,类似java中的类,python中的import
,nodejs通过module.exports
、require
来导出和引入一个模块。nodejs技术栈
模块分类
1. 系统模块
- C/C++模块,也叫built-in内建模块,用于native模块调用,源码存放于
node/src
目录- native模块,开发中使用的
http
、buffer
、fs
等,底层调用内建模块,源码存放于node/lib
目录
2. 三方模块
非nodejs自带的模块称为三方模块,包括路径形式的文件模块(以
.
和..
和/
)和自定义模块(如express、koa框架等)
模块加载机制
在Node中引入模块,需要经历三个步骤, 路径分析
,文件定位
,编译执行
- 缓存检查:node对引入过的模块(包括系统模块和文件模块)会进行缓存,存编译和执行后的对象。
require
方法对相同模块的二次加载都采用缓存优先的方式,核心模块的缓存检查先于文件模块的缓存检查。
- 加载系统模块:系统模块在node源码编译的过程中,编译成为二进制文件,node进程启动时,部分核心模块就被直接加载进内存中,这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略,并在路径分析中优先判断。 (native模块如何进行路径分析?)
- 路径形式的文件模块:
require
方法将相对路径转换为绝对路径,将编译执行的结果放入缓存中。如果文件没有加上扩展名,会依次按照.js
、.json
、.node
进行补足尝试。在尝试的过程中,需要调用fs
模块同步阻塞式地判断文件是否存在。在分析标识符的过程中,**require
通过分析出文件扩展名之后,可能没有查找到对应的文件,得到一个目录,在引入自定义模块个逐个模块进行路径查找时经常出现,node会将目录当作一个包来处理。
- 首先,node在当前目录下查找
package.json
,通过json.parse
解析出包描述对象,从中取出main
属性执行的文件名进行定位,如果文件缺少扩展名,将会进入扩展名分析的步骤。- 如果
main
属性指定的文件名错误,或者没有package.json
文件,node会将index
当作默认文件名,依次查找index.js
,index.json
,index.node
。
4. 包名形式的文件模块: 模块路径是Node在定位文件模块的具体文件时的策略,表现为一个路径组成的数组。模块路径的生成规则为:
- 当前文件目录下的node_modules目录
- 父目录下的node_modules目录
- 父目录的父目录下的node_modules目录
- 沿路径逐级向上递归,直到根目录下的node_modules目录
在个目录下查找文件及文件夹,如果模块路径数组被遍历完毕,依然没有查找到目标文件,则抛出查找失败的异常。
模块编译
结合源码分析 Node.js 模块加载与运行原理
定位到具体的文件后,node会新建一个模块对象,根据路径载入并编译。对于不同文件的扩展名,载入方法有所不同。
-
.js
文件,通过fs
模块同步读取文件后编译执行 -
.node
文件,用C/C++编写的扩展文件,通过dlopen()
方法加载最后编译的文件 -
.json
文件 通过fs
模块同步读取文件后,用JSON.parse
解析返回结果
js模块的编译
在编译过程中,node对获取的js文件内容进行包装,这样每个模块文件之间进行了作用域隔离,包装之后的代码会通过
vm
原生模块runInThisContext
方法执行(具有明确上下文,不污染全局),返回一个具体的function
对象,将当前模块对象的exports
属性,require
方法,module
以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function
执行。
(function(exports, require, module, __filename, __dirname) {
var math = require('math')
exports.area = function(radius) {
return Math.PI * radius * radius
}
})
在执行之后,模块的exports
属性被返回,exports
属性上的任何方法和属性都可以被外部调用,模块中的其余变量或属性则不可被直接调用。不能直接赋值exports
,exports
对象通过形参方式传入,直接赋值形参会改变形参的引用,不会改变作用域外的值。
其中,javascript核心模块编译过程为
- 转存为
C/C++
代码
Node
采用了V8
附带的js2c.py
工具,将所有内置的js
代码(src/node.js
和lib/*.js
)转换成C++里的数组,生成node_native.h
头文件。在这个过程中,js代码以字符串的形式存储在node命名空间中,不可直接执行。在启动node进程时,js代码直接加载进内存中。在加载过程中,js核心模块经历标识符分析后直接定位到内存中,比普通文件模块从磁盘中一处一处查找要快得多。
- 编译Javascript 核心模块
在编译过程中,node对获取的js文件内容进行包装,模块文件进行了作用域隔离,包装之后的代码会通过
vm
原生模块runInThisContext
方法执行(具有明确上下文,不污染全局),返回一个具体的function
对象,将当前模块对象的exports
属性,require
方法,module
、__dirname
、filename
作为参数传递给这个function
执行。js核心模块中,源文件通过process.binding('natives')
取出,编译成功的模块缓存到NativeModule._cache
对象上,文件模块则缓存到Module._cache
对象上。
function NativeModule(id) {
this.filename = id + '.js'
this.id = id
this.exports = {}
this.loaded = false
}
NativeModule._source = process.binding('natives')
NativeModule._cache = {}
C/C++模块的编译
node调用process.dlopen()
方法进行加载和执行,node的dlopen
方法在windows
和*.nix
平台下有不同的实现,通过libuv(跨平台的异步I/O库)层进行封装,使用 uv_dlopen
方法打开动态链接库,然后对 C/C++ 模块进行加载。在 *nix 平台下,实际上调用的是 dlfcn.h
中定义的 dlopen()
方法,而在 Windows
下,则为LoadLibraryExW()
方法,在两个平台下,他们加载的分别是 .so
和 .dll
文件,而 Node.js 中,这些文件统一被命名了 .node 后缀,屏蔽了平台的差异。

C/C++核心模块的编译过程
核心模块中,有些模块由C/C++编写,有些模块由C/C++完成核心部分,其他部分由javascript实现或向外导出。由纯C/C++编写的部分称为内建模块,不直接被用户调用。
strcuct node_module_struct {
int version
void *dso_handle
const char *filename
void (*register_func)(v8::Handle<v8::Object> target)
const char *modname
}
1. 每个内建模块在定义之后,都通过
NODE_MODULE
宏将模块定义到node命名空间中,模块的具体初始化方法挂载为结构的register_func
成员。
2. node_extensions.h将这些散列的内建模块同一放入
node_module_list
数组中,node通过get_builtin_module()
方法从node_module_list
数组中取出这些模块。
3. node在启动过程中,会生成一个全局变量
process
,并提供binding
方法(实现在src/node.cc
中)加载内建模块,先创建exports
空对象,然后调用get_builtin_module()
方法取出内建模块对象,通过执行register_func()
填充exports
对象,最后将exports
对象按模块名缓存,并返回给调用方法完成导出。
核心模块的os的引入流程
require('os')
|
\|/
NativeModule.require('os')
|
\|/
process.binding('os')
|
\|/
get_builtin_module('node_os')
|
\|/
NODE_MODULE(node_os, reg_func)
commonjs规范
commonjs规范提出是为了弥补当前js没有标准的缺陷,期望用commonjs API写出的应用程序可以具备跨宿主环境执行的能力,规范涵盖了模块、二进制、buffer、字符集编码、I/O流、进程环境、文件系统、套接字、单元测试、web服务器网关接口,包管理。
- 模块引用
var math = require('math')
-
模块定义
模块中存在一个module
对象,代表模块自身,exports
是module
的属性,exports
对象用于导出当前模块的方法或变量。 -
模块标识
模块标识是传递给require
方法的参数,需要时以小驼峰命名的字符串,或者路径。
网友评论