4

简述前端包管理工具机制和相关实践

 1 year ago
source link: https://www.fly63.com/article/detial/11638
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

npm 依赖管理机制

区别于 Python 的包管理工具 pip 的全局安装,npm 会安装依赖包到当前项目目录,使不同项目的依赖更成体系,这样做的好处是减轻了包作者的 api 兼容性压力;但是缺陷是如果两个项目依赖了一个相同的库,一般这个库会在这两个项目中各安装一次,即相同的依赖包会被多次安装。

我们先通过一张流程图(源自掘金)来了解下 npm install 的整体流程

6298186680636.jpg

可以看到执行 npm install 后依次会进行以下流程

  • 检查 package-lock.json

  • 通过和 package.json 对比确定是否远程获取包信息

  • 扁平化构建依赖树

  • 下载包并解压到 node_modules

  • 生成新的 lock 文件 值得注意的是,早期 npm 版本(v5.0 - v5.4)发现 package.json 和 package-lock.json 不一致时,对依赖的安装方式是不一样的。 所以对于团队而言,最佳实践应该是保持 npm 版本的一致性!

我们可以从流程图中看到,npm install 的流程中会查找和使用缓存,以及下载包后会添加缓存的环节。由于依赖嵌套机制,项目中 node_moudles 占用的磁盘空间无疑是最大的,如果安装时每次都通过网络下载获取,那么时间成本是巨大的。常见的优化方式是“空间换时间”,npm 也通过缓存机制来解决这个问题。

简单了解下缓存的目录的和清除机制。

通过 npm config get cache 命令可以查询到缓存目录:默认是用户主目录下的 .npm/_cacache 目录。

npm cache clean --force 即可强制清除缓存。

yarn 带来了什么?

yarn 是于 2016 年诞生的,它的出现解决了历史上 npm 的很多问题,比如缺乏对于依赖的完整性和一致性保障(npm v3 版本还没有 package-lock.json),以及 npm 安装速度过慢的问题等。npm 目前已经迭代到 v8 版本,在很多方面已经借鉴了 yarn 的优点,但是我们不妨了解下 yarn 诞生时带来的理念。

  1. 确定性。通过 yarn.lock 等机制,保证了确定性,这里的确定性包括但不限于明确的依赖版本、明确的依赖安装结构等。即在任何机器和环境下,都可以以相同的方式被安装。

  2. 模块扁平化安装。将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余。

  3. 更快的速度。yarn 采取并行安装的机制进行包的安装任务,提高了性能;yarn 引入的缓存机制使二次安装的速度更快。

  4. 更好的语义化。yarn 的命令更加简洁。 解决早期 npm 的依赖管理问题

文章的开始提到 npm 是将依赖放到项目的 node_modules 中,同时如果 node_modules 中的依赖 A 还依赖了其他依赖 B,那么 B 也会被安装到 A 的 node_modules 文件夹,依次递归最终形成非常复杂和庞大的依赖树。

这种依赖管理方式会随着项目的迭代,node_moudles 会变得越来越复杂,从而造成:

  • 非常深的项目依赖层级,难以排查问题

  • 依赖被重复安装,浪费磁盘,网络等资源,安装速度慢 那么 yarn 是如何解决这个问题的呢?那就是模块扁平化安装机制。假如我们有这样一个文件依赖结构。

App
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]

yarn 在安装依赖时会打平依赖,并对重复依赖进行提升,最终形成的依赖结构如下:

App
[email protected]
[email protected]
[email protected]

但是需要注意的是: 模块的安装顺序可能影响 node_modules 内的文件结构。 在 npm v3 版本中,假如 项目一开始依赖了 [email protected],此时 [email protected] 会被安装在顶层目录;随着迭代,又引入了模块 [email protected],而 [email protected] 又依赖了 [email protected],此时 [email protected] 会被安装在 [email protected] 下,因为顶层已经有一个 [email protected] 了。

pnpm: 最先进的包管理工具?

6298186f35165.jpg

在各个场景下,pnpm 相比较于 npm(v8)和 yarn(v3)在性能上都有不错的提升。

pnpm 之所以有如此大的性能提升,简单来说 pnpm 是通过全局 store(目录 ${os.homedir}/.pnpm-store)来存储 node_modules 依赖的 hard-links,当在项目文件中引用依赖的时候则是通过 symlink 去找到对应虚拟磁盘目录下(.pnpm 目录)的依赖地址。相比于 npm 和 yarn 会在每个项目中都安装一份 node_moudles, pnpm 的全局 store 则实现了“安装一次,所有项目复用”,这样避免了二次安装带来的时间消耗。

除此之外,pnpm 本身的设计机制解决了 monorepo 的很多痛点,比如 ”幽灵依赖“ 和 ”依赖重复安装“ 的问题。如图:
6298187b2036c.jpg

下面两小节内容源自: pnpm: 最先进的包管理工具

Phantom dependencies 被称之为幽灵依赖,解释起来很简单,即某个包没有被安装(package.json 中并没有,但是用户却能够引用到这个包)。

引发这个现象的原因一般是因为 node_modules 结构所导致的,例如使用 yarn 对项目安装依赖,依赖里面有个依赖叫做 foo,foo 这个依赖同时依赖了 bar,yarn 会对安装的 node_modules 做一个扁平化结构的处理(npm v3 之后也是这么做的),会把依赖在 node_modules 下打平,这样相当于 foo 和 bar 出现在同一层级下面。那么根据 nodejs 的寻径原理,用户能 require 到 foo,同样也能 require 到 bar。

