当我们谈论NodeJS时,我们在谈论什么(二)

375 阅读19分钟

pnpm 是凭什么对 npm 和 yarn 降维打击的

大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。
那具体好在哪里呢? 我们一起来看一下。

我们按照包管理工具的发展历史,从 npm2 开始讲起:

npm2

用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。

然后找个目录,执行下 npm init -y,快速创建个 package.json。

然后执行 npm install express,那么 express 包和它的依赖都会被下载下来:

展开 express,它也有 node_modules:

再展开几层,每个依赖都有自己的 node_modules:

也就是说 npm2 的 node_modules 是嵌套的。

这很正常呀?有什么不对么?

这样其实是有问题的,多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。

这个还不是最大的问题,致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。

当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:

yarn

yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢?

铺平。所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。

我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express:

这时候 node_modules 就是这样了:

全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:

当然也有的包还是有 node_modules 的,比如这样:

为什么还有嵌套呢?

因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。

npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似:

当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。

yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么?

并不是,扁平化的方案也有相应的问题。

最主要的一个问题是幽灵依赖,也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。

这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。

但是这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了。

这就是幽灵依赖的问题。

而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。

那社区有没有解决这俩问题的思路呢?

当然有,这不是 pnpm 就出来了嘛。

那 pnpm 是怎么解决这俩问题的呢?

pnpm

回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?

那如果不复制呢,比如通过 link。

首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。

如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?

这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。

没错,pnpm 就是通过这种思路来实现的。

再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。

你会发现它打印了这样一句话:

包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。

我们打开 node_modules 看一下:

确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。

展开 .pnpm 看一下:

所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。

比如 .pnpm 下的 expresss,这些都是软链接:

也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。

官方给了一张原理图,配合着看一下就明白了:

这就是 pnpm 的实现原理。

那么回过头来看一下,pnpm 为什么优秀呢?

首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。

其次就是快,因为通过链接的方式而不是复制,自然会快。

这也是它所标榜的优点:

相比 npm2 的优点就是不会进行同样依赖的多次复制。

相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。

这就已经足够优秀了,对 yarn 和 npm 可以说是降维打击。

package-lock.json和pnpm-lock.yaml的作用

pnpm-lock.yaml 文件是 pnpm 包管理器使用的锁定文件。它的主要作用是确保项目中的依赖包版本一致性,避免在不同的环境中因为依赖版本的不同导致问题。具体来说,pnpm-lock.yaml 文件有以下几个重要作用:

主要作用
  1. 锁定依赖版本pnpm-lock.yaml 文件会记录所有项目依赖的确切版本,包括直接依赖和间接依赖。这样可以确保在不同环境中安装依赖时,使用的依赖版本是完全一致的,避免因为版本差异导致的不可预期的问题。
  2. 提高安装速度pnpm-lock.yaml 文件详细记录了依赖关系树,pnpm 可以通过这个文件快速解析和安装依赖,而不需要重新解析 package.json 中的依赖配置,这可以显著提高安装速度。
  3. 保障依赖的一致性:无论是团队开发还是 CI/CD 环境,pnpm-lock.yaml 文件都可以确保每次安装的依赖版本一致,从而保证开发、测试和生产环境的一致性。
文件内容

pnpm-lock.yaml 文件中包含了所有依赖的详细信息,包括:

  • 包名称和版本
  • 包的完整性校验和哈希值
  • 包的依赖关系
  • 包的下载地址
  • 包的元数据(例如 resolution 字段)
示例

以下是一个简化的 pnpm-lock.yaml 文件的示例:

image.png

使用注意事项
  • 不要手动修改pnpm-lock.yaml 文件是由 pnpm 自动生成和管理的,不建议手动修改。任何依赖的变化(如添加、更新或删除)都应该通过 pnpm 命令来完成,这样可以确保 pnpm-lock.yaml 文件的正确性。
  • 版本控制:将 pnpm-lock.yaml 文件提交到版本控制系统(如 Git)中,这样可以确保团队成员和 CI/CD 环境中使用的依赖版本一致。
  • 更新依赖:当需要更新依赖时,可以使用 pnpm update 命令,这样 pnpm 会根据 package.json 中的版本范围更新依赖,并相应地更新 pnpm-lock.yaml 文件。

总之,pnpm-lock.yaml 文件是保证项目依赖版本一致性和稳定性的关键文件,对于项目的可靠性和可维护性有重要作用。

