前言
在编写 MinaPlay 的插件系统的时候遇到了这样的情况,记一下 Node.js module resolve hook 的使用心得。 resolve hook 用在你想要改变 Node.js 导入模块时指定模块路径对应的物理文件位置的时候,听起来可能有些拗口,举个简单的例子:
首先编写了一段模块导入代码:
import lodash from 'lodash';
TYPESCRIPT
Node.js 的默认导入机制在大多情况下都能够加载用户想要的包,但如果我想手动改指明 Node.js 该在哪个物理路径下查找包呢?
比如我想把导入的 lodash
解析为 my-repo/packages/lodash
这个路径而不是加载 node_modules
中的 lodash
。
这个时候 Node.js 的 resolve hook 就派上用场了。
截至这篇 BLOG 发文时间,resolve hook 相关 API 还处于 RC(Release candidate) 阶段,请考虑谨慎使用在生产环境。
在 Node.js 的 node:module
API 出现之前,开发者们可能更习惯用 Node.js module
loaders 来做类似的实现。
在 Node.js 的最近版本将 module loaders 相关特性整合到了 node:module
API 中。
用法
首先需要编写一个 hook.ts
代码文件。
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
export async function resolve(specifier: string, context: object, nextResolve: Function) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const base = pathToFileURL(path.join(__dirname, './my-repo/packages/lodash'));
return nextResolve(specifier.replace('lodash', base.href), context);
}
TYPESCRIPT
之后在 import lodash from 'lodash'
语句执行之前注册此 hook。
import { register } from 'node:module';
function registerImportMap() {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
register(pathToFileURL(path.join(__dirname, './hook.js')));
}
TYPESCRIPT
然后所有的 lodash
导入都会映射到 my-repo/packages/lodash
路径下啦。
原理
resolve(specifier, context, nextResolve)
这个函数代表了用户对模块加载解析的自定义实现,其三个参数分别为:
- specifier - 导入标志。
- context - 导入时的上下文,包括 import attributes 和父模块路径等信息。
- nextResolve - 下一个 resolve hook 的
resolve
函数。
用户可以同时注册多个 hook,Node.js 会以类似于 Express 中间件的方式依次解析,直到最后一个 nextResolve
使用 Node.js
自身的模块解析函数。
import { register } from 'node:module';
register('./foo.mjs', import.meta.url);
register('./bar.mjs', import.meta.url);
await import('./my-app.mjs');
TYPESCRIPT
这种情况下的模块解析顺序是 bar.mjs
-> foo.mjs
-> Node.js 默认解析器
。
延申
前端领域的模块导入路径映射语法 <script type="importmap">
已经在 2023 年被所有主流浏览器支持了,它的使用要简单得多:
<script type="importmap">
{
"imports": {
"lodash": "./my-repo/packages/lodash/index.js"
}
}
</script>
HTML
具体的使用方式可以参考 MDN 。