深入剖析 npm & yarn 包管理机制

深入剖析 npm & yarn 包管理机制

背景

使用npmyarn管理项目依赖时,可能会产生以下疑问:

  1. 项目依赖出现问题怎么办?删了重装,即先删除node_modules再重新install,那这样的操作会不会存在风险?
  2. 把所有依赖都安装到dependencies中,不区分devDependencies会有问题吗?
  3. 我们的应用依赖了pkg-apkg-b,同时pkg-a也依赖了pkg-b,那么pkg-b会被多次安装或重复打包吗?
  4. 一个项目中,我使用npm别人使用yarn,这会引发什么问题?
  5. 我们是否要提交lockfile(package-lock.json/yarn.lock)到项目仓库呢?
  6. lockfilegit操作时,时常会出现大量的冲突,你是怎么解决的呢?

npm 内部机制和背后的思考

先来看下第一个问题,“删除 node_modules,重新 npm install” 这样解决依赖安装问题百试不爽,其中的原理是什么?这样做存在怎样的风险?下面我们一起探究一下。

npm的安装机制非常值得探究。pip是全局安装,但npm的安装机制秉承了不同的设计哲学。

npm 会优先将依赖包安装到项目目录。 这样做的好处是使不同项目的依赖各成体系,同时还减轻了包作者的 API 压力;缺点也比较明显,如果我们的 repo_a 和 repo_b 都有一个相同的依赖 pkg_c,那么这个公共依赖将在两个项目中各被安装一次。也就是说,同一个依赖可能在我们的电脑上多次安装。

npm install

深入剖析 npm & yarn 包管理机制

上图是 npm 安装依赖大致的过程,其中这样几个步骤需要关注:

  1. 检查配置。包括项目级、用户级、全局级、内置的 .npmrc 文件。
  2. 确定依赖版本,构建依赖树。确定项目依赖版本有两个来源,一是 package.json 文件,一是 lockfile 文件,两个确认版本、构建依赖树的来源,互不可少、相辅相成。如果 package-lock.json 文件存在且符合 package.json 声明的的情况下,直接读取;否则重新确认依赖的版本。
  3. 下载包资源。下载前先确认本地是否存在匹配的缓存版本,如果有就直接使用缓存文件,如果没有就下载并添加到缓存,然后将包按依赖树解压到 node_modules 目录。
  4. 生成 lockfile 文件

可以确认这样几个逻辑:

  1. 构建依赖树的过程中,版本确认需要结合 package.json 和 package-lock.json 两个文件。先确认 package-lock.json 安装版本,符合规则就以此为准,否则由 package.json 声明的版本范围重新确认。特别地,若是在开发中手动更改包信息,会导致lockfile 版本信息异常,也可能由 package.json 确认。确认好的依赖树会存到 package-lock.json 文件中,这里跟 yarn.lock 存在差异。
  2. 同一个依赖,更高版本的包会安装到顶层目录,即 node_modules 目录;否则会分散在某些依赖的 node_modules 目录,如:node_modules/expect-jsx/node_modules/react 目录。
  3. 如果依赖升级,造成版本不兼容,需要多版本共存,那么仍然是将高版本安装到顶层,低版本分散到各级目录。
  4. lockfile 的存在,保证了项目依赖结构的确定性,保障了项目在多环境运行的稳定性。

yarn 安装理念以及破解依赖管理困境

