إدارة JSON للإعدادات وfixtures والبيانات المحلية
الخلاصة: يجب أن تكون الإعدادات متعددة الطبقات ومتحققة؛ ويجب أن تكون fixtures حتمية وقابلة للقراءة والـdiff. إذا أُدير هذان النوعان من الملفات بشكل سيئ، تبقى تكاليف التشخيص مرتفعة لوقت طويل.

الخلاصة: ابنِ الإعدادات كقيم افتراضية وبيئة وتجاوزات محلية مع تحقق عند بدء التشغيل قبل القلق بشأن تنسيق الملف؛ واجعل fixtures حتمية في الوقت والمعرفات وترتيب المفاتيح والإزاحة حتى تبقى قابلة للـdiff، وبعد ذلك فقط أتمت الإخفاء.
هذان النوعان من JSON لا يشاركان في منطق الأعمال، لكنهما يحددان "كم يستغرق زميل جديد حتى يبدأ العمل" و"كم يستغرق تحديد مشكلة في الإنتاج".
1. الإعدادات: بنية ثلاثية الطبقات
| الطبقة | المصدر | من يصونها | تُلتزم |
|---|---|---|---|
| القيم الافتراضية | ثوابت في الكود / default.json |
المطورون | نعم |
| إعدادات البيئة | config/production.json، متغيرات البيئة |
العمليات | نعم (دون أسرار) |
| التجاوزات المحلية | .env.local، config/local.json |
الأفراد | لا (مستبعدة من git) |
ترتيب الدمج: الافتراضي ← البيئة ← المحلي، مع تجاوز كل طبقة لاحقة ما قبلها. ادمج فقط بقدر ما تحتاجه الأعمال؛ لا تدمج عميقاً بشكل أعمى — فالدمج العميق يجعل مشكلات مثل "المصفوفات دُمجت بشكل غير متوقع" صعبة الاكتشاف جداً.
/**
* يدمج طبقات الإعدادات حسب الأولوية؛ الطبقات اللاحقة تتجاوز السابقة، والمصفوفات تُستبدل كاملة لا تُدمج.
* @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;
}, {});
}
2. تحقق عند بدء التشغيل، لا تنتظر حتى وقت التشغيل
تنمو تكلفة خطأ الإعداد أسّياً مع تأخر اكتشافه:
| وقت الاكتشاف | التكلفة |
|---|---|
| عند بدء التشغيل | سطر سجل واحد، أعد التشغيل وتابع |
| عند أول طلب | بعض الميزات تفشل، ساعة من التصحيح |
| عند إطلاق مهمة مجدولة | تنبيهات في منتصف الليل |
| يوم التسوية | حادثة مطابقة |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* يتحقق من الإعدادات ويرمي خطأ فوراً عند الفشل، بتنفيذ مبدأ الفشل السريع.
* @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، لذا يجب تحويل متغيرات البيئة صراحةً بعد قراءتها؛ - الأسرار لا تدخل المستودع أبداً؛ اقرأها فقط من متغيرات البيئة أو خدمة إدارة الأسرار.
3. أي تنسيق لملفات الإعدادات
| الملف | التنسيق | السبب |
|---|---|---|
package.json |
JSON قياسي | مدير الحزم يقبل هذا فقط |
tsconfig.json |
JSONC | التعليقات مسموحة ومدعومة رسمياً |
.vscode/settings.json |
JSONC | كما سبق |
| إعدادات الأعمال | YAML أو JSONC | تحتاج تعليقات تشرح معنى كل بند |
| قائمة متغيرات البيئة | .env |
أزواج مفتاح-قيمة مسطّحة، سهلة الحقن |
اختبار عملي: هل سيقرأ هذا الملف أدوات غير بشرية؟ إذا نعم ← JSON قياسي؛ وإذا كان كودك فقط يقرأه ← فلهجة مسموحة.
4. fixtures الاختبار: المجلدات والتسمية
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
اتفاقية تسمية جيدة هي object-scenario: فـorder-missing-field أنفع بكثير من test1.json.
أسماء الملفات غير ASCII تسبب أحياناً مشكلات ترميز في أدوات سلاسل عبر المنصات، لذا أبقِ أسماء الملفات بالإنجليزية بينما يمكن أن يبقى المحتوى بالصينية.
4.1 ثلاثة أنواع من الحالات، ثلاثة أنواع من التوقعات
| المجلد | التوقع | الغرض |
|---|---|---|
valid/ |
ينجح التحقق | المسار الرئيسي، توليد الأنواع |
invalid/ |
يفشل التحقق، والخطأ يشير إلى الحقل المتوقع | معالجة الأخطاء، نسخ الرسائل |
edge/ |
السلوك صريح (النجاح أو الفشل مقبول، لكن يجب تأكيده) | حماية من الانحدار |
يستخدم JSONTestSuite البادئات y_ / n_ / i_ للأنواع الثلاثة نفسها (يجب القبول / يجب الرفض / يحدده التنفيذ)،
ويمكنك استعارة اتفاقية التسمية تلك مباشرةً.
5. الحتمية: اجعل fixtures قابلة للـdiff
fixtures غير المستقرة تجعل مراجعة الكود عديمة الفائدة:
| مصدر الضوضاء | كيفية التعامل |
|---|---|
| الوقت الحالي | ثبّته على قيمة ثابتة، مثل 2026-01-01T00:00:00Z |
| معرفات عشوائية / UUIDs | استخدم تسلسلاً متزايداً أو قيماً ثابتة |
| ترتيب المفاتيح | رتّب أبجدياً وقت التوليد |
| تنسيق الأعداد العشرية | احتفظ بشكل موحّد بخانتين عشريتين أو حوّل إلى نصوص |
| المنطقة الزمنية | استخدم UTC في كل مكان |
| الإزاحة | ثابتة على مسافتين، يفحصها Prettier في CI |
/**
* يرتب المفاتيح عودياً لإنتاج نص JSON مستقر وقابل للـdiff.
* @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);
}
6. الإخفاء والامتثال
لا يمكن استخدام البيانات الحقيقية مباشرة كـfixtures:
- الأسماء ← نصوص نائبة مثل "John Doe" و"Jane Roe"؛
- أرقام الهواتف ← أرقام وهمية بوضوح مثل
13800138000؛ - أرقام الهوية ← أرقام وهمية بالتنسيق الصحيح لكن برقم تحقق غير صالح؛
- العناوين ← احتفظ ببنية المحافظة/المدينة، واحذف رقم المنزل؛
- المبالغ ← احتفظ برتبة المقدار، لكن لا تحتفظ أبداً بمعلومات حساب حقيقية.
من الأفضل أن يكون لدى الفريق scripts/mask-fixture.mjs يعمل على عينة إنتاج مصدّرة لتوليد نسخة مخفية،
متجنباً "عدّل ثلاثة أماكن يدوياً ونسِ الباقي".
7. الترحيل والإصدارات
fixtures تتقادم أيضاً. أضف حقلاً schemaVersion إلى الـfixture، أو ضع ملف VERSION في المجلد:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
عند ترقية عقد API:
- أضف مجلد fixture جديداً (
v3/)؛ لا تعدّل fixtures القديمة في مكانها؛ - احتفظ بـfixtures القديمة للانحدار حتى يُسحب إصدار API القديم؛
- شغّل الإصدارين في CI لتأكيد التوافق الخلفي.
8. قائمة الفحص
- [ ] بنية الإعدادات الثلاثية واضحة، والتجاوزات المحلية غير ملتزمة؛
- [ ] التحقق يحدث عند بدء التشغيل، والفشل يخرج فوراً؛
- [ ] الأسرار تأتي من متغيرات البيئة فقط؛ ملفات الإعدادات تحتوي نصوصاً نائبة فقط؛
- [ ] fixtures مقسّمة إلى مجلدات
valid/invalid/edge، والأسماء تعبّر عن السيناريو؛ - [ ] fixtures حتمية (وقت ومعرفات وترتيب ثابتة) وقابلة للـdiff؛
- [ ] البيانات الحقيقية عولجت بسكربت الإخفاء؛
- [ ] ترقيات العقد تضيف مجلد إصدار جديداً بدلاً من التحرير في مكانه.
FAQ
كيف يجب ترتيب طبقات الإعدادات ودمجها؟
استخدم ثلاث طبقات — القيم الافتراضية، وإعدادات البيئة، والتجاوزات المحلية — مدموجة بالافتراضي ← البيئة ← المحلي، مع تجاوز كل طبقة لاحقة ما قبلها. ادمج فقط بقدر ما تحتاجه الأعمال واستبدل المصفوفات والقيم القياسية كاملة، حتى لا يدمج الدمج العميق مصفوفات غير متوقعة. استبعد ملف التجاوز المحلي من git بدلاً من التزام الإعدادات الشخصية.
لماذا نتحقق من الإعدادات عند بدء التشغيل؟
تنمو تكلفة خطأ الإعداد أسّياً مع تأخر اكتشافه: سطر سجل واحد عند بدء التشغيل مقابل حادثة مطابقة يوم التسوية. تحقق مقابل JSON Schema عند بدء التشغيل واخرج فوراً عند غياب العناصر المطلوبة أو أخطاء النوع، بدلاً من توفير قيمة افتراضية "تبدو معقولة".
هل يجب استخدام بيانات الإنتاج مباشرة كـfixtures اختبار؟
لا. الأسماء وأرقام الهواتف وأرقام الهوية والعناوين والمبالغ كلها تحتاج نصوصاً نائبة أو قيماً وهمية بوضوح، ويجب أن يكون لدى الفريق scripts/mask-fixture.mjs يعمل على تصدير إنتاجي لتوليد نسخة مخفية، متجنباً التعديلات اليدوية التي تنسى شيئاً.