Yesod

بنية تطبيقات المصادر المفتوحة (المجلد 2)Yesod

بنية تطبيقات المصادر المفتوحة (المجلد 2) Yesod

مايكل سنويمان

إذا استمتعت بهذه الكتب، فقد يعجبك أيضًا التصميم البرمجي بالأمثلة في بايثون (Software Design by Example in Python) والتصميم البرمجي بالأمثلة في جافاسكربت (Software Design by Example in JavaScript).

Yesod إطار ويب مكتوب بلغة البرمجة هاسكل. وبينما تستثمر كثير من أطر الويب الشائعة الطابع الديناميكي للغات المستضافة فيها، فإن Yesod يستثمر الطابع الساكن (static) لهاسكل لينتج شيفرة أكثر أمانًا وسرعة.

بدأ التطوير قبل نحو عامين ولم يتوقف منذ ذلك الحين. نضج Yesod في مشاريع واقعية، وُلد كل واحدة من مزاياه الأولى من حاجة حقيقية فعلية. في البداية كان التطوير شبه كاملٍ على يد شخص واحد. وبعد نحو عام من التطوير، انطلقت الجهود المجتمعية، ومنذ ذلك الحين ازدهر Yesod ليصبح مشروعًا مفتوح المصدر نابضًا بالحياة.

خلال المرحلة الجنينية، حين كان Yesod زائلًا على نحو شديد وغير محدَّد المعالم، كان من غير المُجد أن تحاول إسناد العمل عليه إلى فريق. وخلفما استقر بما يكفي ليكون مفيدًا للآخرين، حان الوقت لمعرفة الجوانب السلبية لبعض القرارات التي اتُّخذت. ومنذ ذلك الحين أجرينا تغييرات كبيرة في واجهة المستخدم (API) الموجَّهة للمستخدمين لجعلها أكثر نفعًا، ونوشك على ترسيخ إصدار 1.0.

السؤال الذي قد تطرحه هو: لماذا إطار ويب آخر؟ لنوجّه السؤال بدلًا من ذلك إلى سؤال مختلف: لماذا نستخدم هاسكل؟ يبدو أن معظم العالم راضٍ بواحد من نمطي اللغات:

  • اللغات ساكنة الأنواع (statically typed)، مثل Java وC# وC++. تقدّم هذه اللغات سرعةً وأمانًا في الأنواع، لكنها أكثر إزعاجًا في البرمجة.
  • اللغات ديناميكية الأنواع (dynamically typed)، مثل Ruby وPython. ترفع هذه اللغات الإنتاجية كثيرًا (على الأقل في المدى القصير)، لكن تنفيذها بطيء ولا يتلقى من المُصرِّف إلا دعمًا ضئيلًا جدًا لضمان الصحة. (الحل للنقطة الأخيرة هو اختبارات الوحدة، وسنتناول ذلك لاحقًا.)

هذه ثنائية زائفة. لا سبب يجعل اللغات ساكنة الأنواع بهذا التعثر. تستطيع هاسكل أن تلتقط قدرًا هائلًا من تعبيرية Ruby وPython، مع بقائها لغة قوية الأنواع. بل إن نظام أنواع هاسكل يمسك بأخطاء أكثر بكثير من Java وأمثالها. أما استثناءات المؤشرات الفارغة (null pointer) مُستبعدةً تمامًا؛ وتُبسِّح بنى البيانات غير القابلة للتغيير (immutable) التفكير في شيفرتك، وتُسهِّل البرمجة المتوازية والمتصانة.

إذن لماذا هاسكل؟ إنها لغة كفؤة ومريحة للمطوّرين، تقدّم كثيرًا من فحوص صحة البرنامج في زمن التصريف (compile time).

هدف Yesod هو امتداد نقاط قوة هاسكل إلى تطوير الويب. ويسعى Yesod إلى جعل شيفرتك موجزة قدر الإمكان. وفي أقصى حد ممكن، يُفحص كل سطر من شيفرتك للتأكد من صحته في زمن التصريف. فبدلًا من الحاجة إلى مكتبات ضخمة من اختبارات الوحدة لاختبار خصائص أساسية، يتولّى المُصرِّف ذلك كله نيابةً عنك. وتحت السطح، يستخدم Yesod أقصى ما نستطيع من تقنيات الأداء المتقدّمة لنجعل شيفرتك عالية المستوى تطير.

22.1. مقارنة بأطر أخرى

بوجه عام، Yesod أقرب شبهاً منه اختلافًا إلى الأطر الرائدة مثل Rails وDjango. فهو يتبع عمومًا نموذج Model-View-Controller (MVC)، ولديه نظام قوالب يفصل العرض عن المنطق، ويوفّر نظام تعيين كائني-علائقي (Object-Relational Mapping، ORM)، ويعتمد أسلوب المتحكّم الأمامي (front controller) في التوجيه (routing).

الشيطان في التفاصيل. يسعى Yesod إلى دفع أكبر قدر ممكن من التقاط الأخطاء إلى مرحلة التصريف بدلًا من زمن التشغيل، وإلى التقاط الأخطاء والثغرات الأمنية تلقائيًا عبر نظام الأنواع. وفيما يحاول Yesod الحفاظ على واجهة برمجية (API) عالية المستوى وسهلة الاستخدام، فإنه يستخدم عددًا من التقنيات الأحدث في عالم البرمجة الوظيفية (functional programming) لتحقيق أداء عالٍ، ولا يخشى كشف هذه التفاصيل الداخلية للمطوّرين.

التحدي المعماري الرئيس في Yesod هو موازنة هذين الهدفين اللذين يبدو أنهما متعارضان. فمثلًا لا شيء ثوري في منهج Yesod في التوجيه (ويُسمّى عناوين URL آمنة الأنواع (type-safe URLs)). فتاريخيًا كان تنفيذ حلّ كهذا عمليةً مملّة ومعرَّضة للخطأ. أما ابتكار Yesod فهو استخدام Template Haskell (وهي صورة من توليد الشيفرة) لأتمتة الشيفرة الفعّالة (boilerplate) اللازمة لتهيئة هذه العملية. وعلى نحو مماثل، فإن HTML الآمن أنويعيًا موجود منذ زمن طويل؛ ويسعى Yesod إلى الإبقاء على الجانب المريح للمطوّر في لغات القوالب الشائعة، مع الحفاظ على قوة أمان الأنواع.

22.2. واجهة تطبيقات الويب

يحتاج تطبيق الويب إلى وسيلة ما للتواصل مع خادم. وأحد النهوج الممكنة هو دمج الخادم مباشرةً داخل الإطار، لكن ذلك يقيّد خياراتك في النشر بالضرورة ويؤدي إلى واجهات رديئة. وقد أنشأت لغات كثيرة واجهات قياسية لمعالجة هذه المسألة: لدى Python واجهة WSGI، ولدى Ruby واجهة Rack. وفي هاسكل لدينا WAI: واجهة تطبيقات الويب (Web Application Interface).

ليست WAI واجهة عالية المستوى. لها هدفان محدَّدان: العمومية (generality) والأداء. وبفضل بقائها عامّة، تمكّنت WAI من دعم الواجهات الخلفية (backends) لكل شيء، من الخوادم المستقلّة إلى CGI الكلاسيكي القديم، وتعمل حتى مباشرةً مع Webkit لإنتاج تطبيقات سطح مكتب وهمية. أما جانب الأداء فسيقدّم لنا إلى عدد من مزايا هاسكل الرائعة.

