npm & yarn 包管理机制
Posted ur home
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了npm & yarn 包管理机制相关的知识,希望对你有一定的参考价值。
转载至ELab团队 ELab.zhangqiang
原文链接npm & yarn 包管理机制
背景
使用 npm 或 yarn 管理项目依赖时,可能会产生以下疑问:
-
项目依赖出现问题怎么办?删了重装,即先删除 node_modules 再重新 install,那这样的操作会不会存在风险?
-
把所有依赖都安装到 dependencies 中,不区分 devDependencies 会有问题吗?
-
我们的应用依赖了 pkg-a 和 pkg-b,同时 pkg-a 也依赖了 pkg-b,那么 pkg-b 会被多次安装或重复打包吗?
-
一个项目中,我使用 npm 别人使用 yarn,这会引发什么问题?
-
我们是否要提交 lockfile(package-lock.json/yarn.lock) 到项目仓库呢?
-
lockfile 在 git 操作时,时常会出现大量的冲突,你是怎么解决的呢?
npm 内部机制和背后的思考
先来看下第一个问题,“删除 node_modules,重新 npm install” 这样解决依赖安装问题百试不爽,其中的原理是什么?这样做存在怎样的风险?下面我们一起探究一下。
npm 的安装机制非常值得探究。pip 是全局安装,但 npm 的安装机制秉承了不同的设计哲学。
npm 会优先将依赖包安装到项目目录。 这样做的好处是使不同项目的依赖各成体系,同时还减轻了包作者的 API 压力;缺点也比较明显,如果我们的 repo_a 和 repo_b 都有一个相同的依赖 pkg_c,那么这个公共依赖将在两个项目中各被安装一次。也就是说,同一个依赖可能在我们的电脑上多次安装。
npm install
上图是 npm 安装依赖大致的过程,其中这样几个步骤需要关注:
-
检查配置。包括项目级、用户级、全局级、内置的 .npmrc 文件。
-
确定依赖版本,构建依赖树。确定项目依赖版本有两个来源,一是 package.json 文件,一是 lockfile 文件,两个确认版本、构建依赖树的来源,互不可少、相辅相成。如果 package-lock.json 文件存在且符合 package.json 声明的的情况下,直接读取;否则重新确认依赖的版本。
-
下载包资源。下载前先确认本地是否存在匹配的缓存版本,如果有就直接使用缓存文件,如果没有就下载并添加到缓存,然后将包按依赖树解压到 node_modules 目录。
-
生成 lockfile 文件。
可以确认这样几个逻辑:
-
构建依赖树的过程中,版本确认需要结合 package.json 和 package-lock.json 两个文件。先确认 package-lock.json 安装版本,符合规则就以此为准,否则由 package.json 声明的版本范围重新确认。特别地,若是在开发中手动更改包信息,会导致lockfile 版本信息异常,也可能由 package.json 确认。确认好的依赖树会存到 package-lock.json 文件中,这里跟 yarn.lock 存在差异。
-
同一个依赖,更高版本的包会安装到顶层目录,即 node_modules 目录;否则会分散在某些依赖的 node_modules 目录,如:node_modules/expect-jsx/node_modules/react 目录。
-
如果依赖升级,造成版本不兼容,需要多版本共存,那么仍然是将高版本安装到顶层,低版本分散到各级目录。
-
lockfile 的存在,保证了项目依赖结构的确定性,保障了项目在多环境运行的稳定性。
-
...
yarn 安装理念以及破解依赖管理困境
yarn 作为区别于 npm 的依赖管理工具,诞生之初就是为了解决历史上 npm 的某些不足,比如 npm 缺乏对于依赖的完整性和一致性保障,以及 npm 安装速度过慢的问题等,尽管 npm 发展至今,已经在很多方面向 yarn 看齐,但 yarn 的安装理念仍然需要我们关注。yarn 提出的安装理念很好的解决了当时 npm 的依赖管理问题:
-
确定性。通过 yarn.lock 等机制,保证了确定性,这里的确定性包括但不限于明确的依赖版本、明确的依赖安装结构等。即在任何机器和环境下,都可以以相同的方式被安装。
-
模块扁平化安装。将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余。(npm 也有相同的优化)
-
更好的网络性能。Yarn 采用了请求排队的理念,类似并发连接池,能够更好地利用网络资源;同时引入了更好的安装失败时的重试机制。(npm 较早的版本是顺序下载,当第一个包完全下载完成后,才会将下载控制权交给下一个包)
-
引入缓存机制,实现离线策略。(npm 也有类似的优化)
yarn.lock 文件结构
以 react 等依赖为例,先大致了解一下 yarn.lock 文件的结构以及确定依赖版本的方式:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
expect-jsx@^5.0.0:
version "5.0.0"
resolved "[http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7](http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7 "http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7")"
integrity sha1-YXYbQzZfKFqA6ygMeF4Hg7vjYsc=
dependencies:
collapse-white-space "^1.0.0"
react "^16.0.0"
react-element-to-jsx-string "^13.0.0"
react-rater@^6.0.0:
version "6.0.0"
resolved "[http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927](http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927 "http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927")"
integrity sha512-NP1+rEeL3LyJqA5xF7U2fSHpISMcVeMgbQ0u/P1WmayiHccI7Ixx5GohygmJY82g7SxdJnIun2OOB6z8WTExmg==
dependencies:
prop-types "^15.7.2"
react "^16.8.0"
react-dom "^16.8.0"
//一或多个具有相同版本范围的依赖声明,确定一个可用的版本。这就是 lockfile 的确定性。
react@^16.0.0, react@^16.8.0:
version "16.14.0"
resolved "[http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d](http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d "http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d")"
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
//如果同一个依赖存在多个版本,那么最高版本安装在顶层目录,即 node_modules 目录。
react@^17.0.1:
version "17.0.2"
resolved "[http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037](http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037 "http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037")"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
从上面依赖版本描述的信息中,可以确定以下几点:
-
所有依赖,不管是项目声明的依赖,还是依赖的依赖,都是扁平化管理。
-
依赖的版本是由所有依赖的版本声明范围确定的,具备相同版本声明范围的依赖归结为一类,确定一个该范围下的依赖版本。如果同一个依赖多个版本共存,那么会并列归类。
-
每个依赖确定的版本中,是由以下几项构成:
-
多个依赖的声明版本,且符合 semver 规范;
-
确定的版本号 version 字段;
-
版本的完整性验证字段
-
依赖列表
-
-
相比 npm,Yarn 一个显著区别是 yarn.lock 中子依赖的版本号不是固定版本。 也就是说单独一个 yarn.lock 确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。
yarn install
以下是在 yarn 安装依赖时的步骤:
1、检查(checking)主要是检查项目中是否存在一些 npm 相关的配置文件,如 package-lock.json 等。如果存在,可能会警告提示,因为它们可能会存在冲突。在这一阶段,也会检查系统 OS、CPU 等信息。
2、解析包(resolving packages)这一步主要是解析依赖树,确定版本信息等。首先获取项目 package.json 中声明的首层依赖,包括 dependencies, devDependencies, optionalDependencies 声明的依赖。接着采用遍历首层依赖的方式获取依赖包的版本信息,以及递归查找每个依赖下嵌套依赖的版本信息,并将解析过和正在解析的包用一个 Set 数据结构来存储,这样就能保证同一个版本范围内的包不会被重复解析。
-
对于没有解析过的包,首次尝试从 yarn.lock 中获取到版本信息,并标记为已解析;
-
如果在 yarn.lock 中没有找到包,则向 Registry 发起请求获取满足版本范围的已知最高版本的包信息,获取后将当前包标记为已解析。
总之,在经过复杂的解析算法后,我们就确定了所有依赖的具体版本信息以及下载地址。
3、获取包(fetching packages)这一步主要是利用系统缓存,到缓存中找到具体的包资源。首先会尝试在缓存中查找依赖包,如果没有命中缓存,则将依赖包下载到缓存中。对于没有命中缓存的包,Yarn 会维护一个 fetch 队列,按照规则进行网络请求。这里也是 yarn 诞生之初解决 npm v3 安装缓慢问题的优化点,支持并行下载。
如何判断有没有命中缓存?
判断系统中存在符合 "cachefolder+slug+node_modules+pkg.name" 规则的路径,如果存在则判断为命中缓存,否则就会重新下载。值得注意的是,不同版本的包在缓存中是扁平化管理。以下是缓存中 webpack 的依赖缓存,可以通过 yarn cache dir 查看。
4、链接包(linking dependencies)这一步主要是将缓存中的依赖,复制到项目目录下,同时遵循扁平化原则。前面说到,npm 优先将依赖安装到项目目录,因此需要将全局缓存中的依赖复制到项目。在复制依赖前,Yarn 会先解析 peerDependencies,如果找不到符合 peerDependencies 声明的依赖版本,则进行 warning 提示(这并不会影响命令执行),并最终拷贝依赖到项目中。
5、构建包(building fresh package)如果依赖包中存在二进制包需要进行编译,会在这一步进行。
如果破解依赖管理困境
在 npm v2 时期,安装的依赖会存在于引用依赖的 node_modules 目录,如果依赖过多,会形成一颗巨大的依赖树。这种结构虽然简单明了,但是对于大型项目十分不友好。依赖层级深对开发排查不利,并且依赖的复用也是问题。在 npm v3 中引入扁平化的概念。看几个场景的例子🌰:
场景一:不同 npm 版本安装依赖的结构
pkg-a@1.0.0 依赖 pkg-b@1.0.0,npm v3 是扁平化管理依赖。
场景二:不同 npm 版本处理依赖多版本共存问题
在场景一的基础上,安装 pkg-c@1.0.0,而它依赖另一个版本的 pgk-b@2.0.0。由于根目录下已存在 pkg-b@1.0.0 的依赖,npm v3 会把 pkg-b@2.0.0 安装到 pkg-c@1.0.0 依赖的 node_modules 目录。
靓仔疑惑:为什么 pkg-b@1.0.0 在顶级,而 pkg-b@2.0.0 在子级呢?
场景三:依赖的多版本的数量与依赖版本分布关系
在场景二的基础上,安装 pkg-d@1.0.0,而它也依赖 pkg-b@2.0.0。同样的,由于根目录下已存在 pkg-b@1.0.0 的依赖,npm v3 会把 pkg-b@2.0.0 安装到 pkg-d@1.0.0 依赖的 node_modules 目录。
靓仔疑惑:你可能会疑问,此时存在2个 pkg-b@2.0.0 和1个 pkg-b@1.0.0,出现在顶级安装目录的不应该是 v2 版本而非 v1 版本嘛?
其实这是由依赖的安装顺序决定的,真就是依赖的某个版本如果出现在合适的时间,那么它就会被安装到顶级 node_modules 目录。不同版本的出场顺序导致依赖结构的差异,npm v3 注定不是稳定的包管理工具。跟生活一样,人物的出场顺序很重要,它决定了你在哪里做什么事。
场景四:依赖版本存在重复和可用
在场景三的基础上,安装 pkg-e@1.0.0,它依赖 pkg-b@1.0.0。由于顶级目录已存在目标版本,因此 npm v3 会跳过该依赖的安装。
场景五:版本升级囧境在
场景三的基础上,如果更新了 pkg-a@2.0.0,同时它的依赖是 pkg-b@2.0.0。那么 npm v3 的执行顺序是,删除 pkg-a@1.0.0,安装 pkg-a@2.0.0,安装 pkg-b@2.0.0,留下了 pkg-b@1.0.0 在顶层目录,因此 pkg-b@2.0.0 会安装到其父依赖的 node_modules 目录。
场景六:依赖版本多目录存在且符合复用条件
在场景五的基础上,更新 pkg-e@2.0.0,它依赖了 pkg-b@2.0.0。那么 npm v3 的执行顺序是,删除 pkg-a@1.0.0,安装 pkg-e@2.0.0,删除 pkg-b@1.0.0,安装 pkg-b@2.0.0,于是出现以下结构。
此时你会发现,存在多个 pkg-b@2.0.0 分布在不同的 node_modules 目录,他们是不是只要在顶级目录存在一份即可?没错,我们删除 node_modules 目录重装,得到的就是你想的清晰的结构。
实际上,更优雅的方式是使用 npm dedupe 命令达到上述结构。而 yarn 在安装依赖时会自动执行 dedupe 命令。
正是由于上述一些 npm 历史的坑,所以更建议使用 yarn 作为项目协作的包管理工具。当然 npm 发展至今,很多问题已经优化掉,现在 yarn 和 npm 是两款互相看齐、互相获取灵感的依赖管理工具。
npm vs. yarn 💪
这里简单对比 npm v6 和 yarn v1. 这是我们生产开发常用的版本。
npm 和 yarn 作为两款相似的包管理工具,在一些功能实现上它们互相获取灵感。
相同点:
-
package.json 作为项目依赖描述文件。
-
node_modules 作为依赖存储目录,yarn v2 不再是这样。
-
lockfile 锁定版本依赖,在 yarn 中叫 yarn.lock,在 npm 中叫 package-lock.json,在 npm v7 也支持了 yarn.lock。它确保在不同机器或不同环境中,能够得到稳定的 node_modules 目录结构。
差异:
-
依赖管理策略。
-
lockfile。package-lock.json 自带版本锁定+依赖结构,你想改动一些依赖,可能影响的范围要比表面看起来的复杂的多;而 yarn.lock 自带版本锁定,并没有确定的依赖结构,使用 yarn 管理项目依赖,需要 package.json + yarn.lock 共同确定依赖的结构。
-
性能。(对比 npm v6 和 yarn v1)目前 npm v7 优化了缓存和下载网络策略,性能的差异在缩小。
[拓展] npm 企业级部署私服原理
npm 中的源(registry),其实就是一个查询服务。以 npmjs.org 为例,它的查询服务网址是 https://registry.npmjs.org/ ,在这个网址后加上依赖的名字,就会得到一个 JSON 对象,里面包含了依赖所有的信息。例如:
-
https://registry.npmjs.org/react
-
https://registry.npm.taobao.org/react
我们可以通过 npm config set registry 命令来设置安装源。你知道我们公司为什么要部署私有的 npm 镜像吗?虽然 npm 并没有被屏蔽,但是下载第三方依赖包的速度依然较缓慢,这严重影响 CI/CD 流程或本地开发效率。通常我们认为部署 npm 私服具备以下优点:
-
确保高速、稳定的 npm 服务
-
确保发布私有模块的安全性
-
审核机制可以保障私服上 npm 模块质量和安全
部署企业级私服,能够获得安全、稳定、高速的保障。
管理项目依赖的小技巧(集思广益...)
-
推荐使用 yarn 作为团队包管理工具,而不是 npm。尽管在 npm v6 之后的版本趋向稳定和安全,但由于历史原因和团队管理兼容性,仍然是推荐使用 yarn 作为团队统一的包管理工具。
-
项目中一定要存在 lockfile 文件,且禁止手动修改,因为这是项目稳定性运行的保障。
-
如果 yarn.lock 在代码合并的过程中出现了问题,可以尝试使用 yarn install 解决问题。
-
...
参考资料
-
npm install[1]
-
yarn install[2]
-
yarn.lock[3]
-
NPM vs. Yarn: Which Package Manager Should You Choose?[4]
-
Lockfiles should be committed on all projects[5]
参考资料
[1]npm install: https://docs.npmjs.com/cli/v7/commands/npm-install
[2]yarn install: https://classic.yarnpkg.com/en/docs/cli/install
[3]yarn.lock: https://classic.yarnpkg.com/en/docs/yarn-lock
[4]NPM vs. Yarn: Which Package Manager Should You Choose?: https://www.whitesourcesoftware.com/free-developer-tools/blog/npm-vs-yarn-which-should-you-choose/
[5]Lockfiles should be committed on all projects: https://classic.yarnpkg.com/blog/2016/11/24/lockfiles-for-all/
以上是关于npm & yarn 包管理机制的主要内容,如果未能解决你的问题,请参考以下文章