node require模块加载napi开发的node模块分析(node v15.x源码)
node初学者,工作中涉及了napi模块开发,对于其可直接require的方式引入起了兴趣,因此查阅源码和其他大佬的经验等等,总结记录,目标是尽可能详细,这样之后忘掉了再来看就能少花时间去想了∩( ・ω・)∩
下面是napi插件部分的示例代码
从加载napi的第一步require结构开始查看
node require定义如下:
interface NodeRequire extends NodeJS.Require { }
declare var require: NodeRequire;
它来自nodejs.require,其结构如下
其中id为我们传给require的内容
resolve
resolve 是 require中的一个函数,可以用来查询某个模块的绝对路径
require.resolve(’./myModule.js’);
如果该路径不存在会报cannot find module “./myModule.js”的异常
resolve查询模块过程并不会去加载该模块
在resolve中还有个paths,作用是 返回一个数组,其中包含解析 request 过程中被查询的路径。 如果 request 字符串指向核心模块(例如 http 或 fs),则返回 null。
cache
cache 看名字即可以知道他是个缓存,其中缓存了require的所有的模块(亲测内置模块不会显示),大致结构如下
其中可以看出各module缓存的key为各自的绝对路径,因此可看出模块加载是按照模块存在的路径来判断的,就算是一模一样的模块,只要路径不同就会引入
extensions
extensions是一个已废弃的模块
新增于: v0.3.0
废弃于: v0.10.6
可用于获取模块支持的扩展名,或自定义文件的处理函数,格式如下
结果:
main
当文件直接从 Node.js 运行时,require.main 被设置为其模块。 这意味着可以通过测试 require.main === 模块来直接确定文件是否已运行。
对于 require.main.js
文件,如果通过 node require.main.js
运行则为 true
, 但如果通过 require('./require.main')
运行则为 false
因为module提供了一个filename属性(通常相当于__filename),可以通过检查require.main.filename来获取当前应用的入口点。
require.main结构如下:
require加载模块流程
node加载模块使用的require加载模块,使用的是module.prototype.load方法
在给定的文件路径加载一个模块。 返回该模块的exports
属性。
其中主要为Module._load
1.在module.load中加载模块先通过parent.path和request拼出的relResolveCacheIdentifier,去relativeResolveCache的缓存中查找是否有记录的文件,此处的relativeResolveCache存储的是有父模块的module的缓存
//同一目录中(延迟加载)模块的快速路径。 需要间接缓存以允许缓存失效而不更改旧的缓存键名称。
如果有则Module._cache根据文件的绝对路径去寻找,找到既返回它的exports
这里需注意的updateChildren,会将parent的children更新,可能会导致一些问题,如这位
一行 delete require.cache 引发的内存泄漏血案 - 知乎 (zhihu.com)
relativeResolveCache缓存未命中,则继续进行下一步,
通过Module._resolveFilename获取完整路径
2.以node:开头的优先(原生模块)
3.其次是module._cache缓存中的
4.然后是原生模块
5.如果还是没有,则创建一个module,
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
this.exports = {
};
moduleParentCache.set(this, parent);
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
根据是不是main来确定module.id和process的主模块,
然后存入module._cache,之后根据是否有父模块,来决定是否存入relativeResolveCache
之后进行load,根据是否报错和prototype等判断处理一番之后,返回exports,结束
到此根据流程判断,我们要寻找的.node模块加载,应该是在此处的module.load中进行了处理,我们找到此方法
可以看到在其中先是获取了文件名,路径,扩展名,然后Module._extensions根据扩展名传入this指针和filename执行相应的处理函数,此处的extensions既为require.extensions中的三类[js,json,node]
处理函数执行完,将loaded状态置为true,这之后的esmloader,似乎是esm和commonjs的一些处理
不同类型模块加载
下面看下不同类型模块加载,首先是js,判断扩展名等一系列条件,全部通过后,以utf8的方式读取文件内容,
通过module._compile来返回[exports, require, module, filename, dirname]等,截取的部分代码如下
json格式则反序列化,直接导出
然后是node模块,主要部分是process.dlopen,
DlOpen
我们接下来查看一下dlopen的实现
除去方法前边的一些异常判断,主要部分从node::Utf8Value filename(env->isolate(), args[1]);开始,这里取出了传入的filename,然后调用tryloadAddon,传入了filename,默认flags,和一个以dlib为参数的匿名函数,
我们先看一下tryloadAddon实现
inline void Environment::TryLoadAddon(
const