package和package-lock关于版本管理的那些事

今天本来是个好日子的,但由于昨天发版导致了线上问题,今天就要加紧修复bug了。代码都写好了,可是devops发版却老是报错。前端同事整个人都懵了,一直找不到原因,一般devops也是我在帮忙看。就配合同事梳理了一下原因。

今天发版一直报一下错误:

1
2
3
4
5
6
7
8
9
10
11
ERROR  Failed to compile with 1 errors                                                             

error in ./node_modules/async-generator-function/index.js

Module parse failed: Unexpected token (4:91)
You may need an appropriate loader to handle this file type.
|
| // eslint-disable-next-line no-extra-parens, no-empty-function
| const cached = /** @type {import('.').AsyncGeneratorFunctionConstructor} */ (async function* () {}.constructor);
|
| /** @type {import('.')} */

使用npm ls深入分析

1
npm ls async-generator-function --depth=10

发现完整的依赖链

1
2
3
4
5
6
7
webpack-dev-server@2.9.7
└── express@4.21.2
└── qs@6.13.0
└── side-channel@^1.0.6
└── side-channel-map@^1.0.2
└── get-intrinsic@^1.2.5
└── async-generator-function@^1.0.0

我看版本和引用的有些差异,我就问同事,你们今天有升级过依赖包吗,同事说,已经很久没变动过了。这些就奇怪了,为什么没动过,却突然报错了呢?经过分析,在本地编译不会报错,在服务器上编译就会报错,因为服务器上每次重新下载依赖包。

问题现象

当重新下载依赖包时,即使package.json中的版本号没有变化,实际安装的依赖版本可能会发生变化,导致项目运行异常。

原因分析

1. 语义化版本控制(SemVer)

1
"qs": "^6.11.0"
  • ^符号表示兼容主版本的最新版本
  • 实际安装时可能会安装6.11.x系列的最新版本,如6.11.1、6.11.2等
  • 如果6.11系列发布了新版本,重新安装时会自动安装最新版本

2. 依赖树结构变化

  • npm的依赖解析算法可能在不同时间产生不同的依赖树结构
  • 子依赖的版本更新可能影响整个依赖树

3. 包注册表变化

  • npm仓库中的包可能被更新或删除
  • 维护者可能发布补丁版本修复bug

解决方案

方案1:使用package-lock.json(推荐)

步骤:

  1. 确保项目根目录存在package-lock.json文件
  2. 在版本控制系统中跟踪该文件(git add)
  3. 重新安装依赖时使用npm ci命令

优点:

  • 精确锁定所有依赖的版本号
  • 保证团队成员安装完全相同的依赖版本
  • 提高安装速度

操作:

1
2
3
4
5
# 生成package-lock.json
npm install

# 后续安装使用npm ci
npm ci

方案2:使用精确的版本号

修改package.json:

1
"qs": "6.11.0"  // 移除^符号

优点:

  • 确保每次都安装完全相同的版本

缺点:

  • 无法自动获取补丁更新
  • 需要手动更新版本号

方案3:使用npm shrinkwrap

1
npm shrinkwrap

生成的文件:

  • npm-shrinkwrap.json:锁定所有依赖的精确版本

方案4:配置.npmrc文件

创建.npmrc文件:

1
2
save-exact=true
package-lock=true

一般来说,我们都会一个package-lock.json文件来精准锁住版本,但是我看到项目中没有这个文件,我就问同事,你们没有吧package-lock.json提交到Git吗?同事说,一直没有提交过。那这里其实很清晰了,就是因为引用依赖时版本前面是用的^package-lock.json也未提交导致重新下载时,依赖包更新了。

所以现在要解决上面的问题,多重降级 或者 别名替换
这里我们采用的后者

1
2
3
4
5
6
7
8
9
10
11
12
13
// build/webpack.base.conf.js
const path = require('path')

module.exports = {
resolve: {
alias: {
'@': resolve('src'),
// 替换使用ES2018语法的模块
'async-generator-function': path.resolve(__dirname, '../build/empty-module.js'),
'get-intrinsic': path.resolve(__dirname, '../build/empty-module.js')
}
}
}

创建兼容模块

1
2
3
4
5
6
7
8
9
// build/empty-module.js
/**
* 空模块 - 用于替换使用ES2018语法的模块
* 提供兼容的替代实现
*/
module.exports = function() {
// 返回空函数,避免运行时错误
return function() {};
};

最后我们再来回顾一下package.json版本控制策略

1
2
3
4
5
"dependencies": {
"qs": "^6.11.0", // 核心库使用^,^ 表示兼容更新,实际可能安装 6.13.0
"express": "~4.21.0", // 重要库使用~,~ 表示补丁更新,实际可能安装 4.21.2
"webpack": "3.10.0" // 无符号表示精确版本
}

注意版本的管理,不然哪一天项目可能突然就启动不起来了。

package和package-lock关于版本管理的那些事

https://blogs.52fx.biz/posts/2158816184.html

作者

eyiadmin

发布于

2025-09-30

更新于

2025-09-30

许可协议

评论