الشكل 22.1: البنية العامة لتطبيق Yesod

أنواع البيانات

من أكبر مزايا هاسكل—ومن الأشياء التي نستعين بها أكثر من غيرها في Yesod—هو التنميط الساكن القوي (strong static typing). وقبل أن نبدأ كتابة الشيفرة التي تحلّ مسألة ما، نحتاج أن نفكّر في شكل البيانات. وWAI مثال مثالي على هذا النمط. فالمفهوم الأساسي الذي نريد التعبير عنه هو مفهوم التطبيق. وأبسط تعبير عن تطبيق هو دالة تأخذ طلبًا وتعيد استجابة. بمصطلحات هاسكل:

type Application = Request -> Response

وهذا يطرح سؤالًا فحسب: ما شكل Request وResponse؟ الطلب يحوي عددًا من قطع المعلومات، لكن أكثرها أساسية هي المسار المطلوب، وسلسلة الاستعلام، وترويسات الطلب، وجسم الطلب. أما الاستجابة فليست سوى ثلاثة مكوّنات: رمز الحالة، وترويسات الاستجابة، وجسم الاستجابة.

كيف نمثّل شيئًا مثل سلسلة الاستعلام؟ تحافظ هاسكل على فصل صارم بين البيانات الثنائية والنصية. فالأولى تُمثَّل بـByteString، والثانية بـText. وكلاهما من أنواع بيانات شديدة التحسين توفّر واجهة برمجية (API) عالية المستوى وآمنة. وفي حالة سلسلة الاستعلام، نخزّن البايتات الخام المنقولة عبر الشبكة بوصفها ByteString، والقيم المحلَّلة والمفكوكة ترميزًا بوصفها Text.

البثّ

يمثّل ByteString مخزن ذاكرة واحدًا. فلو استخدمنا ByteString عاديًا بشكل ساذج لاحتفاظ بجسم الطلب أو جسم الاستجابة كاملَين، لاستحال لتطبيقاتنا أن تتوسّع للتعامل مع طلبات أو استجابات كبيرة. وبدلًا من ذلك نستخدم تقنية تسمّى العدّادات (enumerators)، متشابهة جدًا في المفهوم بمولّدات (generators) بايثون. يصبح Application لدينا مستهلكًا (consumer) لتدفّق من ByteStrings يمثّل جسم الطلب الوارد، ومنتجًا (producer) لتدفّق منفصل خاص بالاستجابة.

نحتاج الآن إلى تعديل تعريف Application قليلًا. سيأخذ Application قيمة Request تحتوي على الترويسات وسلسلة الاستعلام وما شابه، ويستهلك تدفّقًا من ByteStrings، ويُنتج Response. إذن تعريف Application المعدَّل هو:

type Application = Request -> Iteratee ByteString IO Response

ومجرد IO يشرح ما أنواع الآثار الجانبية التي يمكن للتطبيق أن يؤديها. وفي حالة IO يمكنه إجراء أي نوع من التفاعل مع العالم الخارجي، وهو أمر بديهي ضروري bagi الغالبية العظمى من تطبيقات الويب.

الباني (Builder)

الحيلة في ترسانتنا هي كيف ننتج مخازن الاستجابة. ولدينا هنا رغبتان متعارضتان: تقليل استدعاءات النظام، وتقليل نسخ المخازن. من جهة، نريد تقليل استدعاءات النظام لإرسال البيانات عبر المقبس (socket). ولتحقيق ذلك نحتاج إلى تخزين البيانات الصادرة في مخزن. لكن إذا جعلنا هذا المخزن كبيرًا أكثر من اللازم، فسنستنزف الذاكرة ونُبطِّئ زمن استجابة التطبيق. ومن جهة أخرى، نريد تقليل عدد المرات التي تُنسخ فيها البيانات بين المخازن، ويفضَّل أن تُنسخ مرة واحدة فقط من المخزن المصدر إلى المخزن الوجهة.

حلّ هاسكل هو الباني (builder). والباني تعليمة حول كيفية ملء مخزن ذاكرة، مثل: ضع البايتات الخمسة «hello» في الموضع المفتوح التالي. وبدلًا من تمرير تدفّق من مخازن الذاكرة إلى الخادم، تمرِّر تطبيق WAI تدفّقًا من هذه التعليمات. يأخذ الخادم التدفّق ويستخدمه لملء مخازن ذاكرة بأحجام مثلى. ومع امتلاء كل مخزن، يُجري الخادم استدعاء نظام لإرسال البيانات عبر الشبكة، ثم يبدأ في ملء المخزن التالي.

(الحجم الأمثل للمخزن سيعتمد على عوامل كثيرة مثل حجم الذاكرة المخبأة (cache). وقد خضعت مكتبة blaze-builder الأساسية لاختبارات أداء واسعة لتحديد أفضل مقايضة.)

نظريًا يمكن إجراء هذا النوع من التحسين داخل التطبيق نفسه. لكن، بترميز هذا النهج في الواجهة، نتمكن ببساطة من إلحاق ترويسات الاستجابة في بداية جسم الاستجابة. والنتيجة أنه، بالنسبة للاستجابات الصغيرة إلى المتوسطة الحجم، يمكن إرسال الاستجابة كاملةً باستدعاء نظام واحد، مع نسخ الذاكرة مرة واحدة فقط.

المعالِجات (Handlers)

الآن وقد صار لدينا تطبيق، نحتاج إلى وسيلة ما لتشغيله. وبلغة WAI، هذا هو المعالِج (handler). ولدى WAI بعض المعالِجات الأساسية القياسية، مثل خادم Warp المستقل (المُناقَش أدناه)، وFastCGI وSCGI وCGI. ويتيح هذا الطيف تشغيل تطبيقات WAI على أي شيء، من خوادم مخصَّصة إلى الاستضافة المشتركة. لكن إلى جانب هذه، فلدى WAI بعض الواجهات الخلفية الأكثر إثارة للاهتمام:

  • Webkit: تُضمر هذه الواجهة الخلفية خادم Warp وتستدعي QtWebkit. وبتشغيل خادم، ثم فتح نافذة متصفح مستقلّة جديدة، نحصل على تطبيقات سطح مكتب وهمية.
  • Launch: هذا نوع معدَّل قليلًا من Webkit. فنشر مكتبتَي Qt وWebkit قد يكون مرهقًا بعض الشيء، لذا نشغِّل بدلًا من ذلك المتصفح الافتراضي للمستخدم.
  • Test: حتى الاختبار يُعدّ معالِجًا. فالاختبار في جوهره مجرّد تشغيل التطبيق وفحص الاستجابات.

على الأرجح أن يستخدم معظم المطوّرين Warp. فهو خفيف بما يكفي لاستخدامه في الاختبار. ولا يحتاج إلى ملفات إعدادات، ولا إلى تسلسل مجلدات، ولا إلى عملية طويلة الأمد يملكها مدير النظام. إنه مكتبة بسيطة تُدمَج في تطبيقك عند التصريف أو تُشغَّل عبر مُفسِّر هاسكل. وWarp خادم سريع للغاية، مع حماية من جميع مسارات الهجوم، مثل Slowloris والترويسات اللانهائية. ويمكن أن يكون Warp هو خادم الويب الوحيد الذي تحتاجه، رغم أنه مريح أيضًا للعمل خلف وكيل (proxy) عكسي لـHTTP.

