聊聊package.json 和 package-lock.json

 

package.json 跟 package-lock.json 的知识点挺多的, 这里只聊一聊部分知识点node

先看下dependencies与devDependencies

npm install 安装依赖的时候,能够经过以下方式,把依赖写入package.jsonreact

npm install --save     或者  npm install -S
npm install --save-dev 或者  npm install -D

dependencies与devDependencies的区别:npm

devDependencies下列出的模块,是咱们开发时用的依赖项,像一些进行单元测试之类的包,好比jest,咱们用写单元测试,它们不会被部署到生产环境。dependencies下的模块,则是咱们生产环境中须要的依赖,即正常运行该包时所须要的依赖项。json

记着这句: "正常运行该包时所须要的依赖项"
后面的package-lock.json 中的 dependencies 对应的就是package.json中的 dependenciesredux

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.二、3.0.一、2.1.1 之间有什么不一样呢。antd

语义化版本规则规定,版本格式为:主版本号.次版本号.修订号,而且版本号的递增规则以下:app

  • 主版本号:当你作了不兼容的 API 修改
  • 次版本号:当你作了向下兼容的功能性新增
  • 修订号:当你作了向下兼容的问题修正

主版本号的更新一般意味着大的修改和更新,升级主版本后可能会使你的程序报错,所以升级主版本号需谨慎,可是这每每也会带来更好的性能和体验。ide

次版本号的更新则一般意味着新增了某些特性,好比 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使用建议

开发系统应用时,建议把 package-lock.json 文件提交到代码版本仓库,从而保证全部团队开发者以及 CI 环节能够在执行 npm install 时安装的依赖版本都是一致的。

在开发一个 npm包 时,你的 npm包 是须要被其余仓库依赖的,因为上面咱们讲到的扁平安装机制,若是你锁定了依赖包版本,你的依赖包就不能和其余依赖包共享同一 semver 范围内的依赖包,这样会形成没必要要的冗余。因此咱们不该该把package-lock.json 文件发布出去( npm 默认也不会把 package-lock.json 文件发布出去,固然若是非要用package-lock.json也是能够的)。