版本管理
核心结论:日常开发用
^+ lock 文件就够了;Monorepo 内部包用 Changesets;外部依赖升级用 Renovate / Dependabot 自动化;绝不在package.json写*/latest。
一、避坑指南(必看)
lock 文件必须 commit,且 CI 用
--frozen-lockfile:pnpm install --frozen-lockfile # CI 必备没 lock = 每次安装可能拿到不同版本 = 线上「本地能跑,部署就炸」的元凶。
禁止
*/latest/ 不写版本:// ❌ 错误:等于把命运交给上游
{ "dependencies": { "react": "latest", "axios": "*" } }
// ✅ 正确:用 ^ 配合 lock 锁住精确版本
{ "dependencies": { "react": "^18.2.0", "axios": "^1.6.0" } }升级前必看 CHANGELOG,尤其是
^跳 major 的情况。^1.0.0→^2.0.0是自动跨大版本的,必须人工确认。Monorepo 内部包用
workspace:*:// apps/web/package.json
{
"dependencies": {
"@my/ui": "workspace:*" // ✅ 始终用工作区最新
}
}发包时 Changesets 会自动把
workspace:*替换成具体版本号。peerDependencies版本范围要宽松:// ❌ 太死板,宿主升级你就废了
{ "peerDependencies": { "react": "18.2.0" } }
// ✅ 兼容同 major 即可
{ "peerDependencies": { "react": ">=18.0.0" } }不要手动改 lock 文件:lock 由包管理器生成,手工改 100% 引发幽灵依赖或循环引用。
核心依赖(react / vue)全仓统一版本,避免多实例问题:
// 根 package.json 统一装,子包不要再装
{ "devDependencies": { "react": "^18.2.0" } }
二、版本范围语法(必背)
| 写法 | 含义 | 示例 | 匹配范围 | ||||
|---|---|---|---|---|---|---|---|
1.2.3 | 精确版本 | 1.2.3 | 仅 1.2.3 | ||||
^1.2.3 | 兼容 major(默认) | ^1.2.3 | >=1.2.3 <2.0.0 | ||||
~1.2.3 | 兼容 minor | ~1.2.3 | >=1.2.3 <1.3.0 | ||||
>=1.2.3 | 大于等于 | >=1.2.3 | 1.2.3 及以上 | ||||
>=1.2.3 <2.0.0 | 区间(最稳) | >=1.2.3 <2.0.0 | 限定范围 | ||||
1.2.x / 1.x | 通配符 | 1.2.x | 1.2.0 ~ 1.2.99 | ||||
* / x | 任何版本(禁止) | * | 任意 | ||||
latest | 最新发布(禁止) | latest | 任意 | ||||
| `1 | 2` | 逻辑或 | `1.0.0 | 2.0.0` | 满足任一 |
^ 的隐藏陷阱:
"^0.1.0" // 实际匹配 >=0.1.0 <0.2.0 ← 0.x 版本只锁 minor!
"^0.0.1" // 实际匹配 >=0.0.1 <0.0.2 ← 0.0.x 几乎等于精确
"^1.0.0" // 实际匹配 >=1.0.0 <2.0.0 ← 1.x 才是常理解释
面试高频:
^0.x和^1.x行为不同。react@18.0.0已发布后,^17.0.0不会自动升到 18,这是 major 保护机制。
三、SemVer(语义化版本)
major.minor.patch[-pre-release][+build]
1 .2 .3 -rc.1 +20260101
| 段位 | 含义 | 升级时机 |
|---|---|---|
| major | 破坏性变更 | API 不兼容、删字段 |
| minor | 新增功能 | 向后兼容的新特性 |
| patch | bug 修复 | 修 bug、性能优化(不改 API) |
-rc.1 | 预发布 | alpha / beta / rc |
+xxx | 构建元数据 | 编译时间、commit hash,不影响版本比较 |
预发布版本优先级:1.0.0-alpha < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0
安装
1.0.0默认不会装1.0.0-rc.1,必须显式指定。
四、dist-tag(发布标签)
dist-tag 是「指向某个版本的指针」,类似 git 的 tag。
| 标签 | 用途 | 命令 |
|---|---|---|
latest | 默认,稳定版 | npm publish(不指定时默认) |
next | 下个 major 的预演 | npm publish --tag next |
beta / alpha | 内测版本 | npm publish --tag beta |
legacy | 老版本维护分支 | 手动 dist-tag add |
# 查看所有 dist-tag
npm dist-tag ls <pkg>
# 用户安装 next 版
npm install <pkg>@next
# 撤回发布(72 小时内)
npm unpublish <pkg>@<version>
五、升级工具
1. 查看过期依赖
pnpm outdated # 看哪些包有新版本
pnpm outdated --filter "@my/*" # 只看 scope 内
2. 批量升级
# 升级 lock 中所有包到 lockfile 允许的最新(不会改 major)
pnpm update
# 升级到最新(含 major)→ 改 package.json
pnpm add <pkg>@latest
3. ncu(npm-check-updates)—— 最强交互式升级
pnpm dlx ncu # 列出可升级
pnpm dlx ncu -i # 交互式选择
pnpm dlx ncu -u # 直接改 package.json
推荐在
package.json用^,跑ncu一次升完,再 commit lock,PR 评审 diff。
六、自动化(团队必备)
| 工具 | 平台 | 特点 | 推荐度 |
|---|---|---|---|
| Renovate | 自托管 / GitHub | 可配置性强、支持 monorepo 矩阵 | ⭐⭐⭐⭐⭐ |
| Dependabot | GitHub 原生 | 零配置,PR 自动维护 | ⭐⭐⭐⭐ |
| Changesets | CLI + CI | Monorepo 版本 + Changelog | ⭐⭐⭐⭐⭐ |
Renovate 配置示例(renovate.json):
{
"extends": ["config:base"],
"packageRules": [
{
"matchUpdateTypes": ["major"],
"enabled": false // major 升级禁止自动 PR
},
{
"matchPackagePatterns": ["^@my/"],
"groupName": "内部包"
}
]
}
七、面试话术
「版本管理分三层:日常开发用
^配合 lock 文件锁住精确版本,CI 加--frozen-lockfile防篡改;Monorepo 内部包用workspace:*+ Changesets 自动化 bump + 生成 CHANGELOG;外部依赖升级靠 Renovate 自动开 PR,但 major 版本禁止自动合,必须人工跑测试。」
八、字段速查(字典式参考)
按需检索。
8.1 package.json 版本字段
| 字段 | 用途 |
|---|---|
dependencies | 生产依赖(^/~ 范围) |
devDependencies | 开发依赖 |
peerDependencies | 宿主依赖(用 >= 宽松范围) |
optionalDependencies | 可选依赖(装失败不报错) |
overrides | 强制覆盖子依赖版本(npm 8+) |
resolutions | yarn 版的 overrides |
engines | Node 包管理器版本约束 |
engines.node | Node 版本(配合 .npmrc 的 engine-strict) |
8.2 常用命令
# 查看
pnpm outdated # 过期依赖
npm view <pkg> versions # 所有版本
npm view <pkg> dist-tags # 所有 dist-tag
# 安装
pnpm add <pkg>@<version> # 装指定版本
pnpm add <pkg>@<tag> # 装 dist-tag
# 升级
pnpm update # 按 lock 范围升
pnpm dlx ncu -u # 升级 package.json
# 发布
npm publish # → latest
npm publish --tag beta # → beta
npm dist-tag add <pkg>@1.2.3 latest
十、项目自身的版本与发布自动化
核心结论:库(package)用 Semantic Release;单仓多包用 Changesets;纯文档/应用型项目用 release-please 或手 bump。底层都依赖 Conventional Commits 规范。
10.1 避坑指南
必须用 Conventional Commits,否则自动化工具无法判断 bump 级别:
# ❌ 错误:无法识别
git commit -m "修复了登录的 bug"
# ✅ 正确:type 决定 bump 级别
git commit -m "fix: 修复登录页 token 失效的 bug"
git commit -m "feat: 新增第三方登录"
git commit -m "feat!: 移除旧版 API"
git commit -m "feat: 重构菜单 #BREAKING CHANGE: 字段重命名"不要手动改
package.json的 version 字段:CI 会根据 commit 自动算出来,手改会冲突。不要混用多套规范:选一种工具就贯彻到底,不要同时跑 semantic-release 和手动
npm version。
10.2 Conventional Commits 速查
| type | 触发版本 | 说明 |
|---|---|---|
feat | minor ⬆ | 新功能 |
fix | patch ⬆ | bug 修复 |
perf | patch | 性能优化 |
refactor | patch | 重构(不改 API) |
docs | 不发布 | 文档变更 |
chore | 不发布 | 构建/工具链变更 |
test | 不发布 | 测试相关 |
! / BREAKING CHANGE | major ⬆ | 破坏性变更 |
10.3 主流工具对比
| 工具 | 定位 | Monorepo | CHANGELOG | GitHub Release | Git Tag | 推荐场景 |
|---|---|---|---|---|---|---|
| Semantic Release | 库(package) | 弱 | ✅ | ✅ | ✅ | 发 npm 包 |
| Changesets | 单仓多包 | 强 | ✅ | ✅ | ✅ | Monorepo 内部包 |
| release-please | 应用 / 通用 | 中 | ✅ | ✅ | ✅ | GitHub 项目(Google 出品) |
| standard-version | 简单本地 bump | 弱 | ✅ | ❌ | ✅ | 个人小项目(已不太维护) |
| np | 交互式发布 | 弱 | ❌ | ❌ | ❌ | 临时手动发版 |
2024+ 事实标准:库用 semantic-release,monorepo 用 changesets,GitHub 应用用 release-please。
10.4 Semantic Release 实战
适用场景:发到 npm 的库(如组件库、工具库)。
安装:
pnpm add -D semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/npm
配置文件 release.config.js:
module.exports = {
branches: ["main"],
plugins: [
"@semantic-release/commit-analyzer", // 解析 commit → 算版本
"@semantic-release/release-notes-generator", // 生成 CHANGELOG
"@semantic-release/changelog",
"@semantic-release/npm", // publish 到 npm
"@semantic-release/git", // commit + tag
"@semantic-release/github", // 创建 GitHub Release
],
};
CI 触发(.github/workflows/release.yml):
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # 关键:需要完整 git 历史
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
工作流:merge 到 main → CI 读 commit → 算出版本 → 改 package.json → 写 CHANGELOG → npm publish → 打 git tag → 发 GitHub Release。全自动化。
10.5 Changesets 实战(monorepo)
适用场景:一个仓里多个包要独立发版(Vercel、字节内部都用)。
初始化:
pnpm add -D @changesets/cli
pnpm changeset init # 生成 .changeset/config.json
.changeset/config.json:
{
"$schema": "https://unpkg.com/@changesets/schema/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch"
}
工作流:
# 1. 改完代码后,声明改了哪些包
pnpm changeset
# 交互式:选包 → 选 bump 类型(major/minor/patch)→ 写 changelog
# 会生成 .changeset/random-hash.md,**这个文件要 commit**
# 2. CI 合并后,自动跑 version(也可手动)
pnpm changeset version
# 自动改所有子包 package.json + 生成 CHANGELOG.md
# 3. 自动发包
pnpm changeset publish
与 semantic-release 区别:
- semantic-release:看 commit 自动算(开发体验好,但 commit 必须规范)
- changesets:改代码时声明(显式控制更灵活,monorepo 必备)
10.6 选型 Checklist
Q1: 是发 npm 库吗?
├─ 是 → Q2
└─ 否(应用/纯文档)→ 用 release-please 或手动 bump
Q2: 单包还是多包?
├─ 单包 → Semantic Release
└─ 多包 → Changesets
Q3: 团队能贯彻 Conventional Commits 吗?
├─ 能 → Semantic Release
└─ 不能/嫌麻烦 → Changesets(手动声明式)
10.7 面试话术
「项目自身的版本管理有三种主流方案:库用 Semantic Release,靠 Conventional Commits 自动算版本 + 发包;Monorepo 用 Changesets,每个 PR 显式声明改哪些包,避免一个 bug 误 bump 整个仓;应用型项目用 release-please,走 GitHub Action 即可。核心都是把版本号从「人记」变成「机器算」,杜绝「发了 1.0.2 才意识到漏改 CHANGELOG」这种问题。」