يقيس معيار الأداء PONG عدد الطلبات في الثانية لدى مختلف الخوادم لجسم استجابة من 4 بايت هو «PONG». وفي الرسم البياني المعروض في الشكل 22.2، يُقاس Yesod بوصفه إطارًا فوق Warp. وكما يتبيّن، تتصدّر الخوادم المكتوبة بهاسكل (Warp وHappstack وSnap) المشهد.

الشكل 22.2: معيار أداء PONG لـWarp

معظم أسباب سرعة Warp سبق بيانها في الوصف العام لـWAI: العدّادات والبانيات وأنواع البيانات المحزومة. والقطعة الأخيرة في اللغز تأتي من بيئة تشغيل مُصرِّف غلاسكو للغة هاسكل متعدّدة الخيوط (GHC). فـGHC، مُصرِّف هاسكل الرئيسي، يملك خيوطًا خضراء خفيفة الوزن (light-weight green threads). وبخلاف خيوط النظام، من الممكن إطلاق آلاف منها دون عقوبات أداء كبيرة. لذا في Warp يتولّى كل اتصال خيطه الأخضر الخاص.

الحيلة التالية هي الإدخال/الإخراج اللامتزامن (asynchronous I/O). فأي خادم ويب يطمح في التوسّع إلى عشرات الآلاف من الطلبات في الثانية سيحتاج إلى شكل من أشكال الاتصال اللامتزامن. وفي معظم اللغات ينطوي هذا على برمجة معقّدة تتضمّن دوال نداء (callbacks). أما GHC فيتيح لنا الغشّ: نبرمج وكأننا نستخدم واجهة متزامنة (synchronous API)، ويبدّل GHC تلقائيًا بين الخيوط الخضراء المختلفة التي تنتظر النشاط.

تحت السطح، يستخدم GHC النظام الذي يوفّره نظام التشغيل المضيف، مثل kqueue وepoll وselect. ومنح هذا كل أداء نظام الإدخال/الإخراج القائم على الأحداث (event-based)، دون قلق بشأن مسائل قابلية النقل عبر المنصات أو الكتابة بأسلوب قائم على دوال النداء.

الوسائط (Middleware)

بين المعالِجات والتطبيقات، لدينا middleware (الوسائط). تقنيًا، الوسائط هي مُحوِّل تطبيقات (application transformer): تأخذ Application واحدًا وتعيد واحدًا جديدًا. وهي مُعرَّفة كالتالي:

type Middleware = Application -> Application

وأفضل طريقة لفهم الغرض من الوسائط هي النظر في بعض الأمثلة الشائعة:

  • gzip يضغط استجابة التطبيق تلقائيًا.
  • jsonp يحوّل استجابات JSON إلى استجابات JSON-P تلقائيًا عندما يزوّد العميل بمعامل نداء (callback).
  • autohead يولّد استجابات HEAD المناسبة بناءً على استجابة GET الخاصة بالتطبيق.
  • debug يطبع معلومات التنقيح إلى الطرفية أو إلى سجل عند كل طلب.

الفكرة هنا هي استخراج الشيفرة المشتركة من التطبيقات وجعلها قابلة للمشاركة بسهولة. ولاحظ أنه بناءً على تعريف الوسائط، يمكننا تكديس هذه الأشياء بسهولة. وسير العمل العام للوسائط هو:

  1. خُذ قيمة الطلب وطبِّق عليها بعض التعديلات.
  2. مرِّر الطلب المعدَّل إلى التطبيق واستقبل استجابة.
  3. عدِّل الاستجابة وأعِدها إلى المعالِج.

وفي حالة الوسائط المكدَّسة، وبدلًا من التمرير إلى التطبيق أو المعالِج، ستمرِّر الوسائط الواقعة بينهما إلى الوسائط الداخلية والخارجية على التوالي.

Wai-test

لا شيء والتنميط الساكن سيُغني عن الحاجة إلى الاختبار. فجميعنا نعلم أن الاختبار الآلي ضرورة لأي تطبيق جاد. وwai-test هو النهج الموصى به لاختبار تطبيق WAI. ولأن الطلبات والاستجابات مجرّد أنواع بيانات بسيطة، من السهل إنشاء طلب وهمي (mock)، ومرّرته إلى تطبيق، واختبار الخصائص المتعلقة بالاستجابة. ويوفّر wai-test ببساطة بعض دوال الراحة لاختبار الخصائص الشائعة، مثل وجود ترويسة أو رمز حالة.

22.3. القوالب

في نموذج Model-View-Controller (MVC) المعتاد، أحد الأهداف هو فصل المنطق عن العرض. ويُنجَز جزء من هذا الفصل من خلال استخدام لغة قوالب. لكن هناك طرقًا مختلفة كثيرة للتعامل مع هذه المسألة. فعلى أحد طرفي الطيف، مثلًا، تتيح لك PHP/ASP/JSP تضمين أي شيفرة عشوائية داخل القالب. وعلى الطرف الآخر، لديك أنظمة مثل StringTemplate وQuickSilver، تُمرَّر إليها بعض الوسائط ولا تملك أي وسيلة أخرى للتفاعل مع بقية البرنامج.

لكل نظام مزاياه وعيوبه. ووجود نظام قوالب أقوى قد يكون راحة بالغة. هل تحتاج إلى عرض محتويات جدول قاعدة بيانات؟ لا مشكلة، اسحبه عبر القالب. لكن هذا النهج قد يؤدي سريعًا إلى شيفرة متشابكة، تمزج فيها تحديثات مؤشّر قاعدة البيانات مع توليد HTML. وهذا أمر شائع في مشروع ASP سيّئ الكتابة.

وفيما تُنتج أنظمة القوالب الضعيفة شيفرة بسيطة، فإنها تميل أيضًا إلى كثير من العمل المكرر. فغالبًا ما ستحتاج، لا إلى الاحتفاظ بقيمك الأصلية في أنواع بيانات فحسب، بل أيضًا إلى إنشاء قواميس قيم تُمرَّر إلى القالب. وصيانة شيفرة كهذه ليست سهلة، وعادةً ما لا توجد طريقة يساعدك فيها المُصرِّف.

تسعى عائلة لغات القوالب في Yesod، أي اللغات الشكسبرية (Shakespearean)، إلى حلّ وسط. فمن خلال الاستفادة من الشفافية المرجعية (referential transparency) المعتادة في هاسكل، يمكننا التأكد من أن قوالبنا لا تُنتج آثارًا جانبية. غير أنها تظلّ تتمتع بوصول كامل إلى جميع المتغيّرات والدوال المتاحة في شيفرتك المكتوبة بهاسكل. ولأنها مفحوصة فحصًا كاملًا من حيث سلامة التركيب، وحلّ المتغيّرات، وأمان الأنواع في زمن التصريف، فإن الأخطاء الإملائية أقل احتمالًا بكثير في دفعك للبحث في شيفرتك عن تحديد عطب ما.

