Skip to main content

版本管理

核心结论:日常开发用 ^ + lock 文件就够了;Monorepo 内部包用 Changesets;外部依赖升级用 Renovate / Dependabot 自动化;绝不在 package.json* / latest

一、避坑指南(必看)

  1. lock 文件必须 commit,且 CI 用 --frozen-lockfile

    pnpm install --frozen-lockfile    # CI 必备

    没 lock = 每次安装可能拿到不同版本 = 线上「本地能跑,部署就炸」的元凶。

  2. 禁止 * / latest / 不写版本

    // ❌ 错误:等于把命运交给上游
    { "dependencies": { "react": "latest", "axios": "*" } }

    // ✅ 正确:用 ^ 配合 lock 锁住精确版本
    { "dependencies": { "react": "^18.2.0", "axios": "^1.6.0" } }
  3. 升级前必看 CHANGELOG,尤其是 ^ 跳 major 的情况。^1.0.0^2.0.0自动跨大版本的,必须人工确认。

  4. Monorepo 内部包用 workspace:*

    // apps/web/package.json
    {
    "dependencies": {
    "@my/ui": "workspace:*" // ✅ 始终用工作区最新
    }
    }

    发包时 Changesets 会自动把 workspace:* 替换成具体版本号。

  5. peerDependencies 版本范围要宽松

    // ❌ 太死板,宿主升级你就废了
    { "peerDependencies": { "react": "18.2.0" } }

    // ✅ 兼容同 major 即可
    { "peerDependencies": { "react": ">=18.0.0" } }
  6. 不要手动改 lock 文件:lock 由包管理器生成,手工改 100% 引发幽灵依赖或循环引用

  7. 核心依赖(react / vue)全仓统一版本,避免多实例问题:

    // 根 package.json 统一装,子包不要再装
    { "devDependencies": { "react": "^18.2.0" } }

二、版本范围语法(必背)

写法含义示例匹配范围
1.2.3精确版本1.2.31.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.31.2.3 及以上
>=1.2.3 <2.0.0区间(最稳>=1.2.3 <2.0.0限定范围
1.2.x / 1.x通配符1.2.x1.2.0 ~ 1.2.99
* / x任何版本(禁止)*任意
latest最新发布(禁止latest任意
`12`逻辑或`1.0.02.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新增功能向后兼容的新特性
patchbug 修复修 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 矩阵⭐⭐⭐⭐⭐
DependabotGitHub 原生零配置,PR 自动维护⭐⭐⭐⭐
ChangesetsCLI + CIMonorepo 版本 + 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+)
resolutionsyarn 版的 overrides
enginesNode 包管理器版本约束
engines.nodeNode 版本(配合 .npmrcengine-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 避坑指南

  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: 字段重命名"
  2. 不要手动改 package.json 的 version 字段:CI 会根据 commit 自动算出来,手改会冲突。

  3. 不要混用多套规范:选一种工具就贯彻到底,不要同时跑 semantic-release 和手动 npm version

10.2 Conventional Commits 速查

type触发版本说明
featminor新功能
fixpatchbug 修复
perfpatch性能优化
refactorpatch重构(不改 API)
docs不发布文档变更
chore不发布构建/工具链变更
test不发布测试相关
! / BREAKING CHANGEmajor破坏性变更

10.3 主流工具对比

工具定位MonorepoCHANGELOGGitHub ReleaseGit 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」这种问题。」

十一、相关文档