package-lock.json 要写进 .gitignore 吗?

答: 不要,package-lock.json最好提交,依赖版本统一化。

当package-lock.json文件产生冲突时,许多人的解决方式都是删掉package-lock.json,再跑一遍npm install。若是如此,package-lock.json存在的意义是啥?

答: package-lock.json会在npm更改node_modules目录树或者package.json时自动生成的。它准确的描述了当前项目npm包的依赖树,并且在随后的安装中会根据package-lock.json来安装,保证是相同的一个依赖树,不考虑这个过程中是否有某个依赖有小版本的更新。

这里有个很重要的点就是,package-lock.json记录的是一个依赖树,而不是你直接在package.json中的依赖项。和直接在package.json中锁死版本不一样的地方在于,package.json中只是锁死了依赖项的版本,而没有锁死依赖项的依赖的版本,这里就是变数的地方。如果不对整个依赖树做锁定,那前后编译出来的应用版本可能是不一样的,有可能开发时能正常工作,而到了线上却不能工作。

所以很明显的package-lock.json是很符合我们的诉求的。我们需要让后面每一次install都是相同的版本,打出来的包都有着相同的依赖,这对于我们项目的稳定性、前后一致性是非常重要的。

如何解决package-lock的冲突呢?

答: 不要试图删除package-lock.json来解决一些问题,这样会破坏package-lock.json的作用。

package-lock是工具自动生成的一个文件内容,对于这种自动生成的文件最好的办法还是交由工具去处理,而不是手工一个一个的去处理产生的冲突。

在开发过程中,合并的时候如何如果出现了冲突,在merge conflicts的阶段,只需要从主分支中checkout去package-lock.json,再以此为基础,重新安装新分支中需要的依赖。

git checkout dev -- package-lock.json;

这样让npm自动的去维护package-lock.json。

我相信这个办法可以很好的解决package-lock.json冲突的问题,并且团队合作中,做merge操作的人可以通过查看package.json的变更知道新安装了哪些依赖包,来重新安装,也可以很好的解决这个问题。

【注】校验package-lock.json的正确性:

在按照上面的步骤解决完package-lock.json的冲突后,code reviewer对package-lock.json的正确性需要做一次校验。将被review的代码拉到本地做一次npm install,检查package-lock.json是否有modified,如果没有modify说明提交的package-lock.json是一份正确的文件。

【注】package-lock.json中的resolved字段会被不同环境中的npm registry改写,这样会导致很多的冲突。所以在经过正确性校验的过程中,可能会因为本地registry的配置问题会导致package-lock.json处于modified状态。所以为了规避这个问题,需要在团队内统一npm registry,可以在项目根目录中使用.npmrc来配置项目级别的registry来进行统一。

话说 npm cache clean –force作用是啥?

答: Node模块的安装过程是这样的:

  1. 发出npm install命令
  2. npm 向 registry 查询模块压缩包的网址
  3. 下载压缩包,存放在~/.npm目录
  4. 解压压缩包到当前项目的node_modules目录

npm cache clean –force 是强制清除~/.npm目录下的压缩包,如果你npm install 报错,不妨试试这个命令。

常见让人迷惑的地方:
  • 比如你检出了一个项目,而你只用了npm i jquery,却发现他把所有的package.json里的依赖都下载下来了,包括你指定的jquery。这时候你要看一下你检出的项目中有没有package-lock.json。问题就在这,当有lock文件时,npm install 的安装会由lock文件中的依赖关系驱动,而此文件一定是包含了package.json文件的所有依赖,所以即使你只指定了安装jquery,他也会把其他的包都下载下来

总结

pnpm 最近经常会听到,可以说是爆火。本文我们梳理了下它爆火的原因:

npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。

npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。

pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。

这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。

pnpm 就是凭借这个对 npm 和 yarn 降维打击的。

ps:要彻底删除一个包,包括从 pnpm 的全局内容寻址存储区中删除,你需要执行以下步骤:

  1. 从项目中删除依赖: 你需要先从所有使用该包的项目中删除这个依赖。
  2. 清理全局内容寻址存储区: 使用 pnpm store prune 命令,这将会清理全局存储区中未被任何项目引用的包。

npn install 命令解析