لماذا الاسم Shakespeare؟

كانت لغة HTML، أي Hamlet، أول لغة كُتبت، وقد استلهمت صياغتها في الأصل من Haml. ولأنها كانت في ذلك الوقت Haml «مبسَّطة»، بدا اسم Hamlet مناسبًا. وحين أضفنا خياري CSS وJavascript، قررنا الحفاظ على نسق التسمية مع Cassius وJulius. وفي هذه المرحلة لم يعد Hamlet يشبه Haml في شيء، لكن الاسم بقي على أي حال.

الأنواع

أحد الموضوعات الشاملة في Yesod هو الاستخدام السليم للأنواع لتخفيف العبء عن المطوّرين. وفي قوالب Yesod، لدينا مثالان رئيسيان:

  1. كل محتوى مضمَّن في قالب Hamlet يجب أن يكون من النوع Html. وكما سنرى لاحقًا، فإن هذا يفرض علينا الهروب (escape) الصحيح من HTML الخطر عند الحاجة، مع تجنّب الهروب المزدوج العرضي أيضًا.
  2. بدلًا من دمج عناوين URL مباشرةً في قالبنا، لدينا أنواع بيانات—تُعرف بعنوان URL آمن الأنواع (type-safe URLs)—تمثّل المسارات (routes) في تطبيقنا.

كمثال واقعي، لنفترض أن مستخدمًا يرسل اسمه إلى تطبيق عبر نموذج. ستُمثَّل هذه البيانات بنوع البيانات Text. والآن نودّ عرض هذا المتغيّر، المسمّى name، في صفحة. ونظام الأنواع—في زمن التصريف—يمنع إدراجه ببساطة في قالب Hamlet، لأنه ليس من النوع Html. وبدلًا من ذلك علينا تحويله بطريقة ما. ولهذا الغرض توجد دالتا تحويل:

  1. toHtml هروب تلقائيًا من أي كيانات (entities). فإذا أرسل مستخدم السلسلة ``، فإن إشارات «أصغر من» ستُحوَّل تلقائيًا إلى <.
  2. preEscapedText، من جهة أخرى، تترك المحتوى كما هو تمامًا.

إذن في حالة المدخلات غير الموثوقة من مستخدم محتمل الشر، فإن toHtml سيكون النهج الذي نوصي به. ومن جهة أخرى، لنفترض لدينا بعض HTML الساكن مخزَّنًا على خادمنا ونودّ إدراجه حرفيًا في بعض الصفحات. في تلك الحالة، يمكننا تحميله في قيمة Text ثم تطبيق preEscapedText، فبتطبيق ذلك نتجنّب أي هروب مزدوج.

افتراضيًا، ستستخدم Hamlet الدالة toHtml مع أي محتوى تحاول استيفاءه (interpolate). لذا لا تحتاج إلى إجراء تحويل صريح إلا إذا أردت تجنّب الهروب. وهذا يتبع قاعدة الميل إلى جانب الحذر.

name <- runInputPost $ ireq textField "name"
snippet <- readFile "mysnippet.html"
return [hamlet|
    <p>Welcome #{name}, you are on my site!
    <div .copyright>#{preEscapedText snippet}
|]

الخطوة الأولى في عناوين URL الآمنة أنويعيًا هي إنشاء نوع بيانات يمثّل جميع المسارات في موقعك. لنفترض أن لديك موقعًا لعرض أعداد فيبوناتشي. وسيكون للموقع صفحة منفصلة لكل عدد في المتسلسلة، إضافةً إلى الصفحة الرئيسية. ويمكن نمذجة ذلك بنوع بيانات هاسكل التالي:

data FibRoute = Home | Fib Int

ويمكننا عندئذٍ إنشاء صفحة على النحو التالي:

<p>You are currently viewing number #{show index} in the sequence. Its value is #{fib index}.
<p>
    <a href=@{Fib (index + 1)}>Next number
<p>
    <a href=@{Home}>Homepage

ثم كل ما نحتاجه هو دالة لتحويل عنوان URL آمن الأنواع إلى تمثيل نصي. وفي حالتنا، قد يبدو شيء من هذا القبيل:

render :: FibRoute -> Text
render Home = "/home"
render (Fib i) = "/fib/" ++ show i

ولحسن الحظ، تتولّى Yesod تلقائيًا كل الشيفرة الفعّالة الخاصة بتعريف أنواع بيانات عناوين URL الآمنة أنويعيًا وعرضها. وسنغطّي ذلك بعمق أكبر لاحقًا.

اللغات الأخرى

إلى جانب Hamlet، هناك ثلاث لغات أخرى: Julius وCassius وLucius. تُستخدم Julius لـJavascript؛ غير أنها لغة تمرير بسيطة (pass-through)، تكتفي بالسماح بالاستيفاء (interpolation). بمعنى آخر، باستثناء الاستخدام العرضي لصيغة الاستيفاء، يمكن إسقاط أي جزء من Javascript داخل Julius ويكون صالحًا. فمثلًا، لاختبار أداء Julius، مُرِّر jQuery عبر اللغة دون أي مشكلة.

واللغتان الأخريان صيغتان بديلتان لـCSS. وسيتعرّف فورًا من يعرف الفرق بين Sass وLess على ذلك: Cassius مُحدَّدة بالمسافات البيضاء (whitespace delimited)، بينما تستخدم Lucius الأقواس المعقوفة. وLucius في الواقع مجموعة شاملة لـCSS (superset)، ما يعني أن كل ملفات CSS الصالحة هي ملفات Lucius صالحة. وإلى جانب السماح باستيفاء النص، تتوافر بعض أنواع البيانات المساعدة لنمذجة وحدات القياس والألوان. كما تعمل عناوين URL الآمنة أنويعيًا في هذه اللغات، مما يجعل تحديد صور الخلفية أمرًا مريحًا.

إلى جانب أمان الأنواع وفحوص زمن التصريف المذكورة أعلاه، تمنحنا لغات متخصّصة لـCSS وJavascript بعض المزايا الأخرى:

  • في الإنتاج، تُصرَّف كل ملفات CSS وJavascript ضمن الملف التنفيذي النهائي، مما يزيد الأداء (بتجنّب الإدخال/الإخراج من الملفات) ويبسّط النشر.
  • وبكونها قائمة على بنية الباني الكفوء الموصوفة سابقًا، يمكن عرض القوالب بسرعة كبيرة.
  • وهناك دعم مدمج لإدراج هذه تلقائيًا في صفحات الويب النهائية. وسنتناول هذا بمزيد من التفصيل عند وصف الودجتات أدناه.

22.4. Persistent

ستحتاج معظم تطبيقات الويب إلى تخزين المعلومات في قاعدة بيانات. وقد يعني هذا تقليديًا نوعًا ما من قواعد بيانات SQL. وفي هذا الصدد، يواصل Yesod تقليدًا طويلًا، مع PostgreSQL بوصفه الواجهة الخلفية الأكثر استخدامًا لدينا. لكن كما لاحظنا في السنوات الأخيرة، لا تُعدّ SQL دائمًا الإجابة على مسألة الاستمرارية (persistence). لذا صُمّم Yesod ليعمل جيدًا مع قواعد بيانات NoSQL أيضًا، ويأتي مع واجهة خلفية لـMongoDB بوصفها مورّدًا من الدرجة الأولى.

