原文链接:https://www.cnblogs.com/yalong/p/15013880.html
package.json 跟 package-lock.json 的知识点挺多的, 这里只聊一聊部分知识点
先看下dependencies与devDependencies
npm install 安装依赖的时候,可以通过如下方式,把依赖写入package.json
npm install --save 或者 npm install -S npm install --save-dev 或者 npm install -D
dependencies与devDependencies的区别:
devDependencies下列出的模块,是我们开发时用的依赖项,像一些进行单元测试之类的包,比如jest,我们用写单元测试,它们不会被部署到生产环境。dependencies下的模块,则是我们生产环境中需要的依赖,即正常运行该包时所需要的依赖项。
记着这句: “正常运行该包时所需要的依赖项”
后面的package-lock.json 中的 dependencies 对应的就是package.json中的 dependencies
Package.json 语义化版本
使用第三方依赖时,通常需要指定依赖的版本范围,比如
"dependencies": { "antd": "3.1.2", "react": "~16.0.1", "redux": "^3.7.2", "lodash": "*" }
上面的 package.json 文件表明,项目中使用的 antd 的版本号是 3.1.2,但是 3.1.1 和 3.1.2、3.0.1、2.1.1 之间有什么不同呢。
语义化版本规则规定,版本格式为:主版本号.次版本号.修订号
,并且版本号的递增规则如下:
- 主版本号:当你做了不兼容的 API 修改
- 次版本号:当你做了向下兼容的功能性新增
- 修订号:当你做了向下兼容的问题修正
主版本号的更新通常意味着大的修改和更新,升级主版本后可能会使你的程序报错,因此升级主版本号需谨慎,但是这往往也会带来更好的性能和体验。
次版本号的更新则通常意味着新增了某些特性,比如 antd 的版本从 3.1.1 升级到 3.1.2,之前的 Select 组件不支持搜索功能,升级之后支持了搜索。
修订号的更新则往往意味着进行了一些 bug 修复。因此次版本号和修订号应该保持更新,这样能让你之前的代码不会报错还能获取到最新的功能特性。
但是,往往我们不会指定依赖的具体版本,而是指定版本范围,比如上面的 package.json 文件里的 react、redux 以及 lodash,这三个依赖分别使用了三个符号来表明依赖的版本范围。语义化版本范围规定:
- ~:只升级修订号
- ^:升级次版本号和修订号
- *:升级到最新版本
因此,上面的 package.json 文件安装的依赖版本范围如下:
react@~16.0.1:>=react@16.0.1 && < react@16.1.0 redux@^3.7.2:>=redux@3.7.2 && < redux@4.0.0 lodash@*:lodash@latest
语义化版本规则定义了一种理想的版本号更新规则,希望所有的依赖更新都能遵循这个规则,但是往往会有许多依赖不是严格遵循这些规定的。
因此,如何管理好这些依赖,尤其是这些依赖的版本就显得尤为重要,否则一不小心就会陷入因依赖版本不一致导致的各种问题中。
npm的历史变迁
嵌套结构
我们都知道,执行 npm install 后,依赖包被安装到了 node_modules ,下面我们来具体了解下,npm 将依赖包安装到 node_modules 的具体机制是什么。
在 npm 的早期版本, npm 处理依赖的方式简单粗暴,以递归的形式,严格按照 package.json 结构以及子依赖包的 package.json 结构将依赖安装到他们各自的 node_modules 中。直到有子依赖包不在依赖其他模块。
举个例子,我们的模块 my-app 现在依赖了两个模块:buffer、ignore:
{ "name": "my-app", "dependencies": { "buffer": "^5.4.3", "ignore": "^5.1.4", } }
ignore是一个纯 JS 模块,不依赖任何其他模块,而 buffer 又依赖了下面两个模块:base64-js 、 ieee754。
{ "name": "buffer", "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }
那么,执行 npm install 后,得到的 node_modules 中模块目录结构就是下面这样的:

这样的方式优点很明显, node_modules 的结构和 package.json 结构一一对应,层级结构明显,并且保证了每次安装目录结构都是相同的。
但是,试想一下,如果你依赖的模块非常之多,你的 node_modules 将非常庞大,嵌套层级非常之深:
- 在不同层级的依赖中,可能引用了同一个模块,导致大量冗余。
- 在 Windows 系统中,文件路径最大长度为260个字符,嵌套层级过深可能导致不可预知的问题。
扁平结构
为了解决以上问题,NPM 在 3.x 版本做了一次较大更新。其将早期的嵌套结构改为扁平结构:
- 安装模块时,不管其是直接依赖还是子依赖的依赖,优先将其安装在 node_modules 根目录。
还是上面的依赖结构,我们在执行 npm install 后将得到下面的目录结构:\


此时我们若在模块中又依赖了 base64-js@1.0.1 版本:
{ "name": "my-app", "dependencies": { "buffer": "^5.4.3", "ignore": "^5.1.4", "base64-js": "1.0.1", } }
- 当安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下安装该模块。
此时,我们在执行 npm install 后将得到下面的目录结构:

对应的,如果我们在项目代码中引用了一个模块,模块查找流程如下:
- 在当前模块路径下搜索
- 在当前模块 node_modules 路径下搜素
- 在上级模块的 node_modules 路径下搜索
- …
- 直到搜索到全局路径中的 node_modules
假设我们又依赖了一个包 buffer2@^5.4.3,而它依赖了包 base64-js@1.0.3,则此时的安装结构是下面这样的:

所以 npm 3.x 版本并未完全解决老版本的模块冗余问题,甚至还会带来新的问题。
另外,为了让开发者在安全的前提下使用最新的依赖包,我们在 package.json 通常只会锁定大版本,这意味着在某些依赖包小版本更新后,同样可能造成依赖结构的改动,依赖结构的不确定性可能会给程序带来不可预知的问题。
Lock文件(package-lock.json)
为了解决 npm install 的不确定性问题,在 npm 5.x 版本新增了 package-lock.json 文件,而安装方式还沿用了 npm 3.x 的扁平化的方式。
package-lock.json 的作用是锁定依赖结构,即只要你目录下有 package-lock.json 文件,那么你每次执行 npm install 后生成的 node_modules 目录结构一定是完全相同的。
例如,我们有如下的依赖结构:
{ "name": "my-app", "dependencies": { "buffer": "^5.4.3", "ignore": "^5.1.4", "base64-js": "1.0.1", } }
在执行 npm install 后生成的 package-lock.json 如下:
{ "name": "my-app", "version": "1.0.0", "dependencies": { "base64-js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz", "integrity": "sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=" }, "buffer": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" }, "dependencies": { "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" } } }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "ignore": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==" } } }
我们来具体看看上面的结构:

