一、写在前面
一次线上项目bug,引发了关于package.json中的^~是否该保留?保留可能引发的后果?以及如何在版本更新便利和版本更稳定中取舍的思考?这个bug是由于线上部署打包时,自己下载了最新依赖,于是线上依赖版本和研发本地依赖版本不同,不巧的是最新依赖有bug而本地早先下载的没有,导致定位bug浪费了大量时间。
最后发现是lock的版本有差异,根据这个方向进一步定位到了bug。
而会导致安装的依赖版本差异的决定性原因则是package.json没有写死版本号,而是使用允许根据市场版本更新的指令符号^~
如果希望避免上述bug,锁定依赖版本,可行的方案是什么?
本文将以上述bug为引,尝试简单讨论和认识:
- package.json中的^~
- 依赖版本锁定:yarn和npm等的lock锁定
- 依赖版本锁定:package.json的锁定
- 让Git记下lock的变更记录的意义
- 小结:前端工程项目依赖版本锁定小结
二、package.json中的^~
- ^表示的意思是要更新【次版本】,当市场有更新的版本时,例如:package.json中是"^2.1.0",库可能会更新到2.2.0的最新版本,但不会更新到3.0.0版本。
- ~表示的意思是要更新【补丁版本】,当市场有更新的版本时,例如package.json中是"~2.1.0",库可能会更新到2.1.1的最新版本,但不会更新到2.2.0版本。
- 版本号前面啥也没有,表示写死了版本号,无论何时何地安装的依赖版本只会是这个。
三、怎么锁定工程依赖的版本
在实践中,工程依赖的版本锁定,可能会有两方面的考虑,一是通过包管理工具的lock进行依赖锁定,二则是通过在package.json中写死版本号来“绝对锁定”依赖版本。依赖版本的锁定,是必须要考虑;否则一个差异和不幸可能需要浪费大量的时间去定位由此导致的bug,那将是痛苦而不值得的,尽管发生的机会比较小。
3.1 package.json的锁定
毫无疑问,package.json具有依赖版本的决定权。是否在安装依赖时,下载最新版本,是否修改lock版本是由package.json中附带~^等命令符号结合市场最新版本决定的,在决定性因素上,与使用的包管理工具并无多大关系,无论是npm、yarn或pnpm。
只要package.json写死版本号,版本号前不携带那些~^等符号,那么无论何时何地何人安装依赖,依赖版本都会是一致的。
因此,在功能已经开发完毕,进入运维阶段的前端工程项目,如果希望减少由于依赖版本差异带来的莫名其妙的bug,那么写死package.json版本号是可行可靠的。如果确有需要升级依赖版本,再单独手动去升级。项目上线转运维阶段后,需要批量更新依赖,从而使用新依赖的新功能的可能性比较小,而运维中的项目保证项目的稳定才是更重要的。
3.2 yarn和npm等的lock锁定
矛盾总是存在的,总有不希望一个一个手动更新依赖的需求,总有希望“一键更新全部依赖”的场景。这种时候,package.json中写死版本号,则不是期望的。那么某种程度上的依赖锁定则出现了,就是通过包管理工具的lock来锁定,例如yarn的yarn.lock,npm的package-lock.json,以及pnpm的pnpm-lock.yaml。
据观测一些知名的开源项目,通常也不会全部一锤将版本号都写死在package.json中,并且通常让Git记下lock的变更记录。例如:
- vuejs/vue(pnpm 的 lock)[1]
- facebook/react(yarn 的 lock)[2]
- axios/axios(npm 的 lock)[3]
- dcloudio/uni-app(yarn 的 lock)[4]
- didi/LogicFlow(yarn 的 lock)[5]
- quilljs/quill(npm 的 lock)[6]
3.3 yarn.lock和package-lock.json下载新依赖上的区别
包管理工具的lock也具有某种程序上的“依赖版本锁定”功能,尽管不同的工具的lock表现具有差异。例如当package-lock.json存在时,即使市场上有比package-lock.json中锁定版本更加新,且package-lock.json中存在^~允许更新,安装的版本也只会是package-lock.json中锁定的版本,不会自动下载更新的版本。当package.json中的版本被手动更新,会触发的package-lock.json连带变更。
而yarn的yarn.lock的“版本锁定”则表现不同,当市场有yarn.lock中更新的版本,且package-lock.json中存在^~允许更新,那yarn会自动安装比yarn.lock更加新的版本并且主动修改yarn.lock。
从这个差异角度看,package-lock.json的“版本锁定”更可靠,可以起到依赖版本保持一致的作用,而yarn.lock则不具备。
3.4 让Git记下lock的变更记录
既然yarn.lock无法帮助我们锁定版本,那么yarn.lock的意义何在?我并不清楚yarn.lock设计的全部意义,但我可以确定一个价值是:让Git记下lock的变更记录,有助于追踪使用的依赖版本记录,有时会很有作用,例如定位某类bug时。
即使lock会被修改,它的存在也很有价值,例如:证明此前被lock的版本在本工程是可用的。因为会存在某个最新版本存在缺陷或不符合本工程的需求的情形。这个新版本缺陷可能导致工程无法运行或运行异常。
例如:新同事安装工程运行异常,而老同事的正常,且通过git证明业务代码无差异,package.json也无差异,此时差异会体现在lock的版本上。此时要想使用最新且能确保本工程正常运行的依赖版本,那么老同事的lock依赖版本就是答案。
而package.json中的版本虽然可用,但由于具有时间跨度的不确定性,可能会比lock的要旧很多。
知名的例子就是vue-router的issues #2881 中提到的,在升级了Vue-Router版本到3.1.0及以上之后,页面在跳转路由控制台会报Uncaught(in promise)的问题,从而导致某些场景的跳转异常。
3.5 关于依赖地狱(Dependency Hell)和依赖分身(Doppelgangers)
package.json的依赖版本设置作用仅有效于当前工程,实践中依赖锁定往往会涉及嵌套依赖等问题,因为依赖也可能会有package.json,依赖的依赖也可能会有package.json。
但本文暂不讨论关于依赖地狱(Dependency Hell)和依赖分身(Doppelgangers)的问题。
小结
总结上述简单的分析和讨论,小结如下:
①package.json 具有锁定依赖版本的决定权,包管理工具不具有。
②package.json ^ 意思是要更新【次版本】,~ 意思是要更新【补丁版本】。
③yarn 的 yarn.lock 和 npm 的 package-lock.json 对“锁定依赖版本”表现不同,package-lock.json 的“锁定效果”更可靠。
④让Git记下 lock 的变更记录是有意义的。一些知名开源项目都让Git记下 lock 的变更记录。