ونتيجةً لهذا القرار التصميمي هي Persistent، خيار التخزين المفضّل في Yesod. وهناك في الواقع مصباحان يسترشد بهما Persistent: اجعله محايدًا تجاه الواجهة الخلفية بقدر الإمكان، ودع شيفرة المستخدم تخضع لفحص أنواع كامل.

وفي الوقت نفسه، ندرك تمامًا أنه من المستحيل تمامًا أن نبعد المستخدم عن جميع تفاصيل الواجهة الخلفية. لذا نوفّر نوعين من مخارج الطوارئ:

  • وظائف خاصة بالواجهة الخلفية عند الحاجة. فمثلًا، يوفّر Persistent ميزات لعمليات ربط جداول SQL وللقوائم والخلاصات (hashes) في MongoDB. وستنطبق تحذيرات قابلية النقل على نحو صحيح، لكن إن أردت هذه الوظيفة فستجدها هناك.
  • سهولة الوصول إلى تنفيذ الاستعلامات الخام (raw queries). لا نؤمن بأن أي تجريد (abstraction) يمكنه تغطية كل حالات استخدام المكتبة الأساسية. فإذا كان عليك كتابة استعلام فرعي (subquery) مترابط على خمسة جداول بـSQL، فامضِ قدمًا.

المصطلحات

أبسط أنواع البيانات في Persistent هو PersistValue. ويمثّل أي بيانات خام يمكن أن تظهر داخل قاعدة البيانات، مثل رقم أو تاريخ أو سلسلة نصية. وبالطبع، أحيانًا ستكون لديك أنواع بيانات أكثر ملاءمة للمستخدم تريد تخزينها، مثل HTML. ولهذا لدينا صنف PersistField. وداخليًا، يعبّر PersistField عن نفسه إلى قاعدة البيانات من حيث PersistValue.

كل هذا رائع جدًا، لكننا نريد أن ندمج حقولًا مختلفة معًا في صورة أكبر. ولهذا لدينا PersistEntity، وهي في الأساس مجموعة من PersistFields. وأخيرًا لدينا PersistBackend الذي يصف كيفية إنشاء هذه الكيانات وقراءتها وتحديثها وحذفها.

كمثال عملي، لنفكّر في تخزين شخص في قاعدة بيانات. نريد تخزين اسم الشخص وتاريخ ميلاده وصورة ملف شخصي (ملف PNG). ننشئ كيانًا جديدًا Person بثلاثة حقول: Text وDay وPNG. ويُخزَّن كل واحد منها في قاعدة البيانات باستخدام مُنشئ PersistValue مختلف: PersistText وPersistDay وPersistByteString على التوالي.

لا شيء مفاجئ في الربطَيْن الأولَيْن، لكن الأخير مثير للاهتمام. إذ لا يوجد مُنشئ محدّد لتخزين محتوى PNG في قاعدة بيانات، لذا نستخدم بدلًا منه نوعًا أكثر عمومية (وهي ByteString، أي تسلسل بايتات فحسب). يمكننا استخدام الآلية ذاتها لتخزين أنواع أخرى من البيانات العشوائية.

(الممارسة الأفضل الشائعة لتخزين الصور هي الاحتفاظ بالبيانات في نظام الملفات وتخزين مسار الصورة في قاعدة البيانات فحسب.نحن لا نحارب هذا النهج، بل نستخدم الصور المخزَّنة في قاعدة البيانات كمثال توضيحي.)

وكيف يمثَّل كل ذلك في قاعدة البيانات؟ لنأخذ SQL مثالًا: يتحوّل الكيان Person إلى جدول بثلاثة أعمدة (الاسم، وتاريخ الميلاد، والصورة). ويُخزَّن كل حقل كنوع SQL مختلف: يتحوّل Text إلى VARCHAR، وDay إلى Date، وPNG إلى BLOB (أو BYTEA).

والقصة مع MongoDB متشابهة جدًا. يتحوّل Person إلى مستند (document) خاص به، ويتحوّل حقوله الثلاثة كلٌّ منها إلى حقل (field) في MongoDB. ولا حاجة إلى أنواع بيانات ولا إلى إنشاء مخطط (schema) في MongoDB.

Persistent SQL MongoDB
PersistEntity جدول مستند
PersistField عمود حقل
PersistValue نوع العمود N/A

أمان الأنواع

يتولّى Persistent كل مسائل ترميز البيانات (marshaling) خلف الكواليس. وأنت كمستخدم لـPersistent، تستطيع أن تتجاهل تمامًا أن Text يتحوّل إلى VARCHAR. فأنت قادر ببساطة على تعريف أنواع بياناتك واستخدامها.

كل تفاعل مع Persistent مُنمَّط بقوة. وهذا يمنعك من وضع رقم في حقول التاريخ بالخطأ؛ فالمُصرِّف لن يقبل ذلك. وبهذه الباكرة تختفي فئات كاملة من الأخطاء الدقيقة.

لا تظهر قوة التنميط القوي بمكان أوضح منها في إعادة الهيكلة (refactoring). فلنفترض أنك كنت تخزّن أعمار المستخدمين في قاعدة البيانات، ثم إدراكت أنك أردت في الواقع تخزين تواريخ الميلاد. تستطيع إجراء تغيير في سطر واحد في ملف تعريف كياناتك، ثم التصريف، فيجد تلقائيًا كل سطر من الشيفرة يحتاج إلى تحديث.

وفي معظم اللغات الديناميكية الأنواع وأطر الويب التابعة لها، يكون النهج الموصى به لحل هذه المسألة كتابة اختبارات وحدة. فإذا كان لديك تغطية اختبارات كاملة، فإن تشغيل اختباراتك سيكشف فورًا ما الذي يحتاج إلى تحديث من الشيفرة. كل هذا حسنٌ وجيء، لكنه حلّ أضعف من الأنواع الحقيقية:

  • الأمر كله يقوم على افتراض امتلاكك تغطية اختبارات كاملة. وهذا يستغرق وقتًا إضافيًا، والأهم أنه شيفرة فعّالة كان ينبغي للمُصرِّف أن يؤديها عنك.
  • قد تكون مطوّرًا مثاليًا لا ينسى أبدًا كتابة اختبار، لكن هل يمكنك قول الشيء نفسه عن كل شخص سيمسّ شيفرتك؟
  • حتى التغطية البالغة 100% لا تضمن أنك اختبرت في واقع الأمر كل حالة. وكل ما تُثبته هو أنك اختبرت كل سطر من الشيفرة.

صياغة مشتركة بين قواعد البيانات

قد يكون إنشاء مخطط SQL يعمل مع محركات SQL متعددة أمرًا عسيرًا بما يكفي. فكيف تنشئ مخططًا سيعمل أيضًا مع قاعدة بيانات غير SQL مثل MongoDB؟

يتيح لك Persistent تعريف كياناتك بصياغة عالية المستوى، وينشئ مخطط SQL تلقائيًا. وفي حالة MongoDB، نستخدم حاليًا نهجًا بلا مخطط (schema-less). وهذا يتيح أيضًا لـPersistent أن يضمن تطابق أنواع بياناتك المكتوبة بهاسكل تمامًا مع تعريفات قاعدة البيانات.