最外面的两个属性 name 、version 同 package.json 中的 name 和 version ,用于描述当前包名称和版本。
dependencies 是一个对象,对象和 node_modules 中的包结构一一对应,对象的 key 为包名称,值为包的一些描述信息:
- version:包版本 —— 这个包当前安装在 node_modules 中的版本
- resolved:包具体的安装来源
- integrity:包 hash 值,基于 Subresource Integrity 来验证已安装的软件包是否被改动过、是否已失效
- requires:对应子依赖的依赖,与子依赖的 package.json 中 dependencies的依赖项相同。
- dependencies:结构和外层的 dependencies 结构相同,存储安装在子依赖 node_modules 中的依赖包。
这里注意,并不是所有的子依赖都有 dependencies 属性,只有子依赖的依赖和当前已安装在根目录的 node_modules 中的依赖冲突之后,才会有这个属性。
例如,回顾下上面的依赖关系:
我们在 my-app 中依赖的 base64-js@1.0.1 版本与 buffer 中依赖的 base64-js@^1.0.2 发生冲突,所以 base64-js@1.0.1 需要安装在 buffer 包的 node_modules 中,对应了 package-lock.json 中 buffer 的 dependencies 属性。这也对应了 npm 对依赖的扁平化处理方式。
所以,根据上面的分析, package-lock.json 文件 和 node_modules 目录结构是一一对应的,即项目目录下存在 package-lock.json 可以让每次安装生成的依赖目录结构保持相同。
package-lock.json的作用
其实用一句话来概括很简单,就是锁定安装时的包的版本号,并且需要上传到git,以保证其他人在npm install时大家的依赖能保证一致。
引用知乎@周载南的回答
根据官方文档,这个package-lock.json 是在 npm install时候生成一份文件,用以记录当前状态下实际安装的各个npm package的具体来源和版本号。 它有什么用呢?因为npm是一个用于管理package之间依赖关系的管理器,它允许开发者在pacakge.json中间标出自己项目对npm各库包的依赖。你可以选择以如下方式来标明自己所需要库包的版本 这里举个例子: "dependencies": { "@types/node": "^8.0.33", }, 这里面的 向上标号^是定义了向后(新)兼容依赖,指如果 types/node的版本是超过8.0.33,并在大版本号(8)上相同,就允许下载最新版本的 types/node库包,例如实际上可能运行npm install时候下载的具体版本是8.0.35。波浪号 大多数情况这种向新兼容依赖下载最新库包的时候都没有问题,可是因为npm是开源世界,各库包的版本语义可能并不相同,有的库包开发者并不遵守严格这一原则:相同大版本号的同一个库包,其接口符合兼容要求。这时候用户就很头疼了:在完全相同的一个nodejs的代码库,在不同时间或者不同npm下载源之下,下到的各依赖库包版本可能有所不同,因此其依赖库包行为特征也不同有时候甚至完全不兼容。 因此npm最新的版本就开始提供自动生成package-lock.json功能,为的是让开发者知道只要你保存了源文件,到一个新的机器上、或者新的下载源,只要按照这个package-lock.json所标示的具体版本下载依赖库包,就能确保所有库包与你上次安装的完全一样。
原来package.json文件只能锁定大版本,也就是版本号的第一位,并不能锁定后面的小版本,你每次npm install都是拉取的该大版本下的最新的版本,为了稳定性考虑我们几乎是不敢随意升级依赖包的,这将导致多出来很多工作量,测试/适配等,所以package-lock.json文件出来了,当你每次安装一个依赖的时候就锁定在你安装的这个版本。
那如果我们安装时的包有bug,后面需要更新怎么办?
在以前可能就是直接改package.json里面的版本,然后再npm install了,但是5版本后就不支持这样做了,因为版本已经锁定在package-lock.json里了,所以我们只能npm install xxx@x.x.x 这样去更新我们的依赖,然后package-lock.json也能随之更新。
假如我已经安装了jquery 2.1.4这个版本,从git更新了package.json和package-lock.json,我npm install能覆盖掉node_modules里面的依赖吗?
其实我也有这个疑问,所以做了测试,在直接更新package.json和package-loc.json这两个文件后,npm install是可以直接覆盖掉原先的版本的,所以在协作开发时,这两个文件如果有更新,你的开发环境应该npm install一下才对。
package-lock.json使用建议
开发系统应用时,建议把 package-lock.json 文件提交到代码版本仓库,从而保证所有团队开发者以及 CI 环节可以在执行 npm install 时安装的依赖版本都是一致的。
在开发一个 npm包 时,你的 npm包 是需要被其他仓库依赖的,由于上面我们讲到的扁平安装机制,如果你锁定了依赖包版本,你的依赖包就不能和其他依赖包共享同一 semver 范围内的依赖包,这样会造成不必要的冗余。所以我们不应该把package-lock.json 文件发布出去( npm 默认也不会把 package-lock.json 文件发布出去,当然如果非要用package-lock.json也是可以的)。
附:npm 拉包机制

1. 检查并获取 npm 配置,这里的优先级为:项目级的 .npmrc 文件 > 用户级的 .npmrc 文件> 全局级的 .npmrc 文件 > npm 内置的 .npmrc 文件。 2. 检查项目中是否有 package-lock.json 文件。 3. 3.1 如果有,则检查 package-lock.json 和 package.json 中声明的依赖是否一致: a: 一致,直接使用 package-lock.json 中的信息,从缓存或网络资源中加载依赖; b: 不一致,按照 npm 版本进行处理(不同 npm 版本处理会有不同,具体处理方式如图所示)。 3.2 如果没有,则根据 package.json 递归构建依赖树。然后按照构建好的依赖树下载完整的依赖资源,在下载时就会检查是否存在相关资源缓存: a: 存在,则将缓存内容解压到 node_modules 中; b: 否则就先从 npm 远程仓库下载包,校验包的完整性,并添加到缓存,同时解压到 node_modules。 4. 生成 package-lock.json。
