軟體工程基本功:用 Semver 駕馭版本控制的藝術
論版本號的重要性,從一個掉進坑裡的故事說起
我剛社會第一年的時候,對套件版本隱藏的相容性問題沒什麼概念,還記得當時我看到一個平常在用的套件發布了最新版,心想:
「哦~有新版本釋出了!第五版耶,官網上描述的新功能看起來挺酷的,更新來玩玩看好了。」
我就不假思索,直接把我專案裡原本用的第四版套件升級到第五版。誰知道這麼一升級,我整個專案就跑不起來了。翻了一下文件才發現,原本我用的某些 API 在第五版已經被移除了,甚至連套件的引入方式都整個改掉了。
舉例來說,原本這樣寫是沒問題的:
import functionA from 'some-library';
但新版本強制使用 named import,變成要這樣寫才行:
import { functionA } from 'some-library';
後來我才知道,這是因為這個套件升級的是「大版號(Major)」,代表它可能包含破壞性變更(breaking changes),使用方式可能會跟之前不一樣。也正是從這次經驗開始,我才慢慢了解版本號其實不是只是個數字而已,而是有一套設計邏輯與意圖,背後還有「語意化版本(Semantic Versioning)」這個規則存在。
後來,我在工作上也接觸到了開發與維護套件的任務,成為了套件與原始碼的維護方,Semantic Versioning 這套軟體版本管理的原則變得更加重要,因為套件版本管理的好與壞會直接影響到團隊協作、使用者體驗,甚至是產品的穩定性。這篇文章我想帶大家一起來探索 Semantic Versioning 的核心概念與我在實際維護套件時的一些經驗,礙於篇幅可能沒辦法涵蓋到 Semantic Versioning 的所有細節,建議讀者有時間的話可以搭配 Semantic Versioning 2.0.0 官方文件 一起閱讀。
語意化版本(Semantic Versioning)是什麼?基礎規則與範例一次搞懂
版本號其實是種「溝通的語言」,那麼這個語言是怎麼設計的呢?這就要介紹 Semantic Versioning(簡稱 SemVer)。
語意化版本的格式:MAJOR.MINOR.PATCH
SemVer 採用一種簡單的三位數格式:
MAJOR.MINOR.PATCH
各位數的意義如下:
欄位 | 說明 |
---|---|
MAJOR | 破壞性變更:不相容的 API 變更 |
MINOR | 新增功能:向下相容的功能擴充 |
PATCH | 修復錯誤:向下相容的 bug 修正 |
來看幾個簡單的例子:
1.2.3
→1.2.4
:修了一個小 bug(patch)1.2.3
→1.3.0
:加入一個新功能(minor)1.2.3
→2.0.0
:有破壞性變更,升級需特別注意(major)
SemVer 的核心精神是「傳遞改動的意圖」。給出版本號,不只是「程式寫完了」,更是告訴使用者:「這次我改了什麼,影響到你嗎?」
預發布版本與建構元資料
除了主版本號,SemVer 還支援額外的標記,用來描述還沒正式發布或內部用途的預發布版本(Pre-release):
格式為:1.0.0-<標記>
常見標記有:
1.0.0-alpha
:非常早期,可能還不穩1.0.0-beta
:比 alpha 穩定,仍可能有問題1.0.0-rc.1
(release candidate):候選版本,準備好要正式釋出了
📌 預發布版本不會自動被安裝,除非你特別指定,例如 npm install some-lib@beta
該升級哪個版本號?用元件庫範例學會判斷 Patch / Minor / Major
上一章我們學會了 版本號三個欄位的意義,這章就要帶大家進入實務場景,用 Component Library 的範例,幫你建立「版本直覺」。判斷邏輯其實可以簡化成一個簡單的直覺:
會壞就 bump
major
,加功能就 bumpminor
,修 bug 就 bumppatch
。
🔴 大版號(Major, X.0.0
):破壞性變更(Breaking Changes)
當你做出「升級後原本的寫法會壞掉」的改動,就屬於破壞性變更,應該 bump MAJOR
。
✅ 刪除或修改現有 API
// v1.x 寫法
<Button variant="outlined" />
// v2.0.0 移除 `variant` prop
<Button /> // 🚨 升級後錯誤:variant 不被支援
✅ 改變命名或使用方式
// v1.x
<Modal isOpen={true} />
// v2.0.0 改為 open(更貼近 HTML 標準)
<Modal open={true} /> // 🚨 需要改程式碼才能用
✅ 改變元件的預設行為
- 原本
Table
預設是 server-side pagination,改為 client-side → 會直接改變使用者的畫面與資料邏輯。
✅ 改變 TypeScript 類型(造成型別不相容)
// v1.x
type InputProps = {
value: string | number;
}
// v2.0.0 僅接受 string
type InputProps = {
value: string;
}
這會導致原本傳 number
的使用者出現型別錯誤。
只要升級會讓現有的寫法不能用,就是 breaking change。
🟡 中版號(Minor, X.Y.0
):新增功能但相容
只要是加東西但不破壞原有功能,通常都可以放心 bump MINOR
。
✅ 新增新的 prop
// v1.2.0
<Tooltip content="Hi" />
// v1.3.0 新增 delay prop
<Tooltip content="Hi" delay={300} /> // ✅ 舊寫法照常可用
✅ 新增支援的事件
// v1.2.0
<Button onClick={handleClick} />
// v1.3.0 新增 onDoubleClick
<Button onClick={handleClick} onDoubleClick={handleDoubleClick} />
✅ 新增樣式選項、變體
// v1.3.0 新增 variant="secondary"
<Button variant="secondary" />
✅ 新增元件
// v1.4.0 加入 AvatarGroup
<AvatarGroup>
<Avatar src="user1.jpg" />
<Avatar src="user2.jpg" />
</AvatarGroup>
新增功能,但不會讓使用者既有程式碼壞掉,就是 minor。
🟢 小版號(Patch, X.Y.Z
):錯誤修正與內部優化
這類改動的特點是:使用者的寫法不變,但效果更好或問題被修正了。
✅ 修 bug
// v1.2.3: Modal 的 onClose 不會觸發(bug)
// v1.2.4 修正後
<Modal open={true} onClose={() => console.log('關閉')} />
使用方式完全沒改,但行為正確了。
✅ 修樣式問題
- 修正
Button
在 Firefox 中 padding 過大的問題。
✅ 效能優化、不影響 API 的重構
Table
加上虛擬化,提升渲染效能- 移除多餘的 re-render,但對外部 API 無影響
✅ 改進 TypeScript 定義(不影響現有用法)
// v1.2.3
type TextFieldProps = {
onChange: (value: any) => void;
}
// v1.2.4
type TextFieldProps = {
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
對使用者來說可選擇更明確的型別,但舊寫法仍可用。
✅ 升級相依套件但不影響對外行為
chore(deps): update react to 18.2.0
使用者感受不到改動,但行為更正確、更穩定,就是 patch。
Semantic Versioning 常見問題整理
到了這裡,相信你對 Semantic Versioning 已經有一定掌握了。不過在實務中,還是常會遇到一些讓人搞混的地方。本章我們就用 常見問題 FAQ 的方式,快速釐清一些細節與 誤解。
❓我只改了內部邏輯,沒改 API,還需要 bump 版本號嗎?
✅ 需要。
即使 API 沒變,但如果改動可能導致元件行為變化(例如回傳值不同、預設行為變了),就應該 bump 版本,視影響程度選擇適當欄位。
📌 常見錯誤:「內部邏輯不算 API」
👉 實際上,行為也是一種 API。只要使用者感受得到變化,就要負責。
❓已經 deprecated 的功能,如果移除了,算是 breaking change 嗎?
✅ 是,還是 breaking change,應該 bump Major。
就算功能早已標註 deprecated,只要使用者還能用,就不能隨意移除。移除的那一刻,仍然是破壞性改動。
⚠️ 重要觀念:deprecated 是預告,不代表你可以跳過版本規則。
❓我新增了一個參數(prop),但改變了元件 預設行為,這樣算 breaking change 嗎?
🟠 視情況而定,但通常應該算是 breaking change。
舉例來說:
// 原本 Tooltip 預設永遠顯示在右側
<Tooltip content="Hi" />
// 新版新增 `placement`,但預設改成 bottom
<Tooltip content="Hi" />
雖然寫法沒變,但畫面結果改了,可能影響使用者體驗或布局。這類行為建議 bump Major 或至少寫清楚 release notes。
💡 判斷依據:使用者不修改程式碼卻出現不同結果 → 通常就算是 breaking。