package.json -> foo(bar 为 foo 依赖)
node_modules
/foo
/bar -> :ghost:依赖

那么这里这个 bar 就成了一个幽灵依赖,如果某天某个版本的 foo 依赖不再依赖 bar 或者 foo 的版本发生了变化,那么 require bar 的模块部分就会抛错。

依赖重复安装

这个问题其实也可以说是 hoist 导致的,这个问题可能会导致有大量的依赖的被重复安装,举个例子:

例如有个 package,下面依赖有 lib_a、lib_b、lib_c、lib_d,其中 a 和 b 依赖 [email protected],而 c 和 d 依赖 [email protected]

那么早期 npm 的依赖结构应该是这样的:

- package
- package.json
- node_modules
- lib_a
- node_modules <- [email protected]
- lib_b
- node_modules <- [email protected]
_ lib_c
- node_modules <- [email protected]
- lib_d
- node_modules <- [email protected]

这样必然会导致很多依赖被重复安装,于是就有了 hoist 和打平依赖的操作:

- package
- package.json
- node_modules
- [email protected]
- lib_a
- lib_b
_ lib_c
- node_modules <- [email protected]
- lib_d
- node_modules <- [email protected]

但是这样也只能提升一个依赖,如果两个依赖都提升了会导致冲突,这样同样会导致一些不同版本的依赖被重复安装多次,这里就会导致使用 npm 和 yarn 的性能损失。

如果是 pnpm 的话,这里因为依赖始终都是存在 store 目录下的 hard links ,一份不同的依赖始终都只会被安装一次,因此这个是能够被彻彻底底的消除的。

项目中的相关场景实践和常见问题

npm link

适用场景:本地调试 npm 模块,将模块链接到对应的业务项目中运行 使用方法:假如我们需要把模块 pkg-a 链接到主项目 App 中,首先在 pkg-a 根目录中执行 npm link,然后在 App 根目录中执行 npm link pkg-a 即可。调试完可以使用 npm unlink 取消关联。原理:npm link 通过软连接将 pkg-a 链接到 node 模块的全局目录和可执行文件中,实现 npm 包命令的全局可执行。

适用场景:在 npm 5.2.0 版本之后,npm 内置了 npx 的包。npx 是一个简单的 cli 工具,可以帮助我们快速的调试,还可以让我们在不通过 npm 安装包的前提下执行一些 npm 包。

使用方法:

Before:一般情况下,如果我们想使用 es-lint, 会先通过 npm install es-lint, 然后在项目根目录执行 ./node_modules/.bin/es-lint your_file.js 或者 通过 package.json 的 npm scripts 调用 eslint。

After:npx es-lint your_file.js

原理:npx 在运行时会自动去 ./node_moudles/.bin 和 环境变量 寻找命令

是否提交 lock.json 到代码仓库

前面我们提到 yarn 带来了 .lock 文件的机制,使得在任何环境下执行 install,都能得到一致的 node_modules 安装结果。但是是否需要提交 lockfiles(package-lock.json/yarn.lock) 到代码仓库呢?

npm 官方文档是建议把 package-lock.json 文件提交到代码仓库的。在多人协作的项目中,这样做确实没有问题。但是如果开发的是库,在 npm publish 的时候最好忽略 lockfiles。因为库一般是被其他项目依赖的,在不使用 lockfiles 的情况下,由于新版 npm 和 yarn 的 hoist 机制,可以复用住项目已经加载过的包,减少依赖重复和体积。

但是存在这样一种现象:即使在一些发布时忽略 lockfiles 的库中,在主项目顶层存在相关依赖包的前提下,最终生成的 lockfile 仍然没复用主项目的包。这是为什么呢?原因是库的依赖包版本和主项目存在的依赖包版本不一致。具体看下图:主项目的 yarn.lock 中显示 browser 这个包依赖了 @babel/[email protected]

6298189065e33.jpg

主项目 node_modules 顶层的 @babel/runtime 版本为 7.10.1

629818951a178.jpg

知道了原因,那么如何减少库项目的依赖项呢。到这里,解决方案也就呼之欲出了:

  1. 库项目尽量使用和主项目版本一致的依赖包

  2. 在库项目 package.json 的 “peerDevpendencies” 字段中声明主项目已有的依赖包

合入其他分支代码后编译报错

相信很多同学都遇到过和我一样的问题:当自己的 feat 分支代码合入 master 或者业务班车分支的代码时,重新 yarn 时,有时候会编译失败,报大量 "can't resolve module xxx"的错误。这种错误有很多情况是依赖版本不一致的问题,但是又极其难以定位,令人头痛。那么此时有另外一个思路,那就是从 master 拉一个最新的分支再进行合入。

但更好的解决方式是:建议在日常开发过程中,定时合入 master 代码,一方面可以合入最新的 feat,另一方面可以避免长时间不合入,最后在上线阶段合入代码,可能出现大量冲突,解决不当或遗漏而造成的编译问题。同时也可以考虑将工具升级为 pnpm,以解决潜在的“幽灵依赖”和“依赖嵌套”问题,同时带来性能上的提升。

原文 https://mp.weixin.qq.com/s/BK3rTPCGCkY-xII13Y8Aqg

链接: https://www.fly63.com/article/detial/11638


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK