在一个进程中使用require多次引入一个动态安装的npm包,会怎样?

402 阅读3分钟

先说结论:会报错也有可能不会报错。

背景

前段时间收到这样一个需求,那就是需要动态的帮用户安装一个npm包。我这边采取的思路如下:

  1. 首先使用require.resolve(xxx)检查用户是否已经安装了对应的npm包
  2. 如果没有安装,则帮用户安装一下
  3. 使用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调试源码

  1. 进入到require.resolve函数中,函数内通过调用Module._resolveFilename函数,对npm包进行定位 image.png
  2. Module._resolveFilename函数内使用Module._resolveLookupPaths函数获取npm包可能存在的目录路径列表paths
  3. 根据Module._resolveLookupPaths函数给出的paths,调用Module._findPath函数查找npm包位置
  4. Module._findPath函数中会遍历paths以查找npm包位置,在遍历的过程中使用resolveExports函数,检查当前npm是否一个package.json文件,并获取该文件中的export字段信息

image.png 5. 关键一步:在上一步提及到的resolveExport函数中,有一个readPackage函数;函数内容如下:

image.png 上述代码描述了如下逻辑:

  • 构造package.json文件路径
  • 查找缓存,如果缓存值 不强等于 undefined,直接返回缓存值
  • 如果缓存值不存在,那就根据路径,读取package.json文件内容;之后根据读取到的内容是否强等于falsejson变量进行赋值;如果最终json为一个undefined,那需要更新缓存信息。注意⚠️,这里的缓存值给的是false

注意⚠️ 针对一个不存在的package.json文件,通过packageJsonReader.read函数读取的内容如下: image.png 6. 使用tryPackage函数去获取package.jsonmain属性值。

  • 如果这个值不存在,就会直接尝试查找路径下是否存在index.js/json/node文件,如果能够找到对应的文件,直接返回。
  • 如果这个值存在,使用path.resolve对路径进行拼接,并使用tryFiletryExtensions函数做最后的校验结果并返回结果。 image.png

结论

到此为止,我们大概可以知道,为什么在一个进程中使用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包的根路径下。