npm & yarn 包更新机制

相关参考,推荐阅读

https://docs.npmjs.com/

https://classic.yarnpkg.com/en/docs/

https://www.sitepoint.com/yarn-vs-npm/

https://mp.weixin.qq.com/s/Yr2_Jr0wv3yFDEQuGVqdyw

https://docs.npmjs.com/cli/v7/commands/npm-install

https://classic.yarnpkg.com/en/docs/cli/install

https://classic.yarnpkg.com/blog/2016/11/24/lockfiles-for-all/

背景

做为一个Web前端,项目开发时经常会用到 npmyarn。平常只是用于安装项目依赖并没有过多研究内部更新机制及理念,本文在这里为大家介绍一下。

在开始之前我们可以先带着问题去阅读方便我们更深入理解,使用 npmyarn 管理项目依赖时,常遇到的问题:

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

npm 内部机制和背后的思考

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

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

npm install

image.png

上图是 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、一个该范围下的依赖版本。如果同一个依赖多个版本共存,那么会并列归类。

4、每个依赖确定的版本中,是由以下几项构成:

  • 多个依赖的声明版本,且符合 semver 规范;
  • 确定的版本号 version 字段;
  • 版本的完整性验证字段
  • 依赖列表

5、相比 npm,Yarn 一个显著区别是 yarn.lock 中子依赖的版本号不是固定版本。 也就是说单独一个 yarn.lock 确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。

yarn install

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

image.png

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 查看。

image.png

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 是扁平化管理依赖。

image.png

场景二:不同 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 目录。

image.png

靓仔疑惑:为什么 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 目录。

image.png
靓仔疑惑:你可能会疑问,此时存在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 会跳过该依赖的安装。

image.png

场景五:版本升级囧境在

场景三的基础上,如果更新了 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 目录。

image.png

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

在场景五的基础上,更新 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,于是出现以下结构。

image.png

此时你会发现,存在多个 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 优化了缓存和下载网络策略,性能的差异在缩小。
No cache cached Reinstall
Npm 6.13.4 67 seconds 61 seconds 28 seconds
Yarn 1.21.1 57 seconds 29 seconds 1.2 seconds

【拓展】npm 企业级部署私服原理

npm 中的源(registry),其实就是一个查询服务。 以 npmjs.org 为例,它的查询服务网址是 https://registry.npmjs.org/ ,在这个网址后加上依赖的名字,就会得到一个 JSON 对象,里面包含了依赖所有的信息。例如:

我们可以通过 npm config set registry 命令来设置安装源。你知道我们公司为什么要部署私有的 npm 镜像吗?虽然 npm 并没有被屏蔽,但是下载第三方依赖包的速度依然较缓慢,这严重影响 CI/CD 流程或本地开发效率。通常我们认为部署 npm 私服具备以下优点:

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

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

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

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

作者:雨中愚
链接:https://juejin.cn/post/7008168128739803150
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

posted @ 2021-09-15 22:52  雨中愚  阅读(1931)  评论(0编辑  收藏  举报