image.png 该图的流程如下:

  1. 执行 npm install

    • 用户在终端或命令行界面中输入 npm install 命令并执行。
  2. 读取配置文件

    • npm 查看是否有配置文件(如 .npmrc),可能存在于项目目录(局部配置,先找这个)或用户主目录(全局配置,后找这个)中,如果都没找到就会去找npm内置的。
    • npm 根据这些配置文件来决定如何进行安装,例如代理服务器、镜像源等设置。
  3. 解析 package.jsonpackage-lock.json 文件

    • npm 读取并解析项目中的 package.jsonpackage-lock.json 文件来确定要安装的依赖包及版本。
  4. 检查 node_modules 目录和 package-lock.json

    • npm 检查 node_modules 目录和 package-lock.json 文件,确定是否已经存在满足版本要求的依赖包。
    • 如果 package-lock.jsonpackage.json版本不一致,并且 npm 版本是5.4(高版本)及以上,那么将会优先按照 package.json 中记录的版本来安装,并且更新lock文件。
    • package-lock.jsonpackage.json版本一致的话,就会遵循lock文件了
  5. 安装依赖

    • 根据解析出来的依赖信息,npm 开始安装依赖到 node_modules 目录中。
    • 如果在 node_modules 中检查缓存,已经存在符合版本要求的包,则不会重复安装,直接就解压了,没有就走另一条下载包资源=>检查完整性=>添加到缓存=>更新package.lock.json文件=>解压到node_modules的道路。
  6. 生命周期脚本执行

    • 在依赖安装的不同阶段,会执行相关的生命周期脚本,如 preinstallinstallpostinstall 等。
  7. 生成或更新 package-lock.json 文件

    • 在安装过程结束后,npm 会生成或更新 package-lock.json 文件。
    • 这保证了以后在其他环境中运行 npm install 能够安装到相同版本的包。

npmrc配置信息

npmrc
 代码解读
复制代码
registry=http://registry.npmjs.org/
# 定义npm的registry,即npm的包下载源

proxy=http://proxy.example.com:8080/
# 定义npm的代理服务器,用于访问网络

https-proxy=http://proxy.example.com:8080/
# 定义npm的https代理服务器,用于访问网络

strict-ssl=true
# 是否在SSL证书验证错误时退出

cafile=/path/to/cafile.pem
# 定义自定义CA证书文件的路径

user-agent=npm/{npm-version} node/{node-version} {platform}
# 自定义请求头中的User-Agent

save=true
# 安装包时是否自动保存到package.json的dependencies中

save-dev=true
# 安装包时是否自动保存到package.json的devDependencies中

save-exact=true
# 安装包时是否精确保存版本号

engine-strict=true
# 是否在安装时检查依赖的node和npm版本是否符合要求

scripts-prepend-node-path=true
# 是否在运行脚本时自动将node的路径添加到PATH环境变量中

package-lock.json 的作用

  • 这个东西不仅可以锁定版本记录依赖树详细信息,还有如下作用
  1. version 该参数指定了当前包的版本号
  2. resolved 该参数指定了当前包的下载地址
  3. integrity 用于验证包的完整性,是一串哈希值
  4. dev 该参数指定了当前包是一个开发依赖包(参数需要是true)
  5. bin 该参数指定了当前包中可执行文件的路径和名称,也就是说有bin就表示这里有可执行文件
  6. engines 该参数指定了当前包所依赖的Node.js版本范围

package-lock.json 帮我们做了缓存,他会通过 name + version + integrity 信息生成一个唯一的key,这个key能找到对应的index-v5 下的缓存记录 也就是npm cache 文件夹下的

  1. 通过npm config list在终端查找缓存文件在哪

image.png image.png

  1. 通过这个路径进行查找文件

image.png

  • index-v5是一个索引目录,记录content-v2的一个索引或者说是位置,也就是name + version + integrity的一个哈希值。如果lock锁文件内的这三者和index-v5能够对上,就会去content-v2找到你缓存的那个文件。

    • 其实就是把项目中name + version + integrity组成的哈希值的看成一个钥匙就够了,而content-v2则是一个宝箱,index-v5则是一个钥匙孔。他们之前的关系就非常清晰了
  • name + version + integrity 哈希值:这相当于是一个钥匙。在npm中,每个包都有一个唯一的名称(name)、版本号(version),以及一个完整性校验值(integrity),这个校验值通常是一个SHA值,用于确保包的内容没有被篡改。将这三者组合起来,就形成了一个能唯一标识和验证一个包的“钥匙”。
  • content-v2:这个目录可以被看作是一个宝箱。它存储了你电脑上缓存的npm包的实际内容。每个缓存的内容都通过一种散列算法(如SHA-512)生成了一个独特的哈希值。
  • index-v5:这个目录就像一个钥匙孔,或者说是一本索引册。它记录了缓存内容的索引信息,这些信息将包的名字、版本和完整性校验值映射到它们在 content-v2 宝箱中的具体位置。

