# npm包版本管理
# 前言
先提一个场景,我们经常有时候clone了一个远古项目,然后在本地npm install之后,就报了很多奇奇怪怪的问题导致项目跑不起来,有时候我们把node_modules删掉,用yarn安装,又莫名奇妙的好了,另外有时候叫可以跑的同事复制一份package-lock.json或者yarn.lock文件,再重新安装就好了。后面章节就力求让这个莫名其妙的过程透明化且可以分析。
# semver版本依赖
npm采用的是semver版本管理方式。简单来说,日常我们看到的版本号形如X.Y.Z,你必须说明每次升级变更会对第三方使用产生哪些影响。这就是语义化版本想要传达的。一个版本有三部分:X, Y, Z,分别指代大版本,小版本,与bugfix版本。比如1.2.3,那么就是大版本1,小版本2,bugfix版本3。bugfix版本不会影响任何功能,小版本变更往往是增加新功能,也不会影响使用。而大版本变更往往会带来使用层面不兼容的情况,需要再做调整。
另外,还有几种符号,代表版本的范围匹配:
- ^1.2.2:代表安装不低于- 1.2.2的版本,但是大版本号需相同,即- 1.2.2即其以上的- 1.x.x版本都是可以的。
- ~1.2.2:代表安装不低于- 1.2.2的版本,但是大版本号和小版本号需相同,即- 1.2.2即其以上的- 1.2.x版本都是可以的。
- 1.2.2:- 1.2.2版本号全匹配。
- 先行版本:当某个版本改动比较大、并非稳定而且可能无法满足预期的兼容性需求时,你可能要先发布一个先行版本。先行版本号可以加到主版本号.次版本号.修订号的后面,先加上一个连接号再加上一连串以句点分隔的标识符和版本编译信息。- 内部版本(alpha)
- 公测版本(beta)
- 正式版本的候选版本rc:即Release candiate
 
