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中的信息安装对应版本的模块。