Mengurus JSON untuk Konfigurasi, Fixture, dan Data Setempat
Inti pati: Konfigurasi mesti berlapis dan disahkan; fixture mesti deterministik, boleh dibaca, dan boleh dibezakan. Jika dua jenis fail ini diurus dengan buruk, kos penyelesaian masalah kekal tinggi untuk masa yang lama.

Kesimpulan utama: bina konfigurasi sebagai nilai lalai, persekitaran, dan override setempat dengan pengesahan semasa permulaan sebelum risau tentang format fail; jadikan fixture deterministik dalam masa, ID, susunan kunci, dan inden supaya ia kekal boleh dibezakan, dan hanya selepas itu automasikan penopengan.
Dua jenis JSON ini tidak mengambil bahagian dalam logik perniagaan, namun ia menentukan "berapa lama rakan sekerja baharu mengambil masa untuk mula berjalan" dan "berapa lama untuk menjejak isu pengeluaran".
1. Konfigurasi: struktur tiga lapisan
| Lapisan | Sumber | Diselenggara oleh | Dikomit |
|---|---|---|---|
| Nilai lalai | Pemalar dalam kod / default.json |
Pembangun | Ya |
| Konfigurasi persekitaran | config/production.json, pemboleh ubah persekitaran |
Operasi | Ya (tanpa rahsia) |
| Override setempat | .env.local, config/local.json |
Individu | Tidak (diabaikan git) |
Urutan cantum: lalai → persekitaran → setempat, dengan lapisan kemudian menimpa yang sebelumnya. Cantum hanya sedalam keperluan perniagaan; jangan cantum mendalam secara buta — cantum mendalam menjadikan masalah seperti "tatasusunan tergabung secara tidak dijangka" sangat sukar dikesan.
/**
* Merges configuration layers by priority; later layers override earlier ones, and arrays are replaced wholesale rather than concatenated.
* @param layers Configuration layers from lowest to highest priority
* @returns The merged configuration object
*/
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;
}
// Arrays and scalars are always replaced wholesale to avoid accidental element concatenation
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
2. Sahkan semasa permulaan, jangan tunggu sehingga masa jalan
Kos kesalahan konfigurasi membesar secara eksponen dengan betapa lambat ia ditemui:
| Bila ia ditemui | Kos |
|---|---|
| Semasa permulaan | Satu baris log, mula semula dan teruskan |
| Pada permintaan pertama | Sesetengah ciri gagal, sejam menyahpepijat |
| Apabila tugas berjadual dicetuskan | Amaran di tengah malam |
| Pada hari penyelesaian | Insiden penyesuaian |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* Validates the configuration and throws immediately on failure, implementing fail fast.
* @param rawConfig The raw merged configuration
* @returns The validated configuration
*/
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>;
}
Prinsip sokongan:
- Keluar serta-merta apabila item wajib tiada; jangan berikan nilai lalai yang "kelihatan munasabah" (seperti menetapkan alamat pangkalan data lalai kepada
localhost); - Keluar serta-merta pada kesalahan jenis; string
"false"bukan booleanfalse, jadi pemboleh ubah persekitaran mesti ditukar secara eksplisit selepas dibaca; - Rahsia tidak pernah masuk ke repositori; baca hanya daripada pemboleh ubah persekitaran atau perkhidmatan pengurusan rahsia.
3. Format mana yang digunakan untuk fail konfigurasi
| Fail | Format | Sebab |
|---|---|---|
package.json |
JSON standard | Pengurus pakej hanya menerima ini |
tsconfig.json |
JSONC | Komen dibenarkan, disokong secara rasmi |
.vscode/settings.json |
JSONC | Sama seperti di atas |
| Konfigurasi perniagaan | YAML atau JSONC | Perlu komen menjelaskan maksud setiap item |
| Senarai pemboleh ubah persekitaran | .env |
Pasangan kunci-nilai rata, mudah disuntik |
Ujian praktikal: adakah fail ini akan dibaca oleh alat bukan manusia? Jika ya → JSON standard; jika hanya kod anda sendiri membacanya → dialek pun boleh.
4. Fixture ujian: direktori dan penamaan
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
Konvensyen penamaan yang baik ialah object-scenario: order-missing-field jauh lebih berguna daripada test1.json.
Nama fail bukan ASCII kadangkala menyebabkan masalah pengekodan dalam rantaian alat merentas platform, jadi kekalkan nama fail dalam bahasa Inggeris manakala kandungan boleh kekal dalam bahasa Cina.
4.1 Tiga jenis kes, tiga jenis jangkaan
| Direktori | Jangkaan | Tujuan |
|---|---|---|
valid/ |
Pengesahan lulus | Aliran utama, penjanaan jenis |
invalid/ |
Pengesahan gagal, dan kesalahan menunjuk medan yang dijangkakan | Pengendalian kesalahan, salinan mesej |
edge/ |
Tingkah laku eksplisit (lulus atau gagal tidak mengapa, tetapi mesti dituntut) | Perlindungan regresi |
JSONTestSuite menggunakan awalan y_ / n_ / i_ untuk tiga jenis yang sama (harus diterima / harus ditolak / ditentukan pelaksanaan),
dan anda boleh meminjam konvensyen penamaan itu secara langsung.
5. Determinisme: jadikan fixture boleh dibezakan
Fixture tidak stabil menjadikan semakan kod tidak berguna:
| Sumber bunyi | Cara mengendalikannya |
|---|---|
| Masa semasa | Tetapkan kepada pemalar, contohnya 2026-01-01T00:00:00Z |
| ID rawak / UUID | Gunakan jujukan bertambah atau nilai tetap |
| Susunan kunci | Isih mengikut abjad semasa penjanaan |
| Pemformatan float | Kekalkan dua perpuluhan secara seragam atau tukar kepada string |
| Zon masa | Gunakan UTC di mana-mana |
| Inden | Tetap 2 ruang, disemak oleh Prettier dalam CI |
/**
* Recursively sorts keys to produce stable, diffable JSON text.
* @param value The value to serialize
* @param indentWidth Indentation width
* @returns A JSON string with a stable order
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* Recursively sorts object keys.
* @param input Any JSON value
* @returns A new value with sorted keys
*/
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);
}
6. Penopengan dan pematuhan
Data sebenar tidak boleh digunakan terus sebagai fixture:
- Nama → pemegang tempat seperti "John Doe" dan "Jane Roe";
- Nombor telefon → nombor rekaan yang jelas seperti
13800138000; - Nombor kad pengenalan → nombor palsu dengan format betul tetapi digit semak tidak sah;
- Alamat → kekalkan struktur negeri/bandar, gugurkan nombor rumah;
- Jumlah → kekalkan urutan magnitud, tetapi jangan sekali-kali menyimpan maklumat akaun sebenar.
Sebaiknya pasukan mempunyai scripts/mask-fixture.mjs yang berjalan pada sampel pengeluaran yang dieksport untuk menjana versi bertopeng,
mengelakkan "ubah tiga tempat dengan tangan, lupa yang lain".
7. Migrasi dan versi
Fixture juga menjadi basi. Tambah medan schemaVersion pada fixture, atau letakkan fail VERSION dalam direktori:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
Apabila kontrak API dinaik taraf:
- Tambah direktori fixture baharu (
v3/); jangan ubah fixture lama di tempatnya; - Kekalkan fixture lama untuk regresi sehingga versi API lama dihentikan;
- Jalankan kedua-dua versi dalam CI untuk mengesahkan keserasian ke belakang.
8. Senarai semak
- [ ] Struktur konfigurasi tiga lapisan jelas, dan override setempat tidak dikomit;
- [ ] Pengesahan berlaku semasa permulaan, dan kegagalan keluar serta-merta;
- [ ] Rahsia hanya datang daripada pemboleh ubah persekitaran; fail konfigurasi mengandungi pemegang tempat sahaja;
- [ ] Fixture dipecahkan kepada direktori
valid/invalid/edge, dan nama menyatakan senario; - [ ] Fixture deterministik (masa, ID, dan susunan tetap) dan boleh dibezakan;
- [ ] Data sebenar telah diproses oleh skrip penopengan;
- [ ] Naik taraf kontrak menambah direktori versi baharu dan bukannya menyunting di tempatnya.
FAQ
Bagaimana lapisan konfigurasi harus disusun dan dicantum?
Gunakan tiga lapisan — nilai lalai, konfigurasi persekitaran, dan override setempat — mencantum lalai → persekitaran → setempat, dengan lapisan kemudian menimpa yang sebelumnya. Cantum hanya sedalam keperluan perniagaan dan ganti tatasusunan serta skalar sepenuhnya, supaya cantum mendalam tidak menggabungkan tatasusunan secara tidak dijangka. Gitignore fail override setempat dan bukannya mengomit tetapan peribadi.
Mengapa mengesahkan konfigurasi semasa permulaan?
Kos kesalahan konfigurasi membesar secara eksponen dengan betapa lambat ia ditemui: satu baris log semasa permulaan berbanding insiden penyesuaian pada hari penyelesaian. Sahkan terhadap JSON Schema semasa permulaan dan keluar serta-merta pada item wajib hilang atau kesalahan jenis, dan bukannya memberikan nilai lalai yang "kelihatan munasabah".
Patutkah saya menggunakan data pengeluaran terus sebagai fixture ujian?
Tidak. Nama, nombor telefon, nombor kad pengenalan, alamat, dan jumlah semuanya memerlukan pemegang tempat atau nilai rekaan yang jelas, dan pasukan harus mempunyai scripts/mask-fixture.mjs yang berjalan pada eksport pengeluaran untuk menjana versi bertopeng, mengelakkan suntingan tangan yang terlepas sesuatu.