Node.js的依赖管理系统公认是非常先进的。这一篇文章来简单谈谈npm如何管理项目的依赖包的版本。
下文大多翻译自Express in Action一书第12章的相关内容
语义化版本号
npm默认所有的Node包都使用语义化版本号,英文叫做semantic versioning。这是一套指导开发人员如何增长版本号的规则,要求:
- 每个版本号都形如
1.2.3
,由三个部分组成,依次叫做“主版本号”、“次版本号”和“修订号” - 当新版本无法兼容基于前一版本的代码时,则提高主版本号
- 当新版本新增了功能与特性,但仍兼容前一版本的代码时,则提高次版本号
- 当新版本仅仅修正漏洞或者增强效率,仍然兼容前一版本代码,则提高修订号
默认情况下,npm install --save
下载的都是最新版本,并且会在package.json
文件里登记一个最优版本号,其形式如下所示:
"dependencies": {
"express": "^4.10.0",
"ejs": "~2.3.2"
}
可以看到,最优版本号在数字之前多出一个“标记”。当以后使用npm install
按照package.json
的这一部分来下载依赖包时,^
意味着所下载的包有可能会有更高的次版本号或者修订版本号,而~
意味着有可能会有更高的修订版本号。
锁定依赖包的版本
如果所有的Node包都严格地符合语义化版本管理的规则,那么npm的最优版本号就能保证所下载的依赖包一定是与代码兼容的。但问题是我们无法保证这一前提,如果想要保证用户(或者其他开发人员)下载依赖包与我们的代码绝对兼容,那么可以用下面两种办法来锁定项目的依赖包的版本号。
回避最优版本号
最简单最快捷的方法,就是不使用最优版本号。对于已经记录在package.json
里的版本号,只需把打头的^
和~
标记去掉即可。而新安装依赖包时,则使用npm install --save-exact <package_name>
或者npm install --save <package_name>@1.2.3
,这样package.json
里就不会出现最优版本的标记。
这个方法虽然简单,但是有个缺陷:无法锁定次级依赖的版本号(依赖包的依赖包,等等)。比如说你的项目依赖某个特定版本的Backbone.js,你可以按照上面的方法在package.json
里去掉最优版本的标记:
"dependencies": {
"backbone": "1.2.3"
}
而这个版本的Backbone有自己的package.json
,里面记录的依赖包使用的很可能还是最优版本号。比如Backbone依赖Underscore.js,你在开发时,npm为Backbone下载的可能是underscore@1.1.1
,而当之后的某个时刻,Underscore有了更新,同一项目的开发人员或者你的包的使用者运行npm install
时下载的可能就是underscore@1.2.0
。大多数情况下,这不会有什么问题,但是万一真的不兼容(或者你就是想要绝对安全),那么就得使用更加复杂一些的方法了。
使用npm shrinkwrap
命令
现在问题的关键在于如何锁定依赖之依赖的版本号。npm有一个命令来解决这个问题:npm shrinkwrap
。
比如说,你在开发某个Node项目时,进行到某个节点,一切都运行顺利,说明目前所有的依赖包(以及更底层的依赖包)和你的代码兼容得很好。这个时候,你就可以在项目文件夹下运行上面的这个命令。它会生成一个npm-shrinkwrap.json
文件,记录目前所有依赖包(及更底层依赖包)的版本信息。这样当以后你(或者你的同事、你的用户)运行npm install
命令时,npm首先会找npm-shrinkwrap.json
文件,依照其中的信息来准确地安装每一个依赖包,只有当这个文件不存在时,npm才会使用package.json
。
在这之后开发的过程中,如果你想要更新某个依赖包,比如将Express从4.13.0更新到4.14.1,那么就只需npm install express@4.14.1
;或者想要添加新的依赖包,比如Helmet,也只需npm install helmet
。经过一段时间的测试与开发,当你确定这些新版本新安装的依赖包与自己的代码兼容后,就可以再次运行npm shrinkwrap
命令来锁定依赖包的版本。
本地安装优于全局安装
npm安装依赖包时有两种模式:本地安装或者全局安装。本地安装表示该依赖包会被下载到当前项目的node_modules
文件夹里,而全局变量则会把它安装到系统级别的目录里。比如用npm install -g typescript
全局安装typescript这个包后,我们就可以在系统的任何位置使用tsc
命令来编译TypeScript文件。这本身没有什么错,有些Node包的命令确实需要在任何位置都能使用,但全局安装依赖包也有隐患:使用你的应用的用户如果没有全局安装typescript怎么办?或者如果她所安装的版本不兼容怎么办?
所以,安装依赖包最好遵循以下实践:
- 尽量不全局安装依赖包,除非是typescript,grunt这种确实有需要的
- 所有的依赖包都应该本地安装,即使是那些已经全局安装过的
本地安装的依赖包,其命令都位于当前项目的node_module/.bin
文件夹下。package.json
文件的"scripts"
部分所使用的依赖包命令都会先去这个文件夹寻找。
例如,你的package.json
里有这么一段:
"scripts": {
"build:js": "tsc"
}
那么,当你运行npm run build:js
时,npm会先去当前项目的node_module/.bin
里寻找对应的执行文件,如果你没有本地安装,才会使用全局级别的命令。
结语
以上就是对npm如何管理项目的依赖包版本的一个简单总结。主要涉及的知识点包括:语义化的版本号,最优版本号,两种锁定依赖包的方法,以及为什么本地安装依赖包会优于全局安装