在pnpm中使用NPM镜像
2024年11月23日
在上一篇文章中,我为了解决网络阻断的问题,搭建了一个NPM代理服务器。文章发布后,收到了不少反馈,其中有一条是关于是否可以在pnpm中直接使用国内镜像。
首先明确一下问题背景与结论:
- 在使用NPM安装包的时候,如果遇到网络阻断问题,可以通过配置NPM镜像来下载依赖,但是有可能影响CI环境以及与他人的协作
- 在pnpm中,可以直接配置使用任意NPM镜像,而不会影响CI环境和他人协作
下面我们来详细看看来龙去脉。
npm pnpm和它们的lock文件
npm在安装包的时候,会生成一个package-lock.json
文件,里面记录了每个包的版本号和下载地址,除此之外还记录了一个integrity
字段,这个字段是一个哈希值,用来校验包的完整性。我们摘录package-lock.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
文件中的一段内容:
/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缓存的影响。
下面是一个真实的例子:
"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.org
和registry.npmmirror.com
两个地址。也就是说,安装同一个包时,不同的机器可能会有不同的resolved
字段,无法保证所有机器上生成的lock文件都是一致的。
如果我们将npm的lock文件提交上去,就有可能导致两个问题:
- CI服务器会尝试访问这个镜像地址,而不是原始的npm registry地址,这可能会导致CI环境无法正常安装依赖(企业中CI机器一般有严格的网络策略,不会允许访问任意registry镜像)。
- 因为不同机器上的lock文件可能不一致,所以在多人协作的情况下,可能会导致不同人生成的lock文件不一致,进而导致冲突。
一些无用小资料
在翻查相关资料的过程中,还发现一些有意思的小细节。
npm的隐藏lock文件
首先是npm的lock文件有3个不同的版本,其中v1是比较老的版本使用,而v2和v3的格式则完全一样,那为什么会有一个v3版本呢?
{
"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在换源后重新安装包的时候会报错:
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
中记录一些信息:
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镜像的同学们表示感谢!