前端工程化之依赖管理

433 阅读7分钟

本文主要关注以下问题:

1.语义化版本

2.npm依赖管理策略

3.yarn依赖管理策略

4.npm和yarn包管理策略的异同

Ⅰ、语义化版本

语义化版本:Semantic Versioning 或者 “semver”。

标准的版本号必须采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须以数值来递增。

其中X: major(主版本号),Y:minor(次版本号),Z: patch(补丁版本)的修改可以理解为:

  • major: 对包做了重大且不兼容的改动。
  • minor: 在包中增加了新功能,并且新功能是向后兼容的。
  • patch: 修复了包中的bug,并且是向后兼容的。

版本号可以和>、>= 、< 、<= 、 =、 ~ 、^等符号一起使用,用来表示一个版本范围。相关的使用和含义这里写的很清楚,值得关注的是,默认安装一个包后,版本号默认是以^来修饰版本号的。npm和yarn的包管理均采用语义化版本。

版本兼容:如果两个版本所表示的范围的存在交集,则表示两个版本可以兼容,则对于npm和yarn而言,会推断出一个可以兼容的版本来使二者均可以正常使用,如^1.2.3 和2.0.3是不兼容的,而^1.2.3和^1.2.9则是兼容的。

Ⅱ、npm包管理策略

npm的包管理经历了不同的阶段。其主要里程碑出现在v3和v5版本。

早期版本的npm主要使用嵌套策略来安装包,简单来说就是在package.json中依赖的包会安装在项目的根目录下,而这些依赖所使用到的包会安装在每个依赖对应文件夹的node_modules文件夹中。此种依赖方式形成了一个依赖树,这样清晰明了,但是也导致了,如果一个依赖包A被不同的依赖包依赖,则它将会出现在每一个包的node_modules文件夹下,这将导致node_modules非常巨大。

V3:为了解决上述问题,在v3版本中引入了扁平依赖的概念。在安装中遇到的依赖首先放在根目录的node_modules中,如果遇到版本不兼容才将其放置在依赖的node_modules文件夹中。如:package.json中依赖了A、B,A依赖了C,B也依赖了C,如果A和B依赖的C的版本不兼容,则在安装过程中,先遍历到了A中的依赖C,则A依赖的C会被放置在根目录的node_modules 中,而B中的C则会被放置在B目录下得node_modules目录下。如果A和B依赖的C的版本兼容,则npm会推断出一个兼容二者的版本的C,并将其放置在根目录的node_modules下。这样,如果依赖的版本都是兼容的,则会省去不少的空间。但是还有一个问题就是,这样虽然能部分解决依赖重复安装的问题,但是还有一些问题没有解决,其中最主要的就是,如果安装的依赖A, B中依赖的C不兼容,究竟A,B中哪个依赖的C会被放置在根目录,这是不太确定的。这将导致不同的人安装后的依赖树结构不同,最终会导致打包的bundle不同。

V5: 为了解决安装依赖的不确定性问题,在v5版本中提出了使用package-lock.json来解决这个问题。当项目中已有 package-lock.json 文件,在安装项目依赖时,将以该文件为主进行解析安装指定版本依赖包,而不是使用 package.json 来解析和安装模块。因为 package-lock 为每个模块及其每个依赖项指定了版本,位置和完整性哈希,所以它每次创建的安装都是相同的。 无论你使用什么设备,或者将来安装它都无关紧要,每次都应该给你相同的结果。

npm

并不是一开始就是按照现有这种规则制定的。

5.0.x 版本

不管 package.json 中依赖是否有更新,npm install 都会根据 package-lock.json 下载。针对这种安装策略,有人提出了这个 issue ,然后就演变成了 5.1.0 版本后的规则。

5.1.0 版本后

package.json 中的依赖项有新版本时,npm install 会无视 package-lock.json 去下载新版本的依赖项并且更新 package-lock.json。针对这种安装策略,又有人提出了一个 issue 参考 npm 贡献者 iarna 的评论,得出 5.4.2 版本后的规则。

