通常,为了保证开发环境、生产环境的一致性,我们需要对依赖的版本进行控制。下图描述了一些依赖的版本描述方法
版本描述的语法可见下表
npm package.json dependencies version doc
| 语法 | 描述 |
|---|---|
| version | 必须与version完全匹配 |
| >version | 必须大于version |
| >=version | 大于或等于version |
| <version | 必须小于version |
| ~version | “大约相当于版本”参见 semver |
| ^version | “兼容版本”参见 semver |
| 1.2.x | 锁定大版本号及中版本号,小版本号按最新处理 |
| latest | 最新版本 |
固定version能固定版本吗?
从上述语法看来,version语法是用来描述一个固定的版本,包管理器会严格按照声明的版本来进行安装。那使用这种语法不应该就能安装一个在任何环境都一样的版本了吗?
但是从这块的标题来看,显然不是的。Why?
依赖的依赖,不是我的依赖
依赖的依赖,不是我的依赖!version语法,固定的是第一层包的版本,但是第一层依赖包的依赖呢?他也有自己的package.json啊,里面也会有他自己的依赖^version来描述版本。js
依赖基于源码分发。为保证包体积轻量化,通常依赖的依赖包不会把它自己的依赖bundle进来,只是声明依赖,而是通过主项目来进行安装和bundle。
如:
element-plus会依赖于vue,如果其将element-plus将vue打包进源码,最终构建时不就会有两份vue源码打包进bundle中?。并且,前端依赖包错综复杂,如果都打包进bundle,最终构建的bundle会非常庞大。
所以通常你的依赖也会有自己的依赖,而它的依赖也会有依赖,层层相套。
因此,你在package.json中固定的版本,也只能固定住第一层包版本。
所以指定version并不是可靠的版本锁定方式。
那^version和~version呢?
在依赖包遵循语义化版本的前提下:
^version只锁定大版本号,即允许次要更新(中版本号,非破坏性的)、补丁更新(最小版本号)
如
^1.0.0, 包管理器会安装从>=1.0.0至<2.0.0中,选取一个最新的来安装
~version锁定大版本号和中版本号,即只允许补丁更新(最小版本号)
如
~1.0.0, 包管理器会安装从>=1.0.0至<1.0.n中,选取一个最新的来安装
可见,^version和~version也不一定能保证各环境的依赖一致性。难道就没有什么办法了吗?
可靠的 lockfile
npm 从 5.0开始,提供了版本锁机制,即在安装完包后,在项目根目录下生成一个package-lock.json,该文件会记录当前项目依赖版本和来源(包括你依赖的依赖,出于种种原因,这个功能在npm@6.0才稳定下来)。
当再次运行npm install时,会优先读取package-lock.json,来按照锁文件中的版本来进行安装。当检测到锁文件没有的包时(比如新增的依赖包),才会重新去找版本安装, 并更新锁文件。
然而,有的项目出于不明目的,会在.gitignore中忽略该文件。这样,版本就不一定稳定了。每次都会用采用最新的版本(规则之类最新的版本)。
目前来看 lockfile是最安全可靠的依赖包版本管理机制,但是由于前端生态过于繁华,lockfile也可能会有一些问题。
最佳实践
前端在最近几年的发展中,有三个比较主流的包管理器,yarn、npm、pnpm。
npm 是node官方的,在早期通过嵌套结构来安装包,导致项目依赖体积巨大,安装效率巨慢(后期有改进,但是用户全被yarn抢走了)
yarn 通过将依赖扁平化,过滤掉了一些重复的依赖,加快了安装效率。
pnpm 则是将依赖存储在全局,通过软链接和硬链接来将依赖链接到局部项目,装过一次的依赖就不会再安装了,得益于此,它的安装速度是这三个中最快的(但不是最快,bun少了个加载node进程的时间,重复安装是毫秒级的),删除node_modules再进行安装,是秒级的。
它各自会生成自己的lockfile,(package-lock.json、yarn.lock、pnpm-lock.yaml)并且互不兼容。
割裂带来的问题可能是开发者各有所好,反正就按自己喜欢的来。可能线上用的是npm,本地用的yarn,并且可能存在本地和线上构建所用包管理器版本不统一的问题(lockfile也有版本,如果不统一,可能导致最终安装的依赖版本不一致)。
node16 和 corepack
node16中新增了一个官方的包管理器的管理器,corepack。
没错,它就是用来专门管理包管理器的。
假如你本地初次安装nodejs, 且版本大于16,那么你可以运行 corepack enable。
运行此行命令后,即可激活corepack(nodejs默认是关闭的)。然后在项目中package.json
中添加"packageManager": "pnpm@6.6.12",然后在package.json同级目录下运行 pnpm -v命令,你应该能获得以下输出:
> pnpm -v
> 8.6.12
可见,在开启了corepack的环境下,包管理器的版本可以通过项目package.json来控制。
那么我们只需要通过corepack 和 package.json中的配置字段,即可保证生产、开发依赖版本完全保持一致了。
集成至生产环境的CI/CD中,只需要进行以下更改。
# 原来的step
npm i
npm build
# 新的step, 需要在node16下,并假设你在package.json 中配置包管理器是 pnpm
coreapck enable # 执行此步骤即可,无需安装 pnpm 了
pnpm i
pnpm build