Skip to content

在pnpm中使用NPM镜像

2024年11月23日

上一篇文章中,我为了解决网络阻断的问题,搭建了一个NPM代理服务器。文章发布后,收到了不少反馈,其中有一条是关于是否可以在pnpm中直接使用国内镜像。

首先明确一下问题背景与结论:

  • 在使用NPM安装包的时候,如果遇到网络阻断问题,可以通过配置NPM镜像来下载依赖,但是有可能影响CI环境以及与他人的协作
  • 在pnpm中,可以直接配置使用任意NPM镜像,而不会影响CI环境和他人协作

下面我们来详细看看来龙去脉。

npm pnpm和它们的lock文件

npm在安装包的时候,会生成一个package-lock.json文件,里面记录了每个包的版本号和下载地址,除此之外还记录了一个integrity字段,这个字段是一个哈希值,用来校验包的完整性。我们摘录package-lock.json文件中的一段内容:

json
"node_modules/is-number": {
    "version": "6.0.0",
    "resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz",
    "integrity": "sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==",
    "engines": {
        "node": ">=0.10.0"
    }
},
"node_modules/is-number": {
    "version": "6.0.0",
    "resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz",
    "integrity": "sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==",
    "engines": {
        "node": ">=0.10.0"
    }
},

其中resolved字段就是包的下载地址,integrity字段是包的哈希值。

pnpm也有一个类似的文件,叫做pnpm-lock.yaml,它的内容和package-lock.json类似,但是有一点不同:pnpm的lock文件中,并不记录包的下载地址,而是只记录了包的哈希值。我们摘录pnpm-lock.yaml文件中的一段内容:

yaml
/is-number@6.0.0:
  resolution: {integrity: sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==}
  engines: {node: '>=0.10.0'}
  dev: false
/is-number@6.0.0:
  resolution: {integrity: sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==}
  engines: {node: '>=0.10.0'}
  dev: false

可以看到,resolved字段被省略了,只有integrity字段,而且integrity字段的值和package-lock.json中的一样。

lock文件的差异导致的registry镜像问题

首先,因为pnpm的lock文件中没有记录包的下载地址,所以更换registry镜像并不会影响lock文件的内容。这意味着,如果我们在pnpm中使用了国内镜像,那么lock文件中的所有内容都和不使用镜像时一致,不会发生改变。

但npm的表现就不一样了。如果我们在NPM中使用了registry镜像,那么lock文件中的resolved字段就可能被替换成镜像地址。注意这里我说的是可能,因为npm客户端记录的lock文件的resolved字段除了受配置的registry镜像影响,还会受到node_modules目录中已经存在的包以及本机npm缓存的影响。

下面是一个真实的例子:

json
"node_modules/is-number": {
    "version": "6.0.0",
    "resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz",
    "integrity": "sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==",
    "engines": {
    "node": ">=0.10.0"
    }
},
"node_modules/is-odd": {
    "version": "3.0.1",
    "resolved": "https://registry.npmmirror.com/is-odd/-/is-odd-3.0.1.tgz",
    "integrity": "sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==",
    "dependencies": {
    "is-number": "^6.0.0"
    },
    "engines": {
    "node": ">=4"
    }
}
"node_modules/is-number": {
    "version": "6.0.0",
    "resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz",
    "integrity": "sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==",
    "engines": {
    "node": ">=0.10.0"
    }
},
"node_modules/is-odd": {
    "version": "3.0.1",
    "resolved": "https://registry.npmmirror.com/is-odd/-/is-odd-3.0.1.tgz",
    "integrity": "sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==",
    "dependencies": {
    "is-number": "^6.0.0"
    },
    "engines": {
    "node": ">=4"
    }
}

虽然我只配置了registry.npmmirror.com一个源,但在这个lock文件中同时存在registry.npmjs.orgregistry.npmmirror.com两个地址。也就是说,安装同一个包时,不同的机器可能会有不同的resolved字段,无法保证所有机器上生成的lock文件都是一致的。

