npm install package-lock.json 的更新策略

14,788 阅读5分钟

npm lockfiles

为什么需要 lockfiles?

npm install 的输入是 package.json,它的输出是一棵 node_modules 树。理想情况下,npm install 应该像纯函数一样工作,对于同一个 package.json 总是生成完全相同的 node_modules 树。在某些情况下,确实如此。但在其他很多情况中,npm 无法做到这一点。有以下原因:

  • 不同版本的 npm 的安装算法不同。
  • 某些依赖项自上次安装以来,可能已发布了新版本,因此将根据 package.json 中的 semver-range version 更新依赖。
  • 某个依赖项的依赖项可能已发布新版本,即使您使用了固定依赖项说明符(1.2.3 而不是 ^1.2.3),它也会更新。

为了在不同的环境下生成相同的 node_modules,npm 使用 package-lock.json 或 npm-shrinkwrap.json。这两个文件都被称为 lockfiles。无论何时运行 npm install,npm 都会生成或更新 lockfiles。以下只讨论其中的 package-lock.json。

不同 npm 版本下 npm i 的规则

  • npm 5.0.x 版本:不管 package.json 中依赖是否有更新,npm i 都会根据 package-lock.json 下载。针对这种安装策略,有人提出了这个 issue - #16866 ,然后就演变成了 5.1.0 版本后的规则。

  • 5.1.0 版本后:当 package.json 中的依赖项有新版本时,npm install 会无视 package-lock.json 去下载新版本的依赖项并且更新 package-lock.json。针对这种安装策略,又有人提出了一个 issue - #17979 ,参考 npm 贡献者 iarna 的评论,得出 5.4.2 版本后的规则。

  • 5.4.2 版本后:

    • 如果只有一个 package.json 文件,运行 npm i 会根据它生成一个 package-lock.json 文件。

    • 如果 package.json 的 semver-range version 和 package-lock.json 中版本兼容,即使此时 package.json 中有新的版本,执行 npm i 也还是会根据 package-lock.json 下载 - 实践场景1。

    • 如果手动修改了 package.json 的 version ranges,且和 package-lock.json 中版本不兼容,那么执行 npm i 时 package-lock.json 将会更新到兼容 package.json 的版本 - 实践场景2。

实践

npm 版本:6.4.1

场景1
  • 假设刚从远程仓库克隆一个项目,此时本地 node_modules 还不存在,package.json 和 package-lock.json 如下。已知 superagent 3.x.x 的最新版本是 3.8.3,那么运行 npm install 是根据 package-lock.json 中指定的版本 3.5.1 去下载还是根据 package.json 去下载最新的 3.x.x ?

    // package.json
    "dependencies": {
        "superagent": "^3.5.1"
    }
    
    // package-lock.json
    {
      "superagent": {
          "version": "3.5.1",
          "resolved": "https://npm.garenanow.com/superagent/-/superagent-3.5.1.tgz",
          "integrity": "sha1-Ck+u/aM2d3d4iDR917TSH0EMhxs=",
          "requires": {
            "component-emitter": "^1.2.0",
            "cookiejar": "^2.0.6",
            "debug": "^2.2.0",
            "extend": "^3.0.0",
            "form-data": "^2.1.1",
            "formidable": "^1.1.1",
            "methods": "^1.1.1",
            "mime": "^1.3.4",
            "qs": "^6.1.0",
            "readable-stream": "^2.0.5"
          }
        },
    }
    

    结论:下载的是 3.5.1。此时 package.json 和 package-lock.json 同时存在,package.json 的 version 是 ^3.5.1,package-lock.json 的 version 是 3.5.1,并且当前 node_modules 中下载的也是 3.5.1

场景2
  • 接着场景1,然后手动修改 package.json 中 superagent 的版本为 ^5.1.0,再执行 npm i,发现不管有没有删除已有的 node_modules,package-lock.json 中 superagent 的版本都变成了 5.1.0,node_modules 中的也变成了 5.1.0

参考

npm ci

介绍

  • ci:Continuous Integration。
  • npm 版本至少是 v5.7.1
  • 此命令与 npm install 类似,不同之处在于它旨在用于自动化环境,例如集成测试环境、线上环境、或者您希望确保干净安装依赖项的任何情况。通过跳过某些面向用户的功能,它可以比常规的 npm install 快得多。它也比常规安装更严格,它可以捕获由于本地环境的增量安装引起的错误或不一致。

npm ci 和 npm install 差异

  • 项目必须存在 package-lock.json 或 npm-shrinkwrap.json。
  • 如果 lockfiles 中的依赖和 package.json 中不匹配,npm ci 会退出并且报错,而不是去更新 lockfiles。
  • npm ci 只能安装整个项目的依赖,无法安装单个依赖。
  • 如果 node_modules 已经存在,它将在 npm ci 开始安装之前自动删除。
  • npm ci 永远不会改变 package.json 和 package-lock.json。

补充

  • npm install 读取 package.json 创建依赖项列表,并使用 package-lock.json 来通知要安装这些依赖项的哪个版本。如果某个依赖项在 package.json 中,但是不在 package-lock.json 中,运行 npm install 会将这个依赖项的确定版本更新到 package-lock.json 中。

  • npm ci 是根据 package-lock.json 去安装确定的依赖,package.json 只是用来验证是不是有不匹配的版本,假设 package-lock.json 中存在一个确定版本的依赖 A,如果 package.json 中不存在依赖 A 或者依赖 A 版本和 lock 中不兼容,npm ci 就会报错。

参考

补充

最近在开发一个新需要,需要使用 echarts,使用 npm install echarts -S 下载 echarts 后,发现 package-lock.json 中很多其他包的 resolved 字段变成了 registry.npmjs.org,具体说明如下 :

  • 首先查询本地的 npm 源:npm config get registry

  • 然后下载 echarts:npm install echarts -S;package-lock.json 会更新。package-lock.json 部分前后对比图:左边是更新前的,右边是更新后的,主要的变化就是 resolved 字段从 npm.garenanow.com 变成了 registry.npmjs.org。

    node_modules 下 @babel/generator 的 package.json 部分截图:

    node_modules 下 @babel/helper-annotate-as-pure 的 package.json 部分截图:

  • 结论:node_modules 下的每个包的 package.json 中指定了 _resolved 字段,当去更新 package-lock.json 时,会去 copy 这个字段,而不是当前 npm 指定的源。很有可能是在初始化项目、一开始执行 npm install 的时候的源是 registry.npmjs.org,后面即使你更新了源,也不会改变已经下载的包的 _resolved 字段。可以通过删掉整个 node_modules 重新下载解决这个问题,当然最好的方式是在项目根目录下创建一个 .npmrc 的文件,在其中指定 npm 源,以保证团队的成员使用的是统一的 npm 源,这样就不会在拉取代码的时候出现 package-lock.json 中有很多冲突的情况。

其他文章链接

TypeScript 踩坑之旅

[译文]一步步构建发布一个 TypeScript NPM 包

npm install package-lock.json 的更新策略

[译]浏览器语言首选项

Date.prototype.toLocaleString()