Yarn破圈之旅

45 阅读10分钟

前言

       Yarn 是一个由 Facebook、Google、Exponent 和 Tilde 构建的新的 JavaScript 包管理器。它的出现是为了解决历史上 npm 的某些不足(比如 npm 对于依赖的完整性和一致性保障,以及 npm 安装速度过慢的问题等),虽然 npm 目前经过版本迭代汲取了 Yarn 一些优势特点(比如一致性安装校验算法等),但我们依然有必要关注 Yarn 的思想和理念。 Yarn 和 npm 的关系,有点像当年的 Io.js 和 Node.js,殊途同归,都是为了进一步解放和优化生产力。这里需要说明的是,不管是哪种工具,你应该做的就是全面了解其思想,优劣胸中有数,这样才能驾驭它,为自己的项目架构服务。

        当 npm 还处在 v3 时期时,一个叫作 Yarn 的包管理方案横空出世。2016 年,npm 还没有 package-lock.json 文件,安装速度很慢,稳定性也较差,而 Yarn 的理念很好地解决了以下问题。

  • 确定性:通过 yarn.lock 等机制,保证了确定性。即不管安装顺序如何,相同的依赖关系在任何机器和环境下,都可以以相同的方式被安装。(在 npm v5 之前,没有 package-lock.json 机制,只有默认并不会使用的npm-shrinkwrap.json。) 

  • 更好的语义化: yarn改变了一些npm命令的名称,比如 yarn add/remove,感觉上比 npm 原本的 install/uninstall 要更清晰

  • 采用模块扁平安装模式:将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余(npm 目前也有相同的优化)。

  • 采用缓存机制,实现了离线模式(npm 目前也有类似实现)。

yarn.lock 结构

"@vue/babel-preset-app@^3.5.1":
  version "3.12.1"
  resolved "http://xxx.com/api/npm/npm/@vue/babel-preset-app/download/@vue/babel-preset-app-3.12.1.tgz#24c477052f078f30fdb7735103b14dd1fa2cbfe1"
  integrity sha1-JMR3BS8HjzD9t3NRA7FN0fosv+E=
  dependencies:
    "@babel/helper-module-imports" "^7.0.0"
    "@babel/plugin-proposal-class-properties" "^7.0.0"
    "@babel/plugin-proposal-decorators" "^7.1.0"
    "@babel/plugin-syntax-dynamic-import" "^7.0.0"
    "@babel/plugin-syntax-jsx" "^7.0.0"
    "@babel/plugin-transform-runtime" "^7.4.0"
    "@babel/preset-env" "^7.0.0 < 7.4.0"
    "@babel/runtime" "^7.0.0"
    "@babel/runtime-corejs2" "^7.2.0"
    "@vue/babel-preset-jsx" "^1.0.0"
    babel-plugin-dynamic-import-node "^2.2.0"
    babel-plugin-module-resolver "3.2.0"
    core-js "^2.6.5"

       和 package-lock.json 结构类似,只不过 yarn.lock 并没有使用 JSON 格式,而是采用了一种自定义的标记格式,新的格式仍然保持了较高的可读性。

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

         项目中如果想进行 npm/Yarn 切换,并不是一件麻烦的事情。甚至还有一个专门的 synp 工具,它可以将 yarn.lock 转换为 package-lock.json,反之亦然。

npm与yarn命令对比

npm 内部机制和背后的思考

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

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

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

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 install

检测(checking)→ 解析包(Resolving Packages) → 获取包(Fetching Packages)→ 链接包(Linking Packages)→ 构建包(Building Packages)

**1、**检查(checking)

       主要是检查项目中是否存在一些 npm 相关的配置文件,如 package-lock.json 等。如果存在,可能会警告提示,因为它们可能会存在冲突。在这一阶段,也会检查系统 OS、CPU 等信息。

2、解析包(resolving packages)

首先获取当前项目中 package.json 定义的 dependencies、devDependencies、optionalDependencies 的内容,这属于首层依赖。

       接着采用遍历首层依赖的方式获取依赖包的版本信息,以及递归查找每个依赖下嵌套依赖的版本信息,并将解析过和正在解析的包用一个 Set 数据结构来存储,这样就能保证同一个版本范围内的包不会被重复解析。

  • 对于没有解析过的包 A,首次尝试从 yarn.lock 中获取到版本信息,并标记为已解析;  

  • 如果在 yarn.lock 中没有找到包 A,则向 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-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 vs. yarn 

这里简单对比 npm v6 和 yarn v1. 这是我们生产开发常用的版本。

相同点:

  1. package.json 作为项目依赖描述文件。

  2. node_modules 作为依赖存储目录,yarn v2 不再是这样。

  3. lockfile 锁定版本依赖,在 yarn 中叫 yarn.lock,在 npm 中叫 package-lock.json,在 npm v7 也支持了 yarn.lock。它确保在不同机器或不同环境中,能够得到稳定的 node_modules 目录结构。

差异:

  1. 依赖管理策略。

  2. lockfile。package-lock.json 自带版本锁定+依赖结构,你想改动一些依赖,可能影响的范围要比表面看起来的复杂的多;而 yarn.lock 自带版本锁定,并没有确定的依赖结构,使用 yarn 管理项目依赖,需要 package.json + yarn.lock 共同确定依赖的结构。

  3. 性能。(对比 npm v6 和 yarn v1)目前 npm v7 优化了缓存和下载网络策略,性能的差异在缩小。

npm 企业级部署私服原理

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

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

  1. 确保高速、稳定的 npm 服务

  2. 确保发布私有模块的安全性

  3. 审核机制可以保障私服上 npm 模块质量和安全

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

参考链接:jishuin.proginn.com/p/763bfbd65…

[1]npm install: docs.npmjs.com/cli/v7/comm…

[2]yarn install: classic.yarnpkg.com/en/docs/cli…

[3]yarn.lock: classic.yarnpkg.com/en/docs/yar…

[4] NPM vs. Yarn: Which Package Manager Should You Choose?: www.whitesourcesoftware.com/free-develo…

[5] Lockfiles should be committed on all projects: classic.yarnpkg.com/blog/2016/1…