likes
comments
collection
share

因为依赖问题加班后,我写了这么个小工具……

作者站长头像
站长
· 阅读数 21

前段时间,我碰到了一个让人非常困惑的问题——我在自己本地开发环境和其它同事的环境上运行的代码,跟在打包之后线上的代码效果不一致。这让我很是困惑,我本地的代码跟线上的代码完全一致,package.json 也一模一样,那么讲道理,输出的 node_modules 也是一样的,两者的效果肯定是相同的才对。结果经过了多次的清缓存和重新部署却依然没有解决这个问题。

当时已是晚上 7 点,女票发来微信问我“什么时候下班呀”。而这个问题必须今天解决了,因为明天就要提测 uat 环境了,不然就要延期影响迭代了。

一边是产品经理催了再催的需求,一边是女票约了又约的饭局,真的是自古忠孝两难全,世上安得两全法……

在经过熟练的道歉之后,我在公司楼下吃了个便饭,便上楼接着死嗑。在查阅了一些资料之后有了一些头绪。果然是本地环境中的 node_modules 跟服务器中的有差异。服务器部署的时候,执行了安装依赖的命令(如:npm install),npm 会尝试从 package-lock.json 文件中下载依赖,如果没有该文件,则通过 package.json 文件的依赖规则去下载对应版本的依赖包。将 package-lock.json跟线上的对比了一下,发现确实有出入。自此一个悬案告破。

欲知此事前因后果,还等我慢慢道来。首先了解一下前置知识。

package.json

package.json 文件除了描述项目的一些基本信息、scripts 脚本之外,最重要的一个就是告诉 node 项目中使用到的相关依赖和依赖的版本号。

package.json 的依赖中,版本号使用的是semver 版本表示法,即“主版本.次版本.补丁版本”的格式,版本号的递增规则如下:

  • 主版本号,一些不兼容的 breaking change
  • 次版本号,能向下兼容,在不影响低版本的使用下可以新增和弃用一些 api,但要确保向下兼容
  • 补丁版本号,一些向下兼容的修正,一般都是对于一些缺陷的修复 当发布新版本的时候,可不能随心所欲地增加数字,一定要遵循上述的规则,这是作为一个合格的项目所必备的基本条件。 同时,npm 也设置了一些规则,用于在运行 npm update 的时候,为 package.json 中的依赖更新到尽可能新的版本号,你一定不陌生:
  • : 只更新修订号,用于静默获取一些包中对于 bug 修复的最新版本
  • ^: 只执行不更新最左边非零数字版本号的更新。例如 ^0.1.0 ,可以更新到 0.1.1 0.1.2 等,但不会更新到 0.2.0 或更高版本;而 1.0.0 可以更新到 1.0.11.1.0 等,但不会更新到 2.0.0 或更高版本
  • >: 只接受高于指定版本的任何版本
  • : 只接受高于或等于指定版本的任何版本
  • <: 只接受低于指定版本的任何版本
  • : 只接受低于或等于指定版本的任何版本
  • =: 接受确切版本
  • -: 接受一定范围的版本,如:2.1.0-2.6.2
  • ||: 组合集合,如: < 2.1 || > 2.6
  • latest: 使用可用的最新版本
  • 无符号,等同于 =

package lock 文件

