



package.json虽然描述了项目依赖的第三方模块,但在版本控制上做的并不完善。package.json提供的模糊版本号匹配规则无法保证多次运行npm install命令后安装的模块是相同的。
以express为例,即使在package.json中明确地将版本号固定在4.17.1,但这样仅能固定express本身的版本,无法控制express自身依赖项的版本。这意味着多次执行npm install express@4.17.1可能会安装不同版本的子模块。
下面列出了express项目的package.json文件中dependencies字段内容。
// express的依赖项及版本号
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
在实际开发中,因为文件体积和数量,开发者通常不会往代码仓库中提交node_modules文件夹,而是将package.json文件提交,然后在部署过程中运行npm install命令。这就可能导致生产环境安装了和开发环境不同的依赖,从而给生产环境的代码运行带来不确定性。
读者可能会产生疑问,既然不能指定具体版本号会有这种缺点,那么为什么不干脆把dependencies的所有字段全都写成具体版本号?更进一步,npm为什么要提供不明确指定版本号的规则?
答案是不指明具体的版本号可以让开发者享受到更新后(通常是一些bug修复)的特性。以express的依赖项accepts为例,假设该模块的1.3.7版本在使用过程中发现了一个严重的bug,那么模块拥有者在修复bug之后,就会将所有包含该bug的版本删除并提供一个新的版本,例如1.3.10。如果在package.json中没有使用~而是明确指定版本号,就会导致构建失败。
为了避免安装过程的不确定性,npm5.0.0(2017年5月发布,对应的Node版本是v8.0)及之后的版本增加了package-lock.json特性,该文件描述了package.json中的所有模块及它们的子模块的详细版本信息。
还是以express为例,如果用户安装了最新版本的Node,那么在运行npm install命令时,除了将express的版本信息写入package.json的dependencies字段中以外,还会把express自身的依赖模块信息写入package-lock.json中。该文件的内容是自动生成的,开发者不需要手动修改里面的内容。
以下是安装express过程中生成的package-lock.json文件的部分内容。
{
"name": "npmtest",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved":"https://registry.npm.taobao.org/accepts/download/
accepts-1.3.7.tgz",
"integrity": "sha1-UxvHJlF6OytB+FACHGzBXqq1B80=",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
// other lines...
},
dependencies属性包括了node_module文件夹下的所有模块,并包含了具体的版本号和下载地址等信息。例如,accepts是experss直接依赖的模块,它同时依赖于mime-types和negotiator两个模块,npm在安装模块时就会向下查找对应的模块信息。
"mime-types": {
"version": "2.1.24",
"resolved":"https://registry.npm.taobao.org/mime-types/download/
mime-types-2.1.24.tgz",
"integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=",
"requires": {
"mime-db": "1.40.0"
}
},
"negotiator": {
"version": "0.6.2",
"resolved":"https://registry.npm.taobao.org/negotiator/download/
negotiator-0.6.2.tgz",
"integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs="
},
mime-types和negotiator两个模块也标注了具体的版本号,其中mime-types还依赖mime-db,那么就继续向下查找对应的版本号即可。通过递归的过程,所有依赖项的版本号都被确定下来。
在实际的开发过程中,将package-lock.json和package.json一起提交到代码库中,那么下次在运行npm install命令时,就会根据package-lock.json中的信息安装对应版本的模块。