yarn 作为区别于 npm 的依赖管理工具,诞生之初就是为了解决历史上 npm 的某些不足,比如 npm 缺乏对于依赖的完整性和一致性保障,以及 npm 安装速度过慢的问题等,尽管 npm 发展至今,已经在很多方面向 yarn 看齐,但 yarn 的安装理念仍然需要我们关注。yarn 提出的安装理念很好的解决了当时 npm 的依赖管理问题:

  1. 确定性。通过 yarn.lock 等机制,保证了确定性,这里的确定性包括但不限于明确的依赖版本、明确的依赖安装结构等。即在任何机器和环境下,都可以以相同的方式被安装。
  2. 模块扁平化安装。将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余。(npm 也有相同的优化)
  3. 更好的网络性能Yarn 采用了请求排队的理念,类似并发连接池,能够更好地利用网络资源;同时引入了更好的安装失败时的重试机制。(npm 较早的版本是顺序下载,当第一个包完全下载完成后,才会将下载控制权交给下一个包)
  4. 引入缓存机制,实现离线策略。(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"

从上面依赖版本描述的信息中,可以确定以下几点:

  1. 所有依赖,不管是项目声明的依赖,还是依赖的依赖,都是扁平化管理。
  2. 依赖的版本是由所有依赖的版本声明范围确定的,具备相同版本声明范围的依赖归结为一类,确定一个该范围下的依赖版本。如果同一个依赖多个版本共存,那么会并列归类。
  3. 每个依赖确定的版本中,是由以下几项构成:
    1. 多个依赖的声明版本,且符合 semver 规范;
    2. 确定的版本号 version 字段;
    3. 版本的完整性验证字段
    4. 依赖列表
  4. 相比 npm,Yarn 一个显著区别是 yarn.lock 中子依赖的版本号不是固定版本。 也就是说单独一个 yarn.lock 确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。

yarn install

以下是在yarn安装依赖时的步骤:

 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 版本安装依赖的结构

[email protected] 依赖 [email protected],npm v3 是扁平化管理依赖。

如果破解依赖管理困境

场景二:不同 npm 版本处理依赖多版本共存问题

在场景一的基础上,安装 [email protected],而它依赖另一个版本的 [email protected] [email protected] 的依赖,npm v3 会把 [email protected] 安装到 [email protected] 依赖的 node_modules 目录。

不同 npm 版本处理依赖多版本共存问题

靓仔疑惑:为什么 [email protected] 在顶级,而 [email protected] 在子级呢?

场景三:依赖的多版本的数量与依赖版本分布关系

在场景二的基础上,安装 [email protected],而它也依赖 [email protected],由于根目录下已存在 [email protected] 的依赖,npm v3 会把 [email protected] 安装到 [email protected] 依赖的 node_modules 目录。

依赖的多版本的数量与依赖版本分布关系

靓仔疑惑:你可能会疑问,此时存在2个 [email protected] 和1个 [email protected],出现在顶级安装目录的不应该是 v2 版本而非 v1 版本嘛?

其实这是由依赖的安装顺序决定的,真就是依赖的某个版本如果出现在合适的时间,那么它就会被安装到顶级 node_modules 目录。不同版本的出场顺序导致依赖结构的差异,npm v3 注定不是稳定的包管理工具。跟生活一样,人物的出场顺序很重要,它决定了你在哪里做什么事。

场景四:依赖版本存在重复和可用

在场景三的基础上,安装 [email protected],它依赖 [email protected],因此 npm v3 会跳过该依赖的安装。

依赖版本存在重复和可用

场景五:版本升级囧境在

场景三的基础上,如果更新了 [email protected],同时它的依赖是 [email protected] npm v3 的执行顺序是,删除 [email protected],安装 [email protected],安装 [email protected],留下了 [email protected] 在顶层目录,因此 [email protected] 会安装到其父依赖的 node_modules 目录。

版本升级囧境

场景六:依赖版本多目录存在且符合复用条件

在场景五的基础上,更新 [email protected],它依赖了 [email protected] npm v3 的执行顺序是,删除 [email protected],安装 [email protected],删除 [email protected],安装 [email protected],于是出现以下结构。

依赖版本多目录存在且符合复用条件

此时你会发现,存在多个 [email protected] 分布在不同的 node_modules 目录,他们是不是只要在顶级目录存在一份即可?没错,我们删除 node_modules 目录重装,得到的就是你想的清晰的结构。

npm v3

实际上,更优雅的方式是使用 npm dedupe 命令达到上述结构。而 yarn 在安装依赖时会自动执行 dedupe 命令。

正是由于上述一些 npm 历史的坑,所以更建议使用 yarn 作为项目协作的包管理工具。当然 npm 发展至今,很多问题已经优化掉,现在 yarn 和 npm 是两款互相看齐、互相获取灵感的依赖管理工具。

npm 与 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 私服具备以下优点:

  1. 确保高速、稳定的npm服务
  2. 确保发布私有模块的安全性
  3. 审核机制可以保障私服上npm模块质量和安全

部署企业级私服,能够获得安全、稳定、高速的保障。

管理项目依赖的小技巧(集思广益…)

  1. 推荐使用yarn作为团队包管理工具,而不是npm。尽管在npm v6之后的版本趋向稳定和安全,但由于历史原因和团队管理兼容性,仍然是推荐使用yarn作为团队统一的包管理工具。
  2. 项目中一定要存在lockfile文件,且禁止手动修改,因为这是项目稳定性运行的保障。
  3. 如果yarn.lock在代码合并的过程中出现了问题,可以尝试使用yarn install解决问题。

以上文章来源于ELab团队 ,作者ELab.zhangqiang

「点点赞赏,手留余香」

1

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有资源及文章均来源于网络及用户分享或为本站原创,仅限用于学习和研究,任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 深入剖析 npm & yarn 包管理机制

发表评论

IT互联网行业相关广告投放 更专业 更精准

立即查看 联系我们