在 npm 5 版本中,引入了 package-lock.json 文件,其它的包管理工具也有其对应的 lock 文件,如 yarnyarn.lockpnpmpnpm-lock.yamlpackage-lock.json 文件相比 package.json 文件,不仅可以跟踪项目中使用到的 npm 包,还能跟踪每个 npm 包的确切版本,以确保产品可以拥有完整且相同的 node_modules 树,产品的表现形式一致。 正如上述关于 package.json 依赖工作的描述,package.json 一直都存在着一个比较尴尬的问题,即在运行 npm update 或者 npm install 的时候,会尽可能地安装最新的依赖,如果虽然补丁版本和次版本不应该引入重大的更改,但并不是所有的开源项目的作者都是遵守 semver 规则(我相信有些人可能都还不知道这东东),免不了的,还是有可能会产生 bug。 那如果在服务器的 ci 服务器,每次都是先 npm install 安装依赖再执行打包构建的时候,不就有可能会产生问题了吗? 这里就不得不先提一下 npm ci 这个命令了,npm cinpm install 不同。看到 ci,也应该能想到持续集成中的这个 ci。没错啦,这个就是专门用于 CI/CD 中的安装依赖操作。 npm ci 相比 npm install 命令,有几个特点:

  1. npm 版本要 ≥ v5.7.1
  2. 不会更改 package.jsonpackage-lock.json 文件
  3. 执行的之前如果存在 node_modules ,会先将之删除
  4. 优先依赖于 package-lock.jsonnpm-shrinkwrap.json 文件安装依赖
  5. 如果在以上两个文件中的依赖在 package.json 中找不到,就抛错退出执行 这些特性确保了相同的 lock 文件能拥有相同的依赖环境,避免在构建的时候出现与开发时环境不一致的情况。

还原案件真相

了解了这些坑点,回顾下我那晚遇到的问题,很明显地,就是我本地与测试服务器之间,npm lock 文件不一致导致的问题。那么为啥好端端的 lock 文件会被人更改了呢? 原来是早些时候,在我合了代码之后,组里的某个小可爱,今天带了他自己的私人电脑来公司开发(公司的电脑比较一言难尽),结果他电脑的 npm 版本是 v7 以上的,而我们项目中使用的 npm 是 v6 的版本,这两个版本的 npm 生成的 lock 文件相差极大,导致 lock 文件多达 2000+个 changes(基本等于改头换面了 🤦),在 mr 的时候回撤回来,但是 lock 文件却丢失了我的变动,才导致了今晚的问题。

撸了个小工具

回家的路上想了很多,组里面相关的文档和规范都很齐全,基本开发上容易导致不一致的情况都事无巨细地规定了一遍。但文档是死的,人是活的,人不是机器,即使再小心还是可能会犯错,我是不是可以做点什么来帮助我们规避这种疏忽。路上路过一家重庆麻辣烫,先打包了一份当作宵夜给女票赔罪。 找 package.json 的资料的时候,意外地发现了 scripts 里的 preinstall 钩子,可以在包管理工具安装之前执行一些脚本操作,我灵机一动,我在这个钩子里判断包管理工具和其对应的版本号,如果不符合条件(比如项目规定用npm@6.14.11 管理依赖,那么使用了 yarn 或者 npm@7 来安装依赖)我就狠狠地报错,那不就可以把这些错误都扼杀在摇篮里了。 在周末的时候实现了自己想要功能。使用非常之简单,在需要控制包管理工具的项目的 package.jsonscripts 增加一个这样的钩子:

{
  "preinstall": "npx pm-keeper npm"
}

也可以控制 npm 的版本号

{
  "scripts": {
    "preinstall": "npx pm-keeper npm@6.14.11",
    // or
    "preinstall": "npx pm-keeper npm 6.14.11"
  }

}

也可以这些新增一个 pmKeeper 的配置项

{
  "scripts": {
    "preinstall": "npx pm-keeper"
  },
  "pmKeeper": {
    "name": "npm",
    "version": "6.14.1"
}

如果用了不规范包管理器或版本的时候,我就抛错 😏

使用 yarn 安装的情况 因为依赖问题加班后,我写了这么个小工具……

使用了不在规范内版本的 npm 安装的情况 因为依赖问题加班后,我写了这么个小工具……

就是这么的简单,这个包现在也在 github 上开源(可以的话来个不要钱的 star🙏🏻),也发布到了 npm,欢迎交流。

参考资料

[1] package.json: nodejs.cn/learn/the-p…

[2] semver 版本表示法: semver.org

[3] package-lock.json: nodejs.cn/learn/the-p…