如果我们将npm的lock文件提交上去,就有可能导致两个问题:

  1. CI服务器会尝试访问这个镜像地址,而不是原始的npm registry地址,这可能会导致CI环境无法正常安装依赖(企业中CI机器一般有严格的网络策略,不会允许访问任意registry镜像)。
  2. 因为不同机器上的lock文件可能不一致,所以在多人协作的情况下,可能会导致不同人生成的lock文件不一致,进而导致冲突。

一些无用小资料

在翻查相关资料的过程中,还发现一些有意思的小细节。

npm的隐藏lock文件

首先是npm的lock文件有3个不同的版本,其中v1是比较老的版本使用,而v2和v3的格式则完全一样,那为什么会有一个v3版本呢?

json
{
  "name": "npm-test",
  "version": "1.0.0",
  "lockfileVersion": 3,
  ...
}
{
  "name": "npm-test",
  "version": "1.0.0",
  "lockfileVersion": 3,
  ...
}

这实际上是npm v7带来的一个新东西,叫“隐藏lock文件”。在npm v7以上的版本中,如果你使用npm安装依赖,它会生成一个node_modules/.package-lock.json文件,注意不是根目录下的package-lock.json文件。这个文件的内容和根目录下的lock文件几乎一样,只是位置不同。这样做的目的是为了减少对node_modules目录的扫描,以提高性能。当满足以下条件时,npm会直接读取隐藏lock文件中的信息,而不对node_modules进行扫描:

  • 隐藏lock文件中所有的包对应的目录都存在
  • 所有包目录中的包都在隐藏lock文件中列出
  • 隐藏lock文件的修改时间不早于所有包目录的修改时间

这3个条件的意思也就是“隐藏lock文件”是在最近一次安装/更新包依赖时被更新的。

因此lockfileVersion: 3的意思也就是“这里有一个隐藏lock文件”。

pnpm对“换源”问题的解决

通过前面的介绍,我们可以知道pnpm对于更换registry镜像是可以无感的,因为它的lock文件中并不记录包的下载地址。但实际上可能是出于对完整性的考虑,pnpm在换源后重新安装包的时候会报错:

shell
ERROR  This modules directory was created using the following registries configuration: {"default":"https://registry.npmjs.org/"}. The current configuration is {"default":"https://registry.npmmirror.com/"}. To recreate the modules directory using the new settings, run "pnpm install".
ERROR  This modules directory was created using the following registries configuration: {"default":"https://registry.npmjs.org/"}. The current configuration is {"default":"https://registry.npmmirror.com/"}. To recreate the modules directory using the new settings, run "pnpm install".

也就是说换源后需要重新使用pnpm install来安装一次依赖。

但是既然pnpm的lock文件并没有记录包的下载地址,它是怎么知道之前这些包是从哪下载的呢?通过查看pnpm的源码,最终找到了答案:pnpm会在node_modules/.modules.yaml中记录一些信息:

yaml
hoistPattern:
  - '*'
hoistedDependencies:
  /is-number/6.0.0:
    is-number: private
included:
  dependencies: true
  devDependencies: true
  optionalDependencies: true
injectedDeps: {}
layoutVersion: 5
nodeLinker: isolated
packageManager: pnpm@8.10.2
pendingBuilds: []
prunedAt: Sat, 23 Nov 2024 04:56:52 GMT
publicHoistPattern:
  - '*eslint*'
  - '*prettier*'
registries:
  default: https://registry.npmjs.org/
skipped: []
storeDir: /Users/toobug/.pnpm-global/store/v3
virtualStoreDir: .pnpm
hoistPattern:
  - '*'
hoistedDependencies:
  /is-number/6.0.0:
    is-number: private
included:
  dependencies: true
  devDependencies: true
  optionalDependencies: true
injectedDeps: {}
layoutVersion: 5
nodeLinker: isolated
packageManager: pnpm@8.10.2
pendingBuilds: []
prunedAt: Sat, 23 Nov 2024 04:56:52 GMT
publicHoistPattern:
  - '*eslint*'
  - '*prettier*'
registries:
  default: https://registry.npmjs.org/
skipped: []
storeDir: /Users/toobug/.pnpm-global/store/v3
virtualStoreDir: .pnpm

这是不是又和npm的隐藏lock文件有点像呢?

结语

pnpm真香!用pnpm解千愁!以及需要认真对维护npm镜像的同学们表示感谢!