当运行npm install时,如果package-lock.json中的包信息(即钥匙)与index-v5的索引信息(即钥匙孔)匹配,npm就知道它可以直接使用content-v2中对应的缓存内容(即宝箱里的宝藏),而无需重新下载相同的包。这种机制加快了安装过程,同时确保了安装的一致性和包的完整性。

npm run 命令解析

在vue和react项目中,我们常常用到npm run dev命令来起本地服务,那么npm run dev到底做了什么?

package.json文件中的字段script的每一个属性都是一个自定义的脚本命令,npm run 其实执行了package.json中的script脚本

下面的例子,我们以vue-cli3脚手架搭建的项目为例说明,所以当我们输入命令npm run serve命令,底层相当执行vue-cli-service serve --port 8055命令

// package.json
"scripts": {
    "serve": "vue-cli-service serve --port 8055",
    "build": "vue-cli-service build",
    "build:test": "vue-cli-service build --mode test",
    "build:beta": "vue-cli-service build --mode beta",
    "lint": "vue-cli-service lint",
    "inspect-old": "vue-cli-service inspect --mode production > webpack.inspect.js",
    "inspect": "vue inspect --mode production",
    "inspectwebpack": "vue inspect --mode production > output.js"
}
知识点总结

npm run如果不加任何参数,直接运行,会列出package.json里面所有可以执行的脚本命令(script字段里面的内容)。

npm start可以运行是为了方便开发者使用,npm start会执行scripts里的start字段。 如果没有start字段则执行node server.js。

This runs an arbitrary command specified in the package's "start" property of its "scripts" object. If no "start" property is specified on the "scripts" object, it will run node server.js.

执行原理

使用npm run script执行脚本的时候都会创建一个shell,然后在shell中执行指定的脚本。

这个shell会将当前项目的可执行依赖目录(即node_modules/.bin)添加到环境变量path中,当执行之后再恢复原样。就是说脚本命令中的依赖名会直接找到node_modules/.bin下面的对应脚本,而不需要加上路径,所以scripts字段里面调用命令时不用加上路径,这就避免了全局安装NPM模块

执行顺序
'&' 并行执行顺序,同时执行
"dev":"node test.js & webpack"

'&&'继发顺序,执行前面之后才可以执行后面
"dev":"node test.js && webpack"
顺序钩子
"predev":"node test_one.js",
"dev":"node test_two.js",
"postdev":"node test_three.js"

当执行 npm run dev 的时候默认就会执行

npm run predev && npm run dev && npm run postdev

ps:当package.json和package-lock.json中下载信息不一致,npm不同版处理:

  • v5.0.x:根据package-lock.json下载
  • v5.1.0-v5.4.2:根据package.json下载,然后更新package-lock.json
  • v5.4.2以上:当package.json与package-lock.json版本有兼容,根据package-lock.json下载;两者不兼容根据package.json下载,然后更新package-lock.json;

node-gyp

node-gyp是一个用于构建 Node.js C++ 插件的工具。它可以将 C++ 代码编译成可在 Node.js 环境下运行的本机模块。

Node.js 本身是使用 JavaScript 编写的,但有时候需要使用 C++ 来实现一些性能密集型的任务或者与系统底层交互。这就需要通过node-gyp工具将 C++ 代码编译成 Node.js 可以加载和调用的动态链接库(.node 文件)。

node-gyp提供了一个简单的构建系统,可以自动化地执行以下操作:

  1. 解析 .gyp 或 .gypi 文件来确定项目的构建配置。

  2. 编译 C++ 源代码文件并生成目标文件。

  3. 链接目标文件来生成最终的可执行文件(.node 文件)。

  4. 将生成的可执行文件复制到正确的位置,使其可以被 Node.js 加载和使用。

通过使用node-gyp,开发者可以方便地在 Node.js 中使用 C++ 编写的模块,从而获得更高的性能和更强大的功能。

参考:segmentfault.com/a/119000002…