详解node中引入模块的原理
Posted wenjinhua
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解node中引入模块的原理相关的知识,希望对你有一定的参考价值。
1. 模块机制
- 1.1 commonjs规范
1.2 node的模块实现(node中引入模块的过程)
- 1.2.1 优先从缓存中加载
- 1.2.2 路径分析
- 1.2.3 文件定位
- 1.2.4 模块编译
1.3 核心模块
- 1.3.1 js核心模块的编译过程
- 1.3.2 c/c++核心模块的编译过程
- 1.3.3 核心模块的引入流程
- 1.4 c/c++扩展模块
1. 模块机制
1.1 commonjs规范
- CommonJS规范为javascript制定了一个美好的愿景——希望JavaScript能够在任何地方运行。
- CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。
- 模块引用:通过require方法引入模块
var math = require(\'math\');
- 模块定义:通过exports对象导出当前模块的方法或变量(exports是module的属性)
exports.add = function() {}
模块标识:就是传递给require方法的参数,可以是以下几种形式(可以没有文件后缀名.js):
- 符合小驼峰命名的字符串
- 以.、..开头的相对路径
- 绝对路径
1.2 node的模块实现(node中引入模块的过程)
node在实现中并非完全按照connonjs规范,而是对模块规范进行了一定的取舍,同时增加了少许自身的特性。
1. 在node中引入模块需要经历如下3个步骤:
- 路径分析
- 文件定位
- 编译执行
2. node中,模块分为以下两类:
核心模块:node提供的模块
- 核心模块在node源代码编译过程中,编译进了二进制执行文件
- 在node进程启动时,部分核心模块就被直接加载到了内存中(所以这部分核心模块在引入时,文件定位和编译执行这两部分是可以省略的,且在路径分析中优先判断,加载速度最快)
文件模块:用户编写的模块
- 需要在运行时动态加载,需要完整的路径分析、文件定位和编译执行。速度较慢
以下为详细的模块加载过程:
1.2.1 优先从缓存中加载
- node对引入过的模块都会进行缓存,以减少二次引入时的开销(和浏览器缓存的不同之处在于:浏览器仅仅缓存文件,而node缓存的是编译和执行后的对象)
- 对核心模块的缓存检查优先于文件模块
1.2.2 路径分析
因为标识符有几种形式,所以对于不同的标识符,模块的查找和定位有着不同程度的差异。模块分类如下:
- 核心模块:如http、fs、path等
- 以.或..开始的相对路径文件模块
- 以/开始的绝对路径文件模块
- 非路径形式的文件模块,如自定义的文件模块
1. 核心模块
核心模块的优先级仅次于缓存加载,它在node源代码编译过程中被编译成了二进制文件,加载速度最快。
2. 路径形式的文件模块
require会将路径转化为真实路径,并以真实路径作为索引,将编译执行后的结果放到缓存中,以使二次加载更快。
3. 自定义模块
node会逐个尝试模块路径中的路径,直到找到目标文件为止。
- 模块路径概念:模块路径是node在定位文件模块的具体文件时指定的查找策略,具体表现为一个路径组成的数组(可通过module.paths输出)
模块路径的生成规则:
- 当前文件目录下的node_modules目录
- 父目录下的node_modules目录
- 父目录的父目录下的node_modules目录
- 沿路径向上逐级递归,直到根目录下的node_modules目录
1.2.3 文件定位
- 文件扩展名的分析
- 目录和包的处理
1. 文件扩展名的分析:当传递给require的标识符不包括文件扩展名时,node会按照以下次序补足扩展名,依次尝试:
- .js
- .json
- .node
在尝试的过程中,node会调用fs模块同步阻塞式的判断文件是否存在。因为node是单线程的,所以这里是一个会引发性能问题的地方。(解决:在引入.json和.node文件时,加上扩展名)
2. 目录和包的处理:require通过分析文件扩展名后,可能得到的是一个目录,此时node会将目录当做一个包来处理。
- 首先,node在当前目录下查找package.json。通过json.parse解析出包描述对象,从中取出main属性指定的文件名进行分析,若缺少扩展名,则会进入扩展名分析的步骤。
- 若main指定的文件名错误,或者没有package.json,那么node会将index作为文件名。然后依次查找index.js、index.json、index.node
- 如果在目录分析的过程中没有成功定位到任何文件,则自定义模块会进入到下一个模块路径继续查找。若遍历完模块路径后仍然没有,则会抛出查找失败的异常。
1.2.4 模块编译
定位到具体的文件后,node会新建一个模块对象,然后根据路径载入和编译。对于不同的扩展名,其载入方式如下:
- .js文件:通过fs模块同步读取文件后编译执行
- .node文件:这是用c/c++编写的扩展文件,通过dlopen加载最后编译生成的文件
- .json文件:通过fs模块同步读取文件后,用Json.parse解析返回结果
- 其余扩展名文件:都被当做.js文件载入
注:每个编译成功的模块,都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能
1. js模块的编译
node对获取的js文件进行了头尾的包装,包装后的代码会通过vm原生模块的runInThisContext()执行
(function(exports, requiure, module, __filename, __dirname){
...
}
2. c/c++模块的编译
node调用process.dlopen()进行加载和执行,(dlope在windows和*nux平台通过libuv进行了兼容).node模块并不需要编译,因为他是编写c/c++模块之后编译生成的。
3. json文件的编译
- 通过fs模块同步读取到json文件的内容
- 通过JSON.parse()得到对象
- 复制给module.exports
1.3 核心模块
- c/c++编写(存放在node的src目录)
- js编写(存放在lib目录)
1.3.1 js核心模块的编译过程
- 转存为c/c++代码:node通过v8附带的js2c.py工具,将所有内置的js代码转换成c++里的数组,生成node_natives.h头文件。(在这个过程中,js代码以字符串的形式存储在node命名空间中。在启动node进程时,js代码直接加载进内存中)
编译js核心模块:也经历了头尾的包装过程(与文件模块的区别在于:获取源码的方式和缓存执行结果的位置)
- 核心模块从内存中加载
- 编译成功的模块缓存到NativeModule._cache对象; 文件模块则缓存到Module._cache对象
1.3.2 c/c++核心模块的编译过程
- 内建模块:全部由c/c++编写(如buuffer、fs、os等)
- c/c++完成核心部分,js实现封装
1. 内建模块
- 每一个内建模块在定义之后,都通过NODE_MODULE宏将模块定义到node命名空间中。
- nodex_extensions.h头文件将这些散列的内建模块统一放到了node_module_list数组
- node提供了get_builtin_module()从node_module_list中取出内建模块
- 内建模块的导出:通过process.binding()来加载内建模块(process是node在启动时生成的全局变量)
1.3.3 核心模块的引入流程
- NODE_MODULE(node_os, reg_func)
- get_builtin_module(\'node_os\')
- process.binding(\'os\')
- NativeModule.require(\'os\')
- require(\'os\')
1.4 c/c++扩展模块
- 模块编写:
普通的扩展模块和内建模块的区别在于:无需将源代码编译进node,而是通过dlopen()动态加载
- 模块编译:通过gpy工具
模块加载:通过require()加载
对于.node文件使用process.dlopen()加载
- 通过uv_dlopen()打开动态链接库
- 通过uv_dlsym()找到动态链接库中通过NODE_MODULE宏定义的方法地址
(注:以上两个方法都是在libuv库中实现的。在不同的操作系统下分别调用不同的方法来分别加载.node在该操作系统下对应的文件.so和.dll)
以上是关于详解node中引入模块的原理的主要内容,如果未能解决你的问题,请参考以下文章