- 内部版本(
注意:semver的~,^与beta,alpha,rc的阻断关系(现在还没get到规律)。举两个antd的例子。
- 例子一:
 
 此时,package.json中写了:(此时会收到先行版本号的截断)
"antd": "^0.1.0-beta2" // 理论上应该安装到0.x.x的最新版本,但是最终会安装到`0.1.0-pre`
"antd": "^0.7.0" // 理论上应该安装到0.x.x的最新版本,最终会安装到`0.7.2`
- 例子二:
 
 此时,package.json中写了:(2.0.0到2.13.4中间也有一些beta版本)。
"antd": "^2.0.0" // 最终会安装到`0.1.0-pre`
"antd": "^0.7.0" // 最终会安装到`0.7.2`
# npm包依赖管理
有了semver版本之后,大家如果都根据这样的规范来做事,那一般都不会出什么大问题。npm install的输入是package.json,它的输出是一棵node_modules树。理想情况下,npm install应该像纯函数一样工作,对于同一个package.json总是生成完全相同的node_modules树。在某些情况下,就算是有一些小版本例如~或者^符号带来的版本微调,但是因为大版本不变,所以从语义上来说我们的核心功能也不会受到影响。
但是规范这种东西,没有强求就一定会走偏。事实上,很多npm包的开发者并不一定会依照这样的版本管理来发布自己的代码。
试想一下,之前你们的同事安装了某个包版本我们叫bowlofnoodles是^1.2.2(实际上经常出现这样的版本号,因为npm install xx不指定版本的情况下默认就是这样的,从语义上来说小版本和bugfix版本都不会影响使用,都是增强功能。)。此时一年后,bowlofnoodles已经升级到了1.8.5。根据semver版本,此时你就会安装到1.8.5,但是开发者在这个版本中把以前1.2.2的一些接口删了,此时就报了bug导致项目启动不了。
聪明的人会想,那我把版本写死1.2.2,不就能安装到同样的版本了。但是node_modules本质是个树形,你可以锁定bowlofnoodles的版本,但是如果同样的情况出现在bowlofnoodles这个包的版本管理中,那怎么办呢?
# package-lock.json的出现
如果我们能够通过什么方式安装得到跟同事相同的node_modules树那肯定就能够解决上述的版本不稳定问题。所以package-lock.json出现了。在npm install的时候,会生成一个package-lock.json,可以理解为描述node_modules树的快照,里面包含着你安装到包的版本记录。那我们下次再根据这个package-lock.json去装包,就可以解决上面的问题了。
那么,package.json和package-lock.json同时存在的时候且他们的版本存在冲突时,哪个优先级更高或者根据什么样的规则来安装呢?
不同npm包版本,关于这个问题其实表现的会有一些不一样:
- 5.0.x版本:不管- package.json中依赖是否有更新,- npm install都会根据- package-lock.json下载。针对这种安装策略,有人提出了这个issue-#16866 ,然后就演变成了5.1.0版本后的规则。
- 5.1.0版本后:当- package.json中的依赖项有新版本时,- npm install会无视- package-lock.json去下载新版本的依赖项并且更新- package-lock.json。针对这种安装策略,又有人提出了一个issue-#17979,得出- 5.4.2版本后的规则。
- 5.4.2版本后: 如果只有一个- package.json文件,运行- npm install会根据它生成一个- package-lock.json文件,这个文件相当于本次- install的一个快照,它不仅记录了- package.json指明的直接依赖的版本,也记录了间接依赖的版本。- 如果 - package.json的- semver-range version和- package-lock.json中版本兼容(- package-lock.json版本在- package.json指定的版本范围内),即使此时- package.json中有新的版本,执行- npm install也还是会根据- package-lock.json下载。- 如果手动修改了 - package.json的- version ranges,且和- package-lock.json中版本不兼容,那么执行- npm install时- package-lock.json将会更新到兼容- package.json的版本。
注意:
npm install读取package.json创建依赖项列表,并使用package-lock.json来通知要安装这些依赖项的哪个版本。如果某个依赖项在package.json中,但是不在package-lock.json中,运行npm install会将这个依赖项的确定版本更新到package-lock.json中,不会更新其它依赖项的版本。
# 依赖版本选择的最佳实践
# 版本发布
- 对外部发布一个正式版本的npm包时,把它的版本标为 - 1.0.0。
- 某个包版本发行后,任何修改都必须以新版本发行。 
- 版本号严格按照 - 主版本号.次版本号.修订号格式命名。
- 版本号发布必须是严格递增的。 
- 发布重大版本或版本改动较大时,先发布 - alpha、- beta、- rc等先行版本。
# 依赖范围选择
- 主工程依赖了很多子模块,都是团队成员开发的npm包,此时建议把版本前缀改为~,如果锁定的话每次子依赖更新都要对主工程的依赖进行升级,非常繁琐,如果对子依赖完全信任,直接开启^每次升级到最新版本。 
- 主工程跑在 - docker线上,本地还在进行子依赖开发和升级,在- docker版本发布前要锁定所有依赖版本,确保本地子依赖发布后线上不会出问题。
# 保持依赖一致。
- 确保npm的版本在 - 5.6以上,确保默认开启- package-lock.json文件。
- 由初始化成员执行 - npm inatall后,将- package-lock.json提交到远程仓库。不要直接提交- node_modules到远程仓库。
- 定期执行 - npm update升级依赖,并提交- lock文件确保其他成员同步更新依赖,不要手动更改- lock文件。
# 依赖变更
- 升级依赖: - 修改package.json文件的依赖版本,执行npm install。此时会根据semver版本更新到对应的最新版本。
- 如果固定只升级某个版本,要npm install package@version。可以根据情况选择。
 
- 修改
- 降级依赖: 直接执行 - npm install package@version(改动- package.json不会对依赖进行降级)。注意改动依赖后提交lock文件。
# 其它的一些问题待补充
- npm shrinkwrap 
- cnpm 
- yarn 
- npm install过程 (另一篇) 
- package.json剖析(另一篇) 
# 参考
← 浏览器的架构演变