5.4.2 版本后

如果只有一个 package.json 文件,运行 npm install 会根据它生成一个 package-lock.json 文件,这个文件相当于本次 install 的一个快照,它不仅记录了 package.json 指明的直接依赖的版本,也记录了间接依赖的版本。

如果 package.jsonsemver-range versionpackage-lock.json 中版本兼容(package-lock.json 版本在 package.json 指定的版本范围内),即使此时 package.json 中有新的版本,执行 npm install 也还是会根据 package-lock.json 下载。

如果手动修改了 package.jsonversion ranges,且和 package-lock.json 中版本不兼容,那么执行 npm installpackage-lock.json 将会更新到兼容 package.json 的版本。

Ⅲ、yarn包管理策略

yarn的主要特点:

  • 并行安装:无论 npm 还是 yarn 在执行包的安装时,都会执行一系列任务。npm 是按照队列执行每个 package,也就是说必须要等到当前 package 安装完成之后,才能继续后面的安装。而 yarn 是同步执行所有任务,提高了性能。
  • 离线模式:如果之前已经安装过一个软件包,用 yarn 再次安装时之间从缓存中获取,就不用像 npm 那样再从网络下载了。
  • 安装版本统一:为了防止拉取到不同的版本,yarn 有一个锁定文件 (lock file) 记录了被确切安装上的模块的版本号。每次只要新增了一个模块,yarn 就会创建(或更新)yarn.lock 这个文件。这么做就保证了,每一次拉取同一个项目依赖时,使用的都是一样的模块版本。
  • 更好的语义化yarn 改变了一些 npm 命令的名称,比如 yarn add/remove,比 npm 原本的 install/uninstall 要更清晰。

Ⅳ、npm和yarn包管理策略的异同

1.npm5+和yarn的确定性算法不同。

查看package-lock.json文件会发现,package-lock.json可以看做安装依赖的一棵完整的依赖树,其完整地标明了,每一个依赖安装后的位置。因此只通过package-lock.json文件就可以确定安装后的文件目录结构。

查看yarn.lock文件会发现,yarn.lock中列出的依赖都是扁平的,无法准确的确定每个依赖是在根目录node_modules下,还是依赖自身的node_modules目录下,因此需要package.json文件来共同确定其位置。具体考量见[参考文献3].

2.扁平化算法不同

npm5+的package-lock.json把所有的包的依赖顺序列出来,第一次出现的包名会提升到顶层,后面重复出现的将会放入被依赖包的node_modules当中。引起不完全扁平化问题(之前有这个问题,现在不知道解决了没有)。

yarn的yarn.lock中则是完全扁平化的,会根据算法计算引用较多的兼容版本会被放在根目录node_modules文件夹下,而引用较少的则分布在依赖的node_modules文件夹下。

yarn.lock中的包依赖只有两层,只列出了直接依赖的包,如果依赖的包又依赖了其他的包,则也以同样的方式列出,如下dumi-theme-moibile依赖了lodash.debounce:

以此类推,会把每个包的依赖都扁平化列出,最终如果版本是兼容的,则会合并为一个,并最终使用最高的版本,如下,项目中使用到了很多版本的lodash,最终yarn会根据兼容策略,选择兼容的最大的版本:

题外话:包管理策略直接影响到了最终webpack等工具打包的大小,因为webpack生成以依赖图的查找包会先从自己文件夹下的node_modules中查找,找不到才会从上层node_modules中找,一直到最上层的node_moduels,找不到则会报错。所以,扁平化会极大的减小包大小。

参考文献:

1.semver.org/lang/zh-CN/

2.yarn.bootcss.com/docs/depend…

3.classic.yarnpkg.com/blog/2017/0…

4.docs.npmjs.com/cli/v6/conf…

5.juejin.cn/post/684490…

6.segmentfault.com/a/119000003…

7.juejin.cn/post/684490…

8.segmentfault.com/a/119000001…

9.zhuanlan.zhihu.com/p/377593512