وإضافةً إلى ذلك، فإن امتلاكنا كل هذه المعلومات يمنح Persistent القدرة على أداء وظائف أكثر تقدّمًا، مثل الترحيلات (migrations)، تلقائيًا.

الترحيلات

لا يكتفي Persistent بإنشاء ملفات المخطط عند الحاجة، بل يطبّق أيضًا ترحيلات قاعدة البيانات تلقائيًا إن أمكن. وتُعدّ تعديل قاعدة البيانات أحد الأجزاء الأقل نضجًا في معيار SQL، ولذلك يختلف كل محرك في تعامله مع هذه العملية. وعليه، يعرّف كل واجهة خلفية من واجهات Persistent مجموعتها الخاصة من قواعد الترحيل. وفي PostgreSQL، الذي يملك مجموعة غنية من قواعد ALTER TABLE، نستخدمها على نطاق واسع. ولأن SQLite يفتقر إلى معظم تلك الوظائف، فنحن نضطر إلى إنشاء جداول مؤقتة ونسخ الصفوف. أما نهج MongoDB بلا مخطط فيعني عدم الحاجة إلى دعم الترحيل.

هذه الميزة مقيَّدة عمدًا لمنع أي نوع من فقدان البيانات. فهي لن تحذف أي عمود تلقائيًا؛ وبدلًا من ذلك سترسل لك رسالة خطأ تخبرك بالعمليات غير الآمنة اللازمة للمتابعة. وعندئذٍ سيكون أمامك خيار إما تشغيل جملة SQL التي يوفّرها لك يدويًا، أو تغيير نموذج بياناتك لتجنّب السلوك الخطر.

العلاقات

Persistent غير علائقي بطبيعته، ما يعني أنه لا يشترط على الواجهات الخلفية دعم العلاقات. لكن في كثير من حالات الاستخدام قد نرغب في استخدام العلاقات. وفي تلك الحالات، سيكون لدى المطوّرين وصول كامل إليها.

لنفترض أننا نريد الآن تخزين قائمة مهارات مع كل مستخدم. فلو كنّا نكتب تطبيقًا خاصًا بـMongoDB، لأمكننا ببساطة تخزين تلك القائمة كحقل جديد في كيان Person الأصلي. لكن هذا النهج لن يعمل في SQL. وفي SQL، نسمّي هذا النوع من العلاقة علاقة واحد إلى متعدد (one-to-many).

والفكرة هي تخزين مرجع إلى الكيان «الواحد» (الشخص) مع كل كيان من كيانات «المتعدد» (المهارة). ثم إن أردنا العثور على كل مهارات شخص ما، فسنبحث ببساطة عن كل مهارة تشير إلى ذلك الشخص. ولكي يكون هذا المرجع ممكنًا، لكل كيان معرّف (ID). وأنت تعرف الآن على الأرجح أن هذه المعرّفات آمنة أنويعيًا بالكامل. ونوع بيانات معرّف الشخص هو PersonId. لذا لإضافة مهارتنا الجديدة، نضيف ببساطة ما يلي إلى تعريف الكيانات لدينا:

Skill
    person PersonId
    name Text
    description Text
    UniqueSkill person name

ويتكرّر هذا المفهوم الخاص بنوع بيانات المعرّف في كل مكان في Persistent وYesod. يمكنك التفريق (dispatch) استنادًا إلى معرّف. وفي هذه الحالة، سيحوّل Yesod تلقائيًا التمثيل النصي للمعرّف إلى التمثيل الداخلي، مع التقاط أي أخطاء تحليل في الطريق. وتُستخدم هذه المعرّفات للبحث والحذف عبر الدالتين get وdelete، وتعيدها دالتا الإدراج والاستعلام insert وselectList.

22.5. Yesod

إذا كنا ننظر إلى نموذج Model-View-Controller (MVC) المعتاد، فإن Persistent هو النموذج (model) وShakespeare هو العرض (view). وهذا يترك Yesod بوصفه المتحكّم (controller).

وأكثر ميزات Yesod أهمية هو التوجيه (routing). فهو يقدّم صياغة تعريفية (declarative) وتفويضًا آمنًا أنويعيًا (type-safe dispatch). وفوق ذلك كله، يوفّر Yesod ميزات أخرى كثيرة: توليد محتوى متدفّق (streaming)، والودجتات، والتدويل (i18n)، والملفات الساكنة (static files)، والنماذج (forms)، والمصادقة (authentication). لكن الميزة الجوهرية التي تضيفها Yesod هي في الحقيقة التوجيه.

ويجعل هذا النهج الطبقي من الأسهل على المستخدمين استبدال مكوّنات مختلفة من النظام. فبعض الأشخاص لا يهمّهم استخدام Persistent. بالنسبة لهم، لا يذكر حتى جوهر النظام شيئًا عن Persistent. وعلى نحو مماثل، رغم أن المصادقة وتقديم الملفات الساكنة ميزات مستخدَمة على نطاق واسع، لا يحتاجها كل أحد.

ومن جهة أخرى، سيرغب كثير من المستخدمين في تكامل كل هذه الميزات. وتنفيذ ذلك، مع تفعيل جميع التحسينات المتاحة في Yesod، ليس دائمًا مباشرًا. ولتبسيط العملية، يوفّر Yesod أيضًا أداة توليد هياكل (scaffolding tool) تُنشئ موقعًا أساسيًا بأكثر الميزات استخدامًا.

المسارات

بما أن التوجيه هو الوظيفة الرئيسة لـYesod فعلًا، فلنبدأ منه. وصياغة التوجيه بسيطة جدًا: نمط مورد (resource pattern)، واسم، وطرق طلب. فمثلًا قد يبدو موقع مدونة بسيطًا كالتالي:

/ HomepageR GET
/add-entry AddEntryR GET POST
/entry/#EntryId EntryR GET

ويعرّف السطر الأول الصفحة الرئيسية. وهو يقول: «أستجيب للمسار الجذري للنطاق، واسمّي HomepageR، وأجيب عن طلبات GET». (والحرف «R» اللاحق في أسماء الموارد مجرد اصطلاح، ولا يحمل أي معنى خاص سوى أنه يوحي للمطوّر بأن شيئًا هو مسار.)

ويعرّف السطر الثاني صفحة add-entry. وفي هذه المرة نستجيب لطلبات GET وPOST معًا. وقد تتساءل لماذا يشترط Yesod، بخلاف معظم الأطر، أن تصرّح بطرق طلباتك. السبب أن Yesod يحاول الالتزام بمبادئ REST بقدر الإمكان، ولفرقي GET وPOST معنيان مختلفان تمامًا. فأنت لا تصرّح بهاتين الطريقتين فحسب، بل ستعرّف لاحقًا دالتي المعالجة كلٍّ منهما على حدة. (هذه في الواقع ميزة اختيارية في Yesod. إن أردت، يمكنك ترك قائمة الطرق جانبًا وستتعامل دالة المعالجة مع جميع الطرق.)

والسطر الثالث أكثر إثارة للاهتمام بقليل. فبعد الشرطة المائلة الثانية لدينا #EntryId. وهذا يعرّف معاملًا من النوع EntryId. وقد سبنا الإشارة إلى هذه الميزة في قسم Persistent: سيحوّل Yesod الآن مكوّن المسار تلقائيًا إلى قيمة المعرّف المعنية. وبافتراض واجهة خلفية SQL (وسنناول Mongo لاحقًا)، فإن طلب المستخدم للمسار /entry/5 سيؤدي إلى استدعاء دالة المعالجة بالمعامل EntryId 5. أما إذا طلب المستخدم /entry/some-blog-post فسيعيد Yesod خطأ 404.

ومن الواضح أن هذا ممكن في معظم أطر الويب الأخرى أيضًا. فالنهج الذي يسلكه Django مثلًا يستخدم تعبيرًا نمطيًا (regular expression) لمطابقة المسارات، مثل r"/entry/(\d+)". لكن نهج Yesod يوفّر بعض المزايا:

  • كتابة «EntryId» أكثر دلالةً وملاءمةً للمطوّر بكثير من تعبير نمطي.
  • التعبيرات النمطية لا تستطيع التعبير عن كل شيء (أو على الأقل لا تستطيع ذلك بإيجاز). يمكننا استخدام /calendar/#Day في Yesod؛ فهل تريد كتابة تعبير نمطي لمطابقة التواريخ في مساراتك؟
  • كما أن Yesod يحوّل البيانات تلقائيًا نيابةً عنا. في حالة التقويم لدينا، ستستقبل دالة المعالجة قيمة Day. وفي نظير Django، ستستقبل الدالة جزءًا من النص يكون عليها أن تحوّله بنفسها. وهذه عملية مملّة ومتكررة وغير فعّالة.
  • افترضنا حتى الآن أن معرّف قاعدة البيانات مجرّد سلسلة من الأرقام. لكن ماذا لو كان أعقد من ذلك؟ فMongoDB مثلًا تستخدم GUIDs. في Yesod سيبقى #EntryId لديك صالحًا للعمل، وسيوجّه نظامُ الأنواع Yesodَ إلى كيفية تحليل المسار. أما في نظام قائم على التعبيرات النمطية، فيلزمك المرور على جميع مساراتك وتغيير \d+ إلى التعبير النمطي البشع الذي يلزم لمطابقة GUIDs.

عناوين URL الآمنة أنويعيًا

يُثري هذا النهج في التوجيه واحدة من أقوى ميزات Yesod: عناوين URL الآمنة أنويعيًا. فبدلًا من مجرّد وصل مقاطع نصية معًا للإشارة إلى مسار، يمكن تمثيل كل مسار في تطبيقك بقيمة هاسكل. وهذا يُلغي فورًا عددًا كبيرًا من أخطاء 404 Not Found: فمن المستحيل ببساطة إنتاج عنوان URL غير صالح. (لكن يبقى من الممكن إنتاج عنوان URL يؤدي إلى خطأ 404، مثلًا بالإشارة إلى تدوينة مدونة غير موجودة. لكن جميع عناوين URL ستُشكَّل على نحو صحيح.)

إذن كيف تعمل هذه السحرية؟ لكل موقع نوع بيانات للمسارات (route datatype)، ولكل نمط مورد مُنشئ خاص به. وفي مثالنا السابق، سنحصل على شيء يشبه:

data MySiteRoute = HomepageR
                 | AddEntryR
                 | EntryR EntryId

إن أردت الربط بالصفحة الرئيسية، تستخدم HomepageR. أما للربط بمدخّلة (entry) محددة، فستخدم المُنشئ EntryR مع معامل من النوع EntryId. فمثلًا، لإنشاء مدخّلة جديدة وإعادة التوجيه إليها، يمكنك كتابة:

entryId <- insert (Entry "My Entry" "Some content")
redirect RedirectTemporary (EntryR entryId)

وتتضمّن Hamlet وLucius وJulius جميعًا دعمًا مدمجًا لهذه عناوين URL الآمنة أنويعيًا. وداخل قالب Hamlet يمكنك بسهولة إنشاء رابط إلى صفحة add-entry:

<a href=@{AddEntryR}>Create a new entry.

وأفضل جزء؟ إننا، تمامًا كما مع كيانات Persistent، سيُبقيك المُصرِّف صادقًا. فإذا غيّرت أيًّا من مساراتك (مثلًا، أردت تضمين السنة والشهر في مسارات المدخلات)، سيُجبر Yesod على تحديث كل مرجع في شيفرتك بالكامل.

المعالِجات

بمجرد أن تُعرّف مساراتك، تحتاج إلى إخبار Yesod كيف تريد الاستجابة للطلبات. وهنا يأتي دور دوال المعالجة (handler functions). والإعداد بسيط: لكل مورد (مثل HomepageR) وطريقة طلب، أنشئ دالة باسم methodResourceR. وفي مثالنا السابق، سنحتاج إلى أربع دوال: getHomepageR وgetAddEntryR وpostAddEntryR وgetEntryR.

وتُمرَّر كل المعاملات المجموعة من المسار كوسائط إلى دالة المعالجة. فستأخذ getEntryR معاملًا أول من النوع EntryId، في حين لا تأخذ الدوال الأخرى أي وسائط.

وتعمل دوال المعالجة داخل مؤنِّث Handler (Handler monad)، وهو ما يوفّر قدرًا كبيرًا من الوظائف، مثل إعادة التوجيه، والوصول إلى الجلسات (sessions)، وتشغيل استعلامات قاعدة البيانات. وبالنسبة للأخير، فإن طريقة نموذجية لبدء دالة getEntryR تكون:

getEntryR entryId = do
    entry <- runDB $ get404 entryId

وسيؤدي هذا إلى تشغيل إجراء على قاعدة البيانات يجلب المدخّلة المرتبطة بالمعرّف المعطى من قاعدة البيانات. وإذا لم تكن هناك مدخّلة كهذه، فسيعيد استجابة 404.

وستعيد كل دالة معالجة قيمة ما، ويجب أن تكون نسخة (instance) من HasReps. وهذه ميزة REST أخرى في العمل: فبدلًا من مجرّد إعادة HTML أو JSON، يمكنك إعادة قيمة ستعيد أحدهما بحسب ترويسة طلب Accept في HTTP. بمعنى آخر، في Yesod المورد جزء محدَّد من البيانات، ويمكن إعادة ذلك الجزء بأحد تمثيلات (representations) كثيرة.

الودجتات (Widgets)

لنفترض أنك تريد تضمين شريط تنقّل (navbar) في بضع صفحات مختلفة من موقعك. سيحمّل هذا الشريط أحدث خمسة مقالات في المدونة (المخزَّنة في قاعدة بياناتك)، ويولّد بعض HTML، ثم يحتاج إلى بعض CSS وJavascript للتنسيق والإثراء.

وبلا واجهة أعلى مستوى تربط هذه المكوّنات ببعضها، قد يكون تنفيذ ذلك مؤلمًا. يمكنك إضافة الـCSS إلى ملف CSS الخاص بالموقع بالكامل، لكن هذا يعني إضافة تعليمات إضافية لا تحتاجها دائمًا. والأمر كذلك مع Javascript، لكن بدرجة أسوأ قليلًا: فوجود ذلك Javascript الإضافي قد يسبّب مشكلات في صفحة لم يقصد أن يكون فيها. وستكسر أيضًا معيارية (modularity) النظام لاضطرارك إلى توليد نتائج قاعدة البيانات من عدة دوال معالجة.

وفي Yesod لدينا حلّ بسيط جدًا: الودجتات (widgets). والودجت هو قطعة شيفرة تربط HTML وCSS وJavascript ببعضها، وتتيح لك إضافة محتوى إلى كلٍّ من head وbody، ويمكنها تشغيل أي شيفرة عشوائية تنتمي إلى دالة معالجة. فمثلًا، لتنفيذ شريط التنقّل لدينا:

-- Get last five blog posts. The "lift" says to run this code like we're in the handler.
entries <- lift $ runDB $ selectList [] [LimitTo 5, Desc EntryPosted]
toWidget [hamlet|
<ul .navbar>
    $forall entry <- entries
        <li>#{entryTitle entry}
|]
toWidget [lucius| .navbar { color: red } |]
toWidget [julius|alert("Some special Javascript to play with my navbar");|]

لكن هنا قوة أكبر تعمل أيضًا. حين تنتج صفحة في Yesod، يكون النهج المعتاد هو دمج عدد من الودجتات معًا في ودجت واحد يحتوي على كل محتوى صفحتك، ثم تطبيق defaultLayout. وهذه الدالة مُعرَّفة لكل موقع على حدة، وتطبّق تخطيط الموقع المعياري.

وهناك نهجان جاهزان للاستخدام للتعامل مع مكان ملفات CSS وJavascript:

  1. دمجها ووضعها في وسمَي style وscript على التوالي، داخل HTML.
  2. وضعها في ملفات خارجية والإشارة إليها بوسمَي link وscript على التوالي.

وإضافةً إلى ذلك، يمكن تصغير Javascript (minify) تلقائيًا. والخيار 2 هو النهج المفضّل، لأنه يسمح بتحسينات إضافية قليلة:

  1. تُنشأ الملفات بأسماء مبنية على تجزئة (hash) لمحتوياتها. وهذا يعني يمكنك وضع قيم مخبأة (cached) في المستقبل البعيد دون قلق من أن يتلقّى المستخدمون محتوى قديمًا.
  2. يمكن تحميل Javascript بشكل غير متزامن (asynchronously).

والنقطة الثانية تستلزم بعض الإيضاح. فالودجتات لا تحتوي على Javascript خام فحسب، بل تحتوي أيضًا على قائمة من تبعيات Javascript. فمثلًا، تشير مواقع كثيرة إلى مكتبة jQuery ثم تضيف Javascript يستخدمها. وYesod قادر على تحويل كل ذلك تلقائيًا إلى تحميل غير متزامن عبر yepnope.js.

وبمعنى آخر، تتيح لك الودجتات إنشاء شيفرة معيارية قابلة للتركيب (modular, composable) تنتهي إلى تقديم مواردك الساكنة بكفاءة بالغة.

المواقع الفرعية (Subsites)

تتشارك مواقع الويب كثيرًا في مجالات وظيفية مشتركة. ولعل أكثر مثالين شيوعًا هما تقديم الملفات الساكنة والمصادقة. وفي Yesod، يمكنك إدراج هذه الشيفرة بسهولة باستخدام موقع فرعي (subsite). وكل ما عليك فعله هو إضافة سطر إضافي إلى مساراتك. فمثلًا، لإضافة الموقع الفرعي الساكن، تكتب:

/static StaticR Static getStatic

والحجة الأولى توضّح أين يبدأ الموقع الفرعي داخل الموقع. يُستخدم الموقع الفرعي الساكن عادةً عند /static، لكن يمكنك استخدام ما تشاء. وStaticR هو اسم المسار؛ وهذا أيضًا متروك لك تمامًا، لكن الاصطلاح هو استخدام StaticR. أما Static فهو اسم الموقع الفرعي الساكن؛ وهذا اسم لا تتحكم فيه. وgetStatic دالة تعيد إعدادات الموقع الساكن، مثل مكان وجود الملفات الساكنة.

ومثل جميع معالِجَاتك، تمتلك معالِجات المواقع الفرعية أيضًا وصولًا إلى الدالة defaultLayout. وهذا يعني أن الموقع الفرعي المصمَّم جيدًا سيستخدم جلد موقعك (site skin) تلقائيًا من دون أي تدخّل إضافي منك.

22.6. الدروس المستفادة

كان Yesod مشروعًا مجزيًا جدًا للعمل عليه. فقد منحني فرصة للعمل على نظام كبير مع مجموعة متنوعة من المطوّرين. ومن الأمور التي صدمتني حقًا مدى اختلاف المنتج النهائي عمّا كنت أنويه في الأصل. بدأت Yesod بإنشاء قائمة من الأهداف. عدد قليل جدًا من المزايا الرئيسة التي نتباهى بها اليوم في Yesod موجودة في تلك القائمة، وجزء كبير من تلك القائمة لم يعد شيئًا أنوي تنفيذه. والدرس الأول هو:

ستكون لديك فكرة أفضل عن النظام الذي تحتاجه بعد أن تبدأ في العمل عليه. لا تقيِّد نفسك بأفكارك الأولية.

ولأن هذا كان أول جزء كبير من شيفرتي المكتوبة بهاسكل، تعلّمت الكثير عن اللغة أثناء تطوير Yesod. وأنا واثق أن الآخرين يشاركون الشعور نفسه: «كيف كتبت الشيفرة بهذه الطريقة أصلًا؟». ومع أن تلك الشيفرة الأولية لم تكن بالمستوى نفسه للشيفرة التي لدينا في Yesod في هذه المرحلة، إلا أنها كانت متينة بما يكفي لإطلاق المشروع. والدرس الثاني هو:

لا تدع غياب الإتقان المزعوم للأدوات التي بين يديك يثبِّطك. اكتب أفضل شيفرة تستطيع، وواصل تحسينها.

كانت إحدى أصعب خطوات تطوير Yesod هي الانتقال من فريق من شخص واحد—أنا—إلى التعاون مع آخرين. بدأ ذلك ببساطة، عبر دمج طلبات السحب (pull requests) على GitHub، ثم انتقل تدريجيًا إلى وجود عدد من المشرفين الأساسيين (core maintainers). كنت قد وضعت بعض أنماط التطوير الخاصة بي، ولم تكن مُشرَحة أو موثَّقة في أي مكان. ونتيجة لذلك، وجد المساهمون صعوبة في سحب آخر تغييراتي غير المُصدَرة وتجربتها. وقد أعاق ذلك الآخرين عند المساهمة وعند الاختبار على حد سواء.

وحين انضم Greg Weber إلى Yesod بوصفه قائدًا إضافيًا، فرض كثيرًا من معايير البرمجة التي كانت تنقصنا بشدّة. ولتعميق المشاكل، كانت هناك صعوبات كامنة في التعامل مع سلسلة أدوات تطوير هاسكل؛ وتحديدًا في التعامل مع العدد الكبير من حزم Yesod. ومنذ ذلك الحين كان أحد أهداف فريق Yesod بأكمله إنشاء نصوص برمجية وأدوات قياسية لأتمتة البناء. وكثير من هذه الأدوات تجد طريقها الآن إلى مجتمع هاسكل العام. والدرس الأخير هو:

خطِّط مبكرًا لكيفية جعل مشروعك في متناول الآخرين.