先说结论:会报错也有可能不会报错。
背景
前段时间收到这样一个需求,那就是需要动态的帮用户安装一个npm包。我这边采取的思路如下:
- 首先使用
require.resolve(xxx)检查用户是否已经安装了对应的npm包 - 如果没有安装,则帮用户安装一下
- 使用
require.resolve(xxx)获取npm的主文件位置,并执行。
但是,当我按照这个方案进行开发时,发现了一个问题:正常情况下,当项目中找不到对应的npm包时,使用require.resolve进行引入的时候就会报错,导致程序终止。所以我编写了如下代码:
try {
const filePath = require.resolve('xxx')
} catch(e) {
// 本地未找到npm包,执行安装
await localInstallPkg('xxx', [])
// 重新查找npm包
const filePath = require.resolve('xxx')
}
// 用于安装npm包
function localInstallPkg(pkgName, options) {
return new Promise((resolve, reject) => {
let child = spawn(npmCmdStr, ['install', pkgName, ...options], {
stdio: 'inherit',
cwd
})
child.on('close', code => {
if (code) {
reject(new Error(`安装${pkgName}失败`))
} else {
resolve()
}
})
})
}
当我在执行了本地安装npm包后,再次使用require.resolve对刚安装的npm进行引入时,仍然报错进而导致程序崩溃。
定位问题
主要的问题应当是出在require.resolve函数中,本篇文章不会就require引入模块的查找规则进行详细讲解,感兴趣的可自行查找资料。
通过vscode调试源码:
- 进入到
require.resolve函数中,函数内通过调用Module._resolveFilename函数,对npm包进行定位 Module._resolveFilename函数内使用Module._resolveLookupPaths函数获取npm包可能存在的目录路径列表paths- 根据
Module._resolveLookupPaths函数给出的paths,调用Module._findPath函数查找npm包位置 Module._findPath函数中会遍历paths以查找npm包位置,在遍历的过程中使用resolveExports函数,检查当前npm是否一个package.json文件,并获取该文件中的export字段信息
5. 关键一步:在上一步提及到的
resolveExport函数中,有一个readPackage函数;函数内容如下:
上述代码描述了如下逻辑:
- 构造
package.json文件路径 - 查找缓存,如果缓存值 不强等于
undefined,直接返回缓存值 - 如果缓存值不存在,那就根据路径,读取
package.json文件内容;之后根据读取到的内容是否强等于false对json变量进行赋值;如果最终json为一个undefined,那需要更新缓存信息。注意⚠️,这里的缓存值给的是false
注意⚠️ 针对一个不存在的package.json文件,通过packageJsonReader.read函数读取的内容如下:
6. 使用
tryPackage函数去获取package.json中main属性值。
- 如果这个值不存在,就会直接尝试查找路径下是否存在
index.js/json/node文件,如果能够找到对应的文件,直接返回。 - 如果这个值存在,使用
path.resolve对路径进行拼接,并使用tryFile、tryExtensions函数做最后的校验结果并返回结果。
结论
到此为止,我们大概可以知道,为什么在一个进程中使用require多次引入一个动态安装的npm包有的时候(主文件名不为index,且主文件不在npm包根路径下)会报错;原因就是,在一个进程内,使用readPackage函数读取npm包的package.json文件信息时,当npm包的package.json文件不存在时,会往readPackage函数内使用的缓存变量packageJsonCache存入一个false。当你在一个进程中再次调用该函数时,读取的值就是缓存中的值,也就是false由于
false !== undefined
是成立的,所以直接返回了这里的缓存值false;之后在tryPackage函数中直接去npm包的根路径查找index.js/json/node也无法找到,进而报错,导致程序崩溃。
那什么时候不会报错呢?
npm包的主文件为index.js/json/node,且主文件在npm包的根路径下。