配置、夾具與本地資料的 JSON 管理
結論:配置要能分層與校驗,夾具要確定、可讀、可 diff。這兩類檔案管不好,排查成本會長期居高不下。

結論:先把配置做成預設值、環境、本地三層並啟動即校驗,再談格式選擇;夾具則先固定時間、ID、鍵序與縮排,保證可 diff,最後才考慮用指令碼批次脫敏。
這兩類 JSON 不參與業務邏輯,卻決定了「新同事多久能跑起來」和「線上問題多久能定位」。
一、配置:三層結構
| 層 | 來源 | 誰維護 | 是否入庫 |
|---|---|---|---|
| 預設值 | 程式碼裡的常量 / default.json |
開發者 | 是 |
| 環境配置 | config/production.json、環境變數 |
運維 | 是(不含金鑰) |
| 本地覆蓋 | .env.local、config/local.json |
個人 | 否(gitignore) |
合併順序:預設 → 環境 → 本地,後者覆蓋前者。合併只做淺合併到業務需要的層級, 不要做無腦深合併——深合併會讓「陣列被意外拼接」這種問題極難發現。
/**
* 按優先順序合併配置層,後者覆蓋前者,陣列整體替換而非拼接。
* @param layers 從低到高的配置層
* @returns 合併後的配置物件
*/
function mergeConfigLayers(layers: Array<Record<string, unknown>>): Record<string, unknown> {
return layers.reduce<Record<string, unknown>>((mergedConfig, currentLayer) => {
for (const [configKey, configValue] of Object.entries(currentLayer)) {
const isPlainObject =
typeof configValue === 'object' && configValue !== null && !Array.isArray(configValue);
const existingValue = mergedConfig[configKey];
if (isPlainObject && typeof existingValue === 'object' && existingValue !== null) {
mergedConfig[configKey] = mergeConfigLayers([
existingValue as Record<string, unknown>,
configValue as Record<string, unknown>,
]);
continue;
}
// 陣列與標量一律整體替換,避免意外的元素拼接
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
二、啟動即校驗,不要執行時才發現
配置錯誤的代價隨發現時間指數上升:
| 發現時機 | 代價 |
|---|---|
| 啟動時 | 一行日誌,重啟即可 |
| 首次請求時 | 部分功能報錯,排查一小時 |
| 定時任務觸發時 | 半夜告警 |
| 結算當天 | 對賬事故 |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* 校驗配置並在失敗時立刻拋錯,實現 fail fast。
* @param rawConfig 合併後的原始配置
* @returns 透過校驗的配置
*/
export function assertConfig(rawConfig: unknown): Record<string, unknown> {
const validate = ajv.compile(configSchema);
if (!validate(rawConfig)) {
const messages = (validate.errors ?? []).map(
(errorItem) => `${errorItem.instancePath || '/'} ${errorItem.message ?? ''}`,
);
throw new Error(`配置校驗失敗:\n${messages.join('\n')}`);
}
return rawConfig as Record<string, unknown>;
}
配套的原則:
- 必填項缺失直接退出,不要給「看起來合理」的預設值(比如把資料庫地址預設成
localhost); - 型別錯誤直接退出,字串
"false"不等於布林false,環境變數讀取後要顯式轉換; - 金鑰不落庫,只從環境變數或金鑰管理服務讀取。
三、配置檔案用什麼格式
| 檔案 | 格式 | 原因 |
|---|---|---|
package.json |
標準 JSON | 包管理器只認它 |
tsconfig.json |
JSONC | 允許註釋,官方支援 |
.vscode/settings.json |
JSONC | 同上 |
| 業務配置 | YAML 或 JSONC | 需要註釋解釋每項含義 |
| 環境變數清單 | .env |
扁平鍵值,便於注入 |
一個實用判斷:這份檔案會不會被非人類工具讀取? 會 → 標準 JSON;只有自家程式碼讀 → 可以用方言。
四、測試夾具:目錄與命名
tests/fixtures/
├─ valid/
│ ├─ order-minimal.json
│ ├─ order-full.json
│ └─ order-with-null-fields.json
├─ invalid/
│ ├─ order-missing-id.json
│ ├─ order-price-as-number.json
│ └─ order-nested-broken.json
└─ edge/
├─ big-integer.json
├─ emoji-and-surrogate.json
└─ deep-nesting-64.json
命名規則建議「物件-場景」:訂單-缺欄位 比 test1.json 有用得多。
中文命名在跨平臺工具鏈裡偶有編碼問題,檔名用英文,內容可以用中文。
4.1 三類用例對應三種期望
| 目錄 | 期望 | 用途 |
|---|---|---|
valid/ |
校驗透過 | 主流程、型別生成 |
invalid/ |
校驗失敗,且錯誤定位到預期欄位 | 錯誤處理、提示文案 |
edge/ |
行為明確(透過或失敗都可以,但要斷言) | 迴歸防護 |
JSONTestSuite 用 y_ / n_ / i_ 字首表達同樣的三類(應當接受 / 應當拒絕 / 實現自定義),
可以直接借鑑這套命名。
五、確定性:讓夾具可 diff
不穩定的夾具會讓 code review 失效:
| 噪聲來源 | 處理方式 |
|---|---|
| 當前時間 | 固定為常量,如 2026-01-01T00:00:00Z |
| 隨機 id / uuid | 用遞增序號或固定值 |
| 鍵順序 | 生成時按字母序排序 |
| 浮點格式 | 統一保留兩位小數或轉字串 |
| 時區 | 全部用 UTC |
| 縮排 | 固定 2 空格,CI 用 Prettier 檢查 |
/**
* 遞迴按鍵名排序,生成穩定可 diff 的 JSON 文字。
* @param value 待序列化的值
* @param indentWidth 縮排寬度
* @returns 穩定順序的 JSON 字串
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* 遞迴排序物件鍵。
* @param input 任意 JSON 值
* @returns 鍵已排序的新值
*/
const sortKeysDeep = (input: unknown): unknown => {
if (Array.isArray(input)) {
return input.map((item) => sortKeysDeep(item));
}
if (typeof input === 'object' && input !== null) {
return Object.fromEntries(
Object.keys(input)
.sort()
.map((entryKey) => [entryKey, sortKeysDeep((input as Record<string, unknown>)[entryKey])]),
);
}
return input;
};
return JSON.stringify(sortKeysDeep(value), null, indentWidth);
}
六、脫敏與合規
真實資料不能直接當夾具:
- 姓名 → 「張三」「李四」這類佔位;
- 手機號 →
13800138000這類明顯虛構的號碼; - 身份證號 → 用格式正確但校驗位無效的假號;
- 地址 → 保留省市結構,去掉門牌;
- 金額 → 可以保留數量級,但不要保留真實賬戶資訊。
團隊裡最好有一個 scripts/mask-fixture.mjs,把生產匯出的一份樣例跑一遍生成脫敏版本,
避免「先手動改三處、剩下忘了」。
七、遷移與版本
夾具也會過期。給夾具加一個 schemaVersion 欄位,或者在目錄裡放 VERSION 檔案:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
當介面契約升級時:
- 新增一版夾具目錄(
v3/),不要原地改舊夾具; - 保留舊夾具用於迴歸,直到舊版本介面下線;
- CI 裡同時跑新舊兩版,確認向後相容。
八、檢查清單
- [ ] 配置三層結構清晰,本地覆蓋不入庫;
- [ ] 啟動即校驗,失敗直接退出;
- [ ] 金鑰只走環境變數,配置檔案裡只有佔位;
- [ ] 夾具按
valid/invalid/edge分目錄,命名錶達場景; - [ ] 夾具確定(時間、id、順序固定),可 diff;
- [ ] 真實資料經脫敏指令碼處理;
- [ ] 契約升級時新增版本目錄,不原地改。
常見問題
如何管理配置檔案的層級與合併順序?
分預設值、環境配置、本地覆蓋三層,合併順序是預設 → 環境 → 本地,後者覆蓋前者。只做業務需要的淺合併,陣列與標量整體替換,避免深合併把陣列意外拼接起來。本地覆蓋檔案加進 .gitignore,不要把個人配置提交進倉庫。
為什麼配置檔案要在啟動時校驗?
配置錯誤的代價隨發現時間指數上升:啟動時只是一行日誌,結算當天就可能變成對賬事故。用 JSON Schema 在啟動時校驗,必填項缺失或型別錯誤直接退出,不要給「看起來合理」的預設值。
是否可以直接用生產資料當測試夾具?
不能。姓名、手機號、身份證號、地址、金額都要換成佔位或明顯虛構的值,團隊最好有一個 scripts/mask-fixture.mjs,把生產匯出跑一遍生成脫敏版本,避免手改遺漏。