作者:酷家乐平台前端博客
出处
:https://webfe.kujiale.com/node_modules-yu-bao-guan-li-qi/
身为前端开发的我们应该每天都会接触 node_modules ,但对于 node_modules 的认知是否充分?也许因为包管理器的存在,平时只需要一个 install 命令,可能就不会去过多关注 node_mdouels 本身。
简单而言, node_modules 是为 Node 设计存放依赖的文件夹。一直到今天, node_modules 能满足很多场景的使用,但同时也存在不少缺陷。
从一个常见的版本冲突场景开始切入主题,查看以下依赖关系:
当出现这种情况时,node_modules 下的文件结构是如何组织的?
如果 X 是像 react 这种不支持多版本共存的,可以进行前置的报错、警告以避免多版本同时存在的情况,进而通过项目内指定唯一版本的方式来避免多版本的问题。
但更多地、 X 会是像 lodash 这类支持多版本共存的模块,那此时如何保证应用在运行时、依赖能加载到他们正确版本的子依赖?
npm 通过 Node 加载模块的路径查找算法 和 node_modules 的目录结构 来配合解决这个问题。
Node 的模块(非内置模块)加载(require)算法会遵循以下两点:
有如下文件:
// ~/desk/projects/demo/a.jsconst _ = require('lodash');
那么应用在运行时,将会按如下顺序去寻找 lodash:
利用 require 会先在同级 node_module 里查找依赖的特性,能想到一个很简单的方式,直接在 node_module 维护模块需要的拓扑图即可:
APP - node_modules├── A│ └── node_modules│ └── X@1.0├── B│ └── node_modules│ └── X@2.0
应用在运行时, A 会就近加载 X@1.0 , B 就近加载 X@2.0 ,依赖加载的准确性自然地得到了保证。
但如果此时新增一个依赖了 X@2.0 的 C 模块,node_modules 就会变成下面这样:
APP - node_modules├── A│ └── node_modules│ └── X@1.0├── B│ └── node_modules│ └── X@2.0├── C│ └── node_modules│ └── X@2.0
虽然依赖加载的版本正确性能得到保障,但其中显然是存在着问题:
flat mode 可以认为是基于 nest mode 的一种优化,同时也是当前 npm 采用的方式。该模式同样利用到向上递归查找依赖的特性,不过区别是会将一些公共依赖提升到项目顶层的 node_modules:
# nest mode - npm v2APP - node_modules├── A│ └── node_modules│ └── X@1.0└── B│ └── node_modules│ └── X@1.0├── C│ └── node_modules│ └── X@2.0# ││# ││# \/# flat mode - npm v3APP - node_modules├── X@1.0├── A├── B└── C └── node_modules └── X@2.0
观察两种文件结构, flat 之后 X@1.0 被提升安装到了顶层, A 、 B 目录下不会再安装 X@1.0 的依赖,并且:
这样一来保证正确性的同时,也一定程度上减少了依赖重复的问题。
但这依旧不能完全解决依赖重复的问题,下面的场景无论把 X@1.0 提升或是将 X@2.0 提升都会导致另一个版本出现重复。
当项目的依赖增多的时候,node_modules 下可以有成千上万个文件,除了 node_modules 的体积会被诟病;因为 Node 寻找依赖的特性,会需要遍历大量的文件才能找到正确版本的依赖,性能也会受到影响;此外,大量的依赖导致包管理器在 install 阶段所经历的 I\O 消耗和时间消耗也成了一个新的问题。
这时候就要上一张黑洞图:
哪有什么岁月静好,不过是有人替你负重前行!
flat mode 相比 nest mode 节省了很多的空间,然而也带来了一个隐式依赖的问题。
比如在实际项目中,我们知道 muya 是依赖了 muya-core 的,所以会直接在项目中使用如下的代码:
import { createOSSPostTool } from '@qunhe/muya-core';
我们能直接使用 muya-core 还不用去管理它的版本,表面上看起来很方便,但问题也出在“ 不用去管理它 ”。
所以,推荐的做法是将直接用到的依赖都应该明确在 package.json 中定义,而且这样做了之后,对于编辑器(比如 vscode)的提示也会有优化作用。
早期的时候,yarn 与 npm 的区别是比较大的,当时的 npm 不够完善,缺少很多特性,yarn 的出现弥补了这些缺失。
而现在可能是因为 yarn 或其他优秀包管理器的刺激,npm 已经不断完善了起来,比如 npm7 也能支持 workspaces,甚至做到了对 yarn.lock 的支持。
yarn 同样使用 flat mode 来组织 node_modules 下的依赖文件,优先提升依赖,只有当子依赖的版本和 root 的冲突的时候,才不进行提升的操作。
yarn 有一种更为激进的模式,即 --flat 模式,该模式下 node_modules 里的各个 package 只允许一个版本的存在,当出现版本冲突的时候,需要选择指定一个版本(即通过指定在 resolution 里,强控版本),但这在大型项目中显然行不通,因为第三方库里存在大量的版本冲突问题(仅 webpack 内就存在 160 + 个版本冲突)。
问题:项目中用到的大部分依赖往往都有子依赖,而项目的 package.json 只能管理项目的直接依赖,并不能保证协作时所有依赖的一致性,如何去做到一致性?
不仅要处理好本地 node_modules 的文件组织,包管理器还得保证持续迭代和协同工作时依赖版本的一致性,于是有了 lock 文件。
yarn 和 npm 在初次安装之后都会生成一个 lock 文件,包含所有依赖的版本信息,这样他人根据 lock 文件就能复现出当前 node_modules 的状态。
不过细节上 yarn.lock 与 package-lock.json 还是有一些区别:
yarn.lock :
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.# yarn lockfile v1"@ant-design/colors@^3.1.0": version "3.2.2" resolved "https://registry.npm.taobao.org/@ant-design/colors/download/@ant-design/colors-3.2.2.tgz#5ad43d619e911f3488ebac303d606e66a8423903" integrity sha1-WtQ9YZ6RHzSI66wwPWBuZqhCOQM= dependencies: tinycolor2 "^1.4.1""@ant-design/create-react-context@^0.2.4": version "0.2.4" resolved "https://registry.npm.taobao.org/@ant-design/create-react-context/download/@ant-design/create-react-context-0.2.4.tgz#0fe9adad030350c0c9bb296dd6dcf5a8a36bd425" integrity sha1-D+mtrQMDUMDJuylt1tz1qKNr1CU= dependencies: gud "^1.0.0" warning "^4.0.3""@ant-design/icons-react@~2.0.1": version "2.0.1" resolved "https://registry.npm.taobao.org/@ant-design/icons-react/download/@ant-design/icons-react-2.0.1.tgz#17a2513571ab317aca2927e58cea25dd31e536fb" integrity sha1-F6JRNXGrMXrKKSfljOol3THlNvs= dependencies: "@ant-design/colors" "^3.1.0" babel-runtime "^6.26.0"
package-lock.json :
{ "name": "design-zone", "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { "@ant-design/colors": { "version": "3.2.2", "resolved": "https://registry.npm.taobao.org/@ant-design/colors/download/@ant-design/colors-3.2.2.tgz", "integrity": "sha1-WtQ9YZ6RHzSI66wwPWBuZqhCOQM=", "requires": { "tinycolor2": "^1.4.1" } }, "@ant-design/create-react-context": { "version": "0.2.5", "resolved": "https://registry.npm.taobao.org/@ant-design/create-react-context/download/@ant-design/create-react-context-0.2.5.tgz", "integrity": "sha1-9fWpFjtHcgl3EoNzl60w4i55+Fg=", "requires": { "gud": "^1.0.0", "warning": "^4.0.3" } } }}
此外,在使用 yarn 的过程中发现会不经意间引入版本重复的问题,随手打开了一个项目的 lock 文件发现了下面这种看起来有点不合“逻辑”的描述片段:
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.0.1, lodash@^4.16.5, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.4: version "4.17.15" resolved "https://registry.npm.taobao.org/lodash/download/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=lodash@^4.17.19: version "4.17.19" resolved "https://registry.npm.taobao.org/lodash/download/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha1-5I3e2+MLMyF4PFtDAfvTU7weSks=
观察 lodash 的版本描述,应该都是符合语义化版本规范的,为何在项目中还会存在两个不同的版本?
实际上这种情况一般是随着项目的迭代、依赖的增加而不经意间引入的,比如下面的场景:
此时将 lock 文件中 lodash 相关的两段描述删除、再重新执行安装即可,此时 lodash 版本为 4.17.20,同时去除了重复:
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.0.1, lodash@^4.16.5, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.4: version "4.17.20" resolved "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=
当然,更推荐借助 yarn-deduplicate 工具来自动进行去重操作,例 npx yarn-deduplicate yarn.lock 。
monorepo 模式目前也用得越来越多,我个人也很喜欢这种模式,而且我还喜欢将各个子包的依赖描述都定义在 根 package.json 中,因为这样在各个 package 中可以自由、方便地使用依赖,但这实际上是一个误区行为。
在 monorepo 模式中,无论是 lerna 还是 yarn 工作机制的核心都是:
但是
因为这样一来,实际上将是隐式依赖的问题加剧放大了,所以使用的时候还是需要稍加注意。
初衷是想重点介绍本节的内容,但在准备过程中发现 《node_modules 困境》 一文对 node_modules 相关的描写挺全面的,遂进行了一些二次整理和结合,同时压缩了这一节。
Berry 是 Yarn 2 的代号,同时也是 Yarn 2 仓库 的名称。
yarn 2 有一个理念,表示 yarn 虽作为一个包管理器,但 yarn 本身也是项目的依赖之一,yarn 认为 yarn 作为项目的第一个依赖,也应该被锁定。因此,yarn 2 及更高版本通过项目内安装的形式达到按项目进行管理的目的。
只需在已经使用 yarn 1 的项目内,进行本地升级,即可将某个项目的 yarn 升级至新版:
$ yarn set version berry
接下来就可以开始体验 yarn 的新特性了。
当然,提到 yarn 2,我觉得 pnp 才应该是首要关注的一大特性,这是 yarn 对 node_modules 做出的一次重大变革。
实际上 pnp 的功能早在 18 年 9 月份就被 提出 并 实现 了,但在 yarn 2 中算是正式出道吧,因为 yarn 2 默认使用 pnp 模式。
根据前文可以发现,Node 寻找模块实际上效率不高,而大量的依赖导致包管理器在安装依赖的时候也会有大量工作,对于 yarn 在 install 大体会经历四个阶段:
其中第 4 步涉及大量的文件 I/O,导致安装依赖时效率不高(尤其是在 CI 环境,每次都是重新安装全部依赖)。
pnp 就是为了解决这些问而出现的新特性:既然 Node 查找的方式低效,为什么不直接告诉 Node 文件在哪里呢?Node 所要做的仅仅只是从一个地方加载文件;同时 Node 不需要再自行寻找 node_modules 了,那么也无需为了模拟拓扑结构而重复拷贝依赖了。
在开启 pnp 的情况下,在安装之后 yarn 会生成一个 .pnp.js 文件,而 node_modules 不会再有了,取而代之的是一个 .yarn/cache 目录,作为依赖的存放位置。
.pnp.js 包含了两个映射表,可概括成以下信息:
.pnp.js 还包含一个 resolver 来告诉 Node 如何加载依赖。总之,使用了 pnp 可以预计是可以获得这些收益的:
开启 pnp 后的安装结果:
.├── .pnp.js├── .yarn│ ├── cache│ │ ├── @ant-design-colors-npm-3.2.2-71aac486be-b42a2e5422.zip│ │ ├── @ant-design-create-react-context-npm-0.2.5-7998e8d912-d86c381caf.zip│ │ ├── @ant-design-css-animation-npm-1.7.3-f3d18e5bbb-2d0e5c0a61.zip│ │ ├── @ant-design-icons-npm-2.1.1-c472b7964a-8e3682f594.zip│ │ ├── @ant-design-icons-react-npm-2.0.1-d1619b6de4-12eedf6ecd.zip│ │ ├── @babel-code-frame-npm-7.0.0-beta.44-de6de9a17f-58b214c926.zip│ │ ├── @babel-code-frame-npm-7.12.11-b0730d1d28-033d3fb3bf.zip│ │ ├── @babel-compat-data-npm-7.12.7-79f7d2298d-96e60c267b.zip│ │ ├── @babel-core-npm-7.12.10-6f71cf4941-4d7b892764.zip│ │ ├── @babel-generator-npm-7.0.0-beta.44-2d4de4045e-9c2e655e61.zip│ │ ├── @babel-generator-npm-7.12.11-d1390772ed-eb76477ff8.zip│ │ ├── @babel-helper-annotate-as-pure-npm-7.12.10-c32669dae2-d237f38b6a.zip│├── .yarnrc.yml├── package.json└── yarn.lock
最直观上的感受就是体积和文件数量上的变化(感觉终于不会再是黑洞了):
另一点因为 yarn 2 使用 zip 的形式保存依赖,依赖体积上有了很大的改善,使用版本管理工具直接管理依赖成为了一种现实易行的方式, berry cache 就是采用这种形式。
这样一来能带来新的改善:
不过,pnp 不是能直接使用的,需要各种工具进行支持,好消息是目前为止,社区的大部分工具都能直接支持 pnp 了,可以在 官方文档看到现在的支持情况 。
yarn 2 还有不少新特性和改善,如配置文件和 lock 文件使用标准 yml 格式,自带对 lock 文件中的依赖去重、支持 yarn 插件、更好的 workspaces 支持、新的模块协议等等,但本篇到此结束、不过多扩展了。
参考资料:
作者:酷家乐平台前端博客
出处
:https://webfe.kujiale.com/node_modules-yu-bao-guan-li-qi/