一、Nodejs 模块机制
首先在开始之前先简单介绍一下 Nodejs 里面的模块引入机制。
1. Node.js 核心模块
例如 fs、net、path 这样的模块,代码在 nodejs 源码(lib目录下)中,通过 API 来暴露给开发者,这些核心模块都有自己的预留标识,当 require() 函数传入的标识和核心模块相同时,就会返回核心模块的 API。
const fs = require('fs');
2. 文件模块
文件模块则分为两种方式:
2.1 第三方模块
这些模块以 Nodejs 依赖包的形式存在。例如一些常见的 npm 包 axios、webpack等等。
Nodejs require 这一类模块的话是会去找该模块项目下面的 package.json 文件,如果 package.json 文件合法,则会解析 main 字段的那个路径。
当 require() 函数中传入一个第三方模块,例如 axios,那么 Nodejs 对于寻找这个 axios 目录的路径的过程是这样的:
- 去当前文件目录下 node_modules 中找
- 没找到就去当前文件父目录下的 node_modules 中找
- 还没找到就再往上一层
- 还没找到就重复3,直到找到符合的模块或者根目录为止
以一个 monorepo 项目为例子,一般在 monorepo 中一些包管理工具例如 yarn workspace 下会把一些依赖提升到外层的目录中来,那么子项目就是这样去寻找外层的依赖的:
node_modules -> find axios here
packages
package-a
node_modules -> axios not found
index.js -> const axios = require('axios');
2.2 项目模块
在项目中执行 require() 来载入 "/"、"./" 或者 "../" 开头的模块就是项目模块。这里根据相对路径或者绝对路径所指向的模块去进行加载。通过加载模块的时候如果不指定后缀名,Nodejs 则会通过枚举去尝试后缀名。后缀名依次是 .js 、.json 和 .node ,其中 .node后缀的文件就是 C++ 拓展。
例如目录下有个 addon.node 文件,我们可以 require 去加载(nodejs 是默认支持的):
const addon = require('./addon');
二、什么是 Nodejs C++ 拓展
本质
Node.js 是基于 C++ 开发的(底层用 chrome v8 做 js 引擎 && libuv 完成事件循环机制),因此它的所有底层头文件暴露的 API 也都是适用于 C++ 的。
上一节中提到 nodejs 模块寻径的时候会默认找 .node 为后缀名的模块,实际上这是个 C++ 模块的二进制文件,即编译好之后的 C++ 模块,本质上是个动态链接库。例如 (Windows dll/Linux so/Unix dylib)
在 Nodejs 在调用原生的 C++ 函数和调用 C++ 拓展函数的本质区别在于前者的代码会直接编译成 Node.js 可执行文件,而后者则是在动态链接库中。
C++ 拓展加载方式
C++ 拓展的加载过程源码可以参考:
https://github.com/nodejs/node/blob/master/src/node_binding.cc#L415
通过 uv_dlopen 这个方法去加载动态链接库文件来完成
C++ 拓展模块(.node二进制链接库文件)的具体加载过程:
- 在用户首次执行 require 时使用 uv_dlopen 来加载cpp addon 的 .node 链接库文件
- 链接库内部把模块注册函数赋值给 mp
- 将执行 require 时传入的 module 和 exports 两个对象传入模块注册函数(mp 实例)进行导出
相关加载代码参考:
void DLOpen(const FunctionCallbackInfo& args) {
Environment* env = Environment::GetCurrent(args);
uv_lib_t lib;
...
Local