مدة القراءة: 5 د
جلب نظام إدارة المحتوى مرة واحدة بدلاً من مرة واحدة لكل صفحة
كان هذا الموقع يستعين سابقًا بـ Strapi لجلب المحتوى نفسه في كل صفحة يتم إنشاؤها. أما الآن، فيقوم مُحمِّل بجلب كل مصدر مرة واحدة لكل عملية بناء، وتقرأ كل صفحة من مخزن محلي. إليكم ما تغيّر، وما تم الاستغناء عنه تلقائيًا، وثلاثة أمور تأثرت سلبًا.
كل صفحة في هذا الموقع تُبنى مسبقًا. لا يوجد خادم عند وقت الطلب، لذا فإن كل قراءة من نظام المحتوى تحدث أثناء البناء — وهذا كان صحيحًا دائمًا ولا يزال. الخطأ كان في عدد المرّات التي يحدث فيها ذلك.
التخطيط المشترك للصفحات يعرض تذييلًا يحتاج إلى ملفي الشخصي. لذلك كان الملف الشخصي يُجلب مرّة لكل صفحة. لا مرّة واحدة في البناء، بل مرّة لكل صفحة. يبني هذا الموقع ١٣٦ صفحة، أي أنّ نوعًا من المحتوى قد يتغيّر مرّتين في السنة كان يُسحب من Strapi ١٣٦ مرّة في كل نشر. أمّا المشاريع فكان يجلبها فهرس المشاريع، ثم تجلبها مرّة أخرى صفحة كل مشروع، ثم يجلبها فهرس البحث مرّة ثالثة. وقد نشأت في ميزتين ذاكرتا وعود مكتوبتان يدويًّا لا لشيء إلّا للتغطية على ذلك.
flowchart LR
p1["/ "] --> d1["data.ts"]
p2["/المشاريع"] --> d2["data.ts"]
p3["و١٣٤ صفحة أخرى"] --> d3["data.ts"]
d1 --> cms[("Strapi")]
d2 --> cms
d3 --> cms
محمِّل واحد لكل مصدر
طبقة المحتوى في Astro تقلب هذا الترتيب. فبدلًا من أن تسحب الصفحة البيانات عند العرض، تُعلِن مجموعة لها محمِّل، ويعمل هذا المحمِّل مرّة واحدة في كل مزامنة — قبل عرض أي صفحة — فيكتب نتائجه في مخزن محتوى محلّي. ثمّ تقرأ الصفحات من ذلك المخزن.
صار هناك أربع عشرة مجموعة، واحدة لكل مصدر في نظام المحتوى. وملف content.config.ts كلّه
ليس إلّا بيانًا معلنًا؛ أمّا الجلب فيبقى في الوحدات التي كانت تملكه من الأصل.
flowchart TD
subgraph sync["المزامنة — مرّة واحدة في البناء"]
loader["المحمِّل"] --> api["api.ts"]
api --> cms[("Strapi")]
loader --> store[("مخزن المحتوى")]
end
subgraph render["العرض — مرّة لكل صفحة"]
pages["١٣٦ صفحة"] --> store
end
حافظت دوال الوصول على أسمائها وبصماتها، فلم تتغيّر صفحة واحدة ولا مكوّن واحد. فـ
getProfile(locale) لا تزال تعيد ملفًا شخصيًّا — غير أنّها تقرأ الآن مدخلًا بدلًا من أن
تُصدر طلبًا.
معرّفات المدخلات تحمل اللغة: en/abc123 وar/abc123. فيختار المستهلك لغةً بمرشّح على
البادئة، ولأنّ المعرّف هو اللغة مضافًا إليها المفتاح، لا يوجد حقل لغة منفصل قد يخالفه.
أمّا الأنواع المفردة فتترك المفتاح أصلًا وتستخدم en أو ar مجرّدًا، وهو ما يحوّل القراءة
إلى وصول مباشر لا إلى بحث.
وثمّة تفصيل يبدو تزيّدًا وهو ليس كذلك: كل مدخل يخزّن حقل order صريحًا، مأخوذًا من موضعه في
استجابة الواجهة المرتَّبة سلفًا. فترتيب التعداد في المخزن تفصيلٌ تنفيذي، والاعتماد عليه يعني
أنّ الترتيب الذي طلبته من Strapi قد يتوقّف بهدوء عن النجاة في الطريق.
ثلاثة أشياء اختفت
الأمر اللافت في هذا الترحيل هو حجم ما حذفه من التعليمات البرمجية لا ما أضافه.
ذهبت الذاكرتان المؤقّتتان. لم تكن ذاكرتا الوعود المكتوبتان يدويًّا موجودتين إلّا لمنع الجلب المتكرّر. ومع وجود المخزن لم يبقَ ما يُحفظ، فذهبتا ومعهما التعليقات التي تشرح سبب الحاجة إليهما.
ذهبت الاستعلامات المرشَّحة المكرّرة. كان «أعطني المشاريع المميّزة فقط» طلبًا ثانيًا مع
مرشّح. وهو الآن .filter() على مجموعة يملكها المحمِّل أصلًا. والأمر نفسه ينطبق على وسوم
المهارات، ونفسه على أبرز محطّات الخط الزمني في الصفحة الرئيسة.
صار التراجع في الترجمة صادقًا. كان المستند الذي لا صفّ له في لغة ما يعني ٤٠٤ من Strapi،
يُلتقط بحسب صنف الخطأ، ثم يُتراجع إلى اللغة الافتراضية. أمّا الآن فيعني أنّه لا يوجد مدخل
عند ar/abc123. بيانات غائبة يُعبَّر عنها كغياب، لا كاستثناء:
const localized = await getEntry('projects', `${locale}/${canonical.documentId}`);
return localized?.data ?? canonical;
ثلاث مصاعب
صار astro check يحتاج نظام محتوى قابلًا للوصول، وهذا ما فاجأني. فالتحقّق من الأنواع لا
يعتمد على البيانات — إذ تُشتقّ أنواع getCollection('projects') من مخطّط المجموعة لا من
صفوفها. لكنّ astro check يشغّل مزامنةً أوّلًا، والمزامنة تفعل شيئين في وقت واحد: تولّد
أنواع المحتوى، وهذا لا يحتاج نظام محتوى، وتشغّل كل محمِّل، وهذا يحتاجه. ولا سبيل إلى طلب
الأوّل وحده.
flowchart LR check["astro check"] --> sync["astro sync"] sync --> types["توليد الأنواع<br/>(بلا نظام محتوى)"] sync --> loaders["تشغيل المحمِّلات<br/>(يحتاج نظام المحتوى)"] types --> tsc["التحقّق من الأنواع"]
لذلك يحمل التكامل المستمر الآن بيانات وصول حقيقية للقراءة. والآلية نفسها تفسّر إزعاجًا أصغر: في التطوير، يحتاج تعديل نظام المحتوى إلى إعادة تشغيل لا إلى إعادة تحميل، لأنّ المحمِّلات تعمل عند المزامنة.
لا تمنح TypeScript الواجهةَ بصمة فهرسة. فـ parseData في Astro مُعرَّف على أنّه
TData extends Record<string, unknown>، وTypeScript تمنح بصمات فهرسة ضمنية للأنواع المُخطَّطة
وللأسماء البديلة لأنواع حرفية — لكن لا تمنحها لواجهة قطّ، ولا عبر اسم بديل مجرّد يشير إليها.
ونصف أنواع المحتوى عندي واجهات. والإصلاح المُغري هو إعادة كتابة كل واحدة على هيئة
{ [K in keyof T]: T[K] }، وهو يعمل فعلًا ولا يُفقد شيئًا. أمّا الإصلاح الصحيح فكان أن أكفّ
عن ترك قيدٍ عامّ يحدّد شكل أنواع نطاقي: أن أرفع القيد عن مصانعي الخاصّة، وأحوّل النوع مرّة
واحدة عند الحدّ.
astro:content خاصّ بالخادم، وهذا تسريب ينتظر فرصته. وقد وقع في هذا المستودع من قبل:
جزيرة رسم بياني كانت تستورد وحدةً تصل إلى عميل Strapi، ومن خلاله إلى وحدة افتراضية خاصّة
بالخادم، فأخفقت الجزيرة في التمهيد بخطأ ٥٠٠. والتعليق الذي يسجّل الحادثة ما زال في الملف.
وقد أعاد هذا الترحيل إنشاء الخطر نفسه من طريق جديد، لأنّ كل وحدة بيانات تستورد الآن
astro:content.
الاستيرادات المقصورة على الأنواع تُمحى عند البناء، فهي آمنة — وهذا وحده سبب صمود الحدّ القائم. وهذا أدقّ من أن يُترك للذاكرة، فصار هناك اختبار يمشي في مخطَّط الاستيراد انطلاقًا من كل نقطة دخول للعميل، ويفشل عند أي استيراد وقت التشغيل لوحدة خاصّة بالخادم. وقد كانت نسخته الأولى تنجح بينما كان في الشجرة انتهاك حقيقي، لأنّها لم تكن تتحقّق إلّا من الاستيرادات المباشرة لامتداد ملف واحد. والحارس الذي لم تره يفشل ليس حارسًا.
ما استقرّ عليه الأمر
أربعة عشر محمِّلًا، يعمل كل منها مرّة واحدة بالضبط في كل بناء؛ و٧٩٦ مدخلًا في المجموعات موزّعة على أربع لغات؛ و١٣٦ صفحة. البناء ليس أسرع بصورة مثيرة — فـ Strapi يعمل على الجهاز نفسه ولم يكن قطّ عنق الزجاجة — لكنّ الشكل صار صحيحًا أخيرًا، وحجم التعليمات البرمجية التي لم تكن موجودة إلّا للتحايل على الشكل القديم صار صفرًا.