تعلم الدردار من جهاز تسلسل الطبل (الجزء الأول)

نشرت: 2022-03-10
ملخص سريع ↬ يوجه مطور الواجهة الأمامية براين هولت القراء من خلال بناء مُسلسِل أسطوانة في Elm. في الجزء الأول من هذه السلسلة المكونة من جزأين ، يقدم مفاهيم Elm النحوية والإعداد والمفاهيم الأساسية. سوف تتعلم كيفية العمل مع هندسة Elm من أجل إنشاء تطبيقات بسيطة.

إذا كنت مطورًا أماميًا تتابع تطور تطبيقات الصفحة الواحدة (SPA) ، فمن المحتمل أنك سمعت عن Elm ، اللغة الوظيفية التي ألهمت Redux. إذا لم تقم بذلك ، فهذه لغة ترجمة إلى JavaScript يمكن مقارنتها بمشاريع SPA مثل React و Angular و Vue.

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

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

تنمية الوعي بالتبعية

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

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

العب بالمشروع النهائي هنا ، وتحقق من الكود الخاص به هنا.

منظم الخطوة في العمل
إليك ما سيبدو عليه منظم الخطوة المكتمل أثناء العمل.

الشروع في العمل مع Elm

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

المزيد بعد القفز! أكمل القراءة أدناه ↓

خلال هذه المقالة ، سأقوم بالربط بإصدارات Ellie قيد التنفيذ ، على الرغم من أنني قمت بتطوير جهاز التسلسل محليًا. وبينما يمكن كتابة CSS بالكامل في Elm ، فقد كتبت هذا المشروع في PostCSS. يتطلب هذا القليل من التكوين لـ Elm Reactor من أجل التنمية المحلية من أجل تحميل الأنماط. من أجل الإيجاز ، لن أتطرق إلى الأنماط في هذه المقالة ، لكن روابط Ellie تتضمن جميع أنماط CSS المصغرة.

Elm هو نظام بيئي قائم بذاته يتضمن:

  • صنع الدردار
    لتجميع كود Elm الخاص بك. بينما لا يزال Webpack شائعًا في إنتاج مشاريع Elm جنبًا إلى جنب مع الأصول الأخرى ، فإنه ليس مطلوبًا. في هذا المشروع ، اخترت استبعاد Webpack ، والاعتماد على elm elm make لتجميع الكود.
  • حزمة الدردار
    مدير حزم يمكن مقارنته بـ NPM لاستخدام الحزم / الوحدات التي أنشأها المجتمع.
  • مفاعل الدردار
    لتشغيل خادم تطوير يتم تجميعه تلقائيًا. والأكثر شهرة ، أنه يتضمن Time Travelling Debugger مما يجعل من السهل التنقل خلال حالات تطبيقك وإعادة تشغيل الأخطاء.
  • الدردار
    لكتابة أو اختبار تعبيرات Elm البسيطة في المحطة.

تعتبر كافة ملفات Elm modules . ستشمل أسطر البداية لأي ملف module FileName exposing (functions) حيث FileName هو اسم الملف الحرفي ، functions هي الوظائف العامة التي تريد إتاحتها للوحدات النمطية الأخرى. مباشرة بعد تعريف الوحدة يتم استيرادها من الوحدات الخارجية. تتبع بقية الوظائف.

 module Main exposing (main) import Html exposing (Html, text) main : Html msg main = text "Hello, World!"

تعرض هذه الوحدة ، المسماة Main.elm ، وظيفة واحدة ، main ، وتستورد Html text من وحدة / حزمة Html . تتكون الوظيفة main من جزأين: نوع التعليق التوضيحي والوظيفة الفعلية. يمكن اعتبار التعليقات التوضيحية من النوع بمثابة تعريفات للوظائف. يذكرون أنواع الوسيطة ونوع الإرجاع. في هذه الحالة ، تنص وظيفتنا على أن الوظيفة main لا تأخذ أي وسيطات وتعيد Html msg . تعرض الوظيفة نفسها عقدة نصية تحتوي على "Hello، World". لتمرير الوسائط إلى دالة ، نضيف أسماء مفصولة بمسافات قبل علامة التساوي في الوظيفة. نضيف أيضًا أنواع الوسيطات إلى التعليق التوضيحي للنوع ، بترتيب الوسائط ، متبوعًا بسهم.

 add2Numbers : Int -> Int -> Int add2Numbers first second = first + second

في JavaScript ، وظيفة مثل هذه قابلة للمقارنة:

 function add2Numbers(first, second) { return first + second; }

وفي لغة مكتوبة ، مثل TypeScript ، يبدو الأمر كما يلي:

 function add2Numbers(first: number, second: number): number { return first + second; }

تأخذ add2Numbers عددين صحيحين وتعيد عددًا صحيحًا. دائمًا ما تكون القيمة الأخيرة في التعليق التوضيحي هي القيمة المرجعة لأن كل دالة يجب أن تُرجع قيمة. نسميها add2Numbers مع 2 و 3 لنحصل على 5 مثل add2Numbers 2 3 .

مثلما تربط مكونات React ، نحتاج إلى ربط كود Elm المترجم بـ DOM. الطريقة القياسية للربط هي استدعاء embed() على الوحدة النمطية الخاصة بنا وتمرير عنصر DOM إليها.

 <script> const container = document.getElementById('app'); const app = Elm.Main.embed(container); <script>

على الرغم من أن تطبيقنا لا يفعل أي شيء حقًا ، إلا أن لدينا ما يكفي لتجميع كود Elm الخاص بنا وتقديم النص. تحقق من ذلك على Ellie وحاول تغيير الوسيطات add2Numbers على السطر 26.

يعرض تطبيق Elm الأرقام المضافة
تطبيق bare-bones Elm الذي يعرض الأرقام المضافة على الشاشة.

نمذجة البيانات بأنواعها

قادمة من لغة مكتوبة ديناميكيًا مثل JavaScript أو Ruby ، ​​قد تبدو الأنواع غير ضرورية. تحدد هذه اللغات نوع الوظائف المأخوذة من القيمة التي يتم تمريرها أثناء وقت التشغيل. تعتبر وظائف الكتابة بشكل عام أسرع ، لكنك تفقد الأمان الذي يضمن أن وظائفك يمكن أن تتفاعل بشكل صحيح مع بعضها البعض.

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

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

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

التطبيق النهائي المكون من مسارات مع تسلسل الخطوات
لقطة شاشة لتطبيقنا النهائي ، مكونة من مسارات متتابعة من الخطوات.

نمذجة تطبيقنا في JavaScript

للحصول على فكرة أفضل عن أنواعنا ، دعنا نفكر في كيفية تصميم مُسلسِل الأسطوانة هذا في JavaScript. هناك مجموعة من المسارات. يحتوي كل كائن مسار على معلومات عن نفسه: اسم المسار ، والعينة / المقطع الذي سيتم تشغيله ، وتسلسل قيم الخطوة.

 tracks: [ { name: "Kick", clip: "kick.mp3", sequence: [On, Off, Off, Off, On, etc...] }, { name: "Snare", clip: "snare.mp3", sequence: [Off, Off, Off, Off, On, etc...] }, etc... ]

نحتاج إلى إدارة حالة التشغيل بين التشغيل والإيقاف.

 playback: "playing" || "stopped"

أثناء التشغيل ، نحتاج إلى تحديد الخطوة التي يجب تشغيلها. يجب أن نأخذ في الاعتبار أيضًا أداء التشغيل ، وبدلاً من اجتياز كل تسلسل في كل مسار في كل مرة يتم زيادة الخطوة ؛ يجب علينا تقليل جميع الخطوات النشطة في تسلسل تشغيل واحد. تمثل كل مجموعة ضمن تسلسل التشغيل جميع العينات التي يجب تشغيلها. على سبيل المثال ، تعني ["kick", "hat"] أنه يجب تشغيل عينات الركلة والمرتفع ، بينما تعني ["hat"] فقط يجب أن يلعب هاي هات. نحتاج أيضًا إلى كل مجموعة لتقييد التفرد بالعينة ، لذلك لا ينتهي بنا الأمر بشيء مثل ["hat", "hat", "hat"] .

 playbackPosition: 1 playbackSequence: [ ["kick", "hat"], [], ["hat"], [], ["snare", "hat"], [], ["hat"], [], ... ],

ونحتاج إلى ضبط وتيرة التشغيل ، أو BPM.

 bpm: 120

النمذجة مع الأنواع في علم

إن نسخ هذه البيانات إلى أنواع Elm هو في الأساس وصف لما نتوقع أن تتكون منه بياناتنا. على سبيل المثال ، نحن نشير بالفعل إلى نموذج البيانات الخاص بنا كنموذج ، لذلك نطلق عليه اسم مستعار من النوع. تُستخدم الأسماء المستعارة للنوع لتسهيل قراءة التعليمات البرمجية. إنها ليست نوعًا بدائيًا مثل قيمة منطقية أو عدد صحيح ؛ إنها ببساطة أسماء نعطيها نوعًا بدائيًا أو بنية بيانات. باستخدام واحد ، نحدد أي بيانات تتبع هيكل نموذجنا كنموذج وليس كهيكل مجهول. في العديد من مشاريع Elm ، يتم تسمية الهيكل الرئيسي باسم Model.

 type alias Model = { tracks : Array Track , playback : Playback , playbackPosition : PlaybackPosition , bpm : Int , playbackSequence : Array (Set Clip) }

على الرغم من أن نموذجنا يشبه إلى حد ما كائن JavaScript ، إلا أنه يصف Elm Record. تُستخدم السجلات لتنظيم البيانات ذات الصلة في عدة حقول لها نوع خاص بها من التعليقات التوضيحية. يسهل الوصول إليها باستخدام field.attribute ، كما يسهل تحديثها وهو ما سنراه لاحقًا. الكائنات والسجلات متشابهة جدًا ، مع بعض الاختلافات الرئيسية:

  • لا يمكن استدعاء الحقول غير الموجودة
  • لن تكون الحقول null أو undefined أبدًا
  • لا يمكن استخدام this self

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

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

 type alias Track = { name : String , clip : Clip , sequence : Array Step } type Step = On | Off type alias Clip = String

نحن نعلم أن التسلسل سيكون عبارة عن مجموعة من قيم التشغيل / الإيقاف. يمكننا تعيينها كمجموعة من القيم المنطقية ، مثل sequence : Array Bool ، لكننا سنفقد فرصة للتعبير عن نموذج أعمالنا! بالنظر إلى أن أدوات تسلسل الخطوات مصنوعة من خطوات ، فإننا نحدد نوعًا جديدًا يسمى الخطوة . يمكن أن تكون الخطوة اسمًا مستعارًا من النوع boolean ، ولكن يمكننا المضي قدمًا خطوة أخرى: تحتوي الخطوات على قيمتين محتملتين ، تشغيل وإيقاف ، لذلك هذه هي الطريقة التي نحدد بها نوع الاتحاد. الآن يمكن فقط تشغيل أو إيقاف الخطوات ، مما يجعل جميع الحالات الأخرى مستحيلة.

نحدد نوعًا آخر للتشغيل ، وهو اسم مستعار لـ Playback ، ونستخدم Clip عند تعريف playbackSequence PlaybackPosition تحتوي على مجموعات من المقاطع. تم تعيين BPM كمعيار Int .

 type Playback = Playing | Stopped type alias PlaybackPosition = Int

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

استخدام Elm Architecture

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

  • النموذج ، الذي يحتوي على حالة التطبيق ، والهيكل الذي نكتبه نموذج مستعار
  • وظيفة التحديث التي تقوم بتحديث الحالة
  • ووظيفة العرض التي تجعل الدولة بصريا

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

  • Types.elm ، التي تحتوي على جميع تعريفات النوع
  • Main.elm ، الذي يقوم بتهيئة البرنامج وتشغيله
  • Update.elm ، يحتوي على وظيفة التحديث التي تدير الحالة
  • View.elm ، التي تحتوي على كود Elm لعرضها في HTML

جارٍ بدء تطبيقنا

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

 type alias Model = { track : Track } type alias Track = { name : String , sequence : Array Step } type Step = On | Off type alias Clip = String

لتصيير Elm بتنسيق HTML ، نستخدم حزمة Elm Html. لديها خيارات لإنشاء ثلاثة أنواع من البرامج التي تعتمد على بعضها البعض:

  • برنامج المبتدئين
    برنامج مخفض يستبعد الآثار الجانبية ومفيد بشكل خاص لتعلم Elm Architecture.
  • برنامج
    البرنامج القياسي الذي يعالج الآثار الجانبية ، ومفيد للعمل مع قواعد البيانات أو الأدوات الموجودة خارج Elm.
  • برنامج مع الأعلام
    برنامج موسع يمكنه تهيئة نفسه ببيانات حقيقية بدلاً من البيانات الافتراضية.

من الممارسات الجيدة استخدام أبسط نوع ممكن من البرامج لأنه من السهل تغييره لاحقًا باستخدام المترجم. هذه ممارسة شائعة عند البرمجة في Elm ؛ استخدم فقط ما تحتاجه وقم بتغييره لاحقًا. لأغراضنا ، نعلم أننا بحاجة إلى التعامل مع JavaScript ، والذي يُعتبر من الآثار الجانبية ، لذلك نقوم بإنشاء Html.program . في Main.elm نحتاج إلى تهيئة البرنامج عن طريق تمرير الوظائف إلى مجالاته.

 main : Program Never Model Msg main = Html.program { init = init , view = view , update = update , subscriptions = always Sub.none }

يقوم كل حقل في البرنامج بتمرير وظيفة إلى Elm Runtime ، والتي تتحكم في تطبيقنا. باختصار ، لعبة Elm Runtime:

  • يبدأ البرنامج بقيمنا الأولية من init .
  • يعرض العرض الأول عن طريق تمرير نموذجنا المُهيأ في view .
  • يعيد عرض العرض باستمرار عند تمرير الرسائل update من طرق العرض أو الأوامر أو الاشتراكات.

محليًا ، سيتم استيراد وظائف view update الخاصة بنا من View.elm و Update.elm على التوالي ، وسننشئهما في لحظة. تستمع subscriptions إلى الرسائل لإحداث تحديثات ، ولكن في الوقت الحالي ، نتجاهلها من خلال تعيين always Sub.none . وظيفتنا الأولى ، init ، هيئ النموذج. فكر في init مثل القيم الافتراضية للتحميل الأول. نحدده بمسار واحد يسمى "kick" وسلسلة من خطوات Off. نظرًا لأننا لا نحصل على بيانات غير متزامنة ، فإننا نتجاهل صراحة الأوامر باستخدام Cmd.none بدون آثار جانبية.

 init : ( Model, Cmd.Cmd Msg ) init = ( { track = { sequence = Array.initialize 16 (always Off) , name = "Kick" } } , Cmd.none )

نوع الشرح التوضيحي الخاص بنا يطابق برنامجنا. إنها بنية بيانات تسمى tuple ، والتي تحتوي على عدد ثابت من القيم. في حالتنا ، Model والأوامر. في الوقت الحالي ، نتجاهل دائمًا الأوامر باستخدام Cmd.none حتى نكون مستعدين للتعامل مع الآثار الجانبية لاحقًا. تطبيقنا لا يقدم شيئًا ، لكنه يجمع!

تقديم طلبنا

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

 <div class="track"> <p class "track-title">Kick</p> <div class="track-sequence"> <button class="step _active"></button> <button class="step"></button> <button class="step"></button> <button class="step"></button> etc... </div> </div>

سنبني ثلاث وظائف لتقديم وجهات نظرنا:

  1. واحد لتقديم مسار واحد ، والذي يحتوي على اسم المسار والتسلسل
  2. آخر لتقديم التسلسل نفسه
  3. ومرة أخرى لعرض كل زر خطوة فردي داخل التسلسل

ستعرض وظيفة العرض الأولى لدينا مسارًا واحدًا. نحن نعتمد على نوع التعليق التوضيحي الخاص بنا ، renderTrack : Track -> Html Msg ، لفرض تمرير مسار واحد. يعني استخدام الأنواع أننا نعلم دائمًا أن renderTrack سيكون له مسار. لا نحتاج إلى التحقق مما إذا كان حقل name موجودًا في السجل ، أو إذا مررنا سلسلة بدلاً من سجل. لن يقوم Elm بالتجميع إذا حاولنا تمرير أي شيء آخر غير Track to renderTrack . والأفضل من ذلك ، إذا ارتكبنا خطأ وحاولنا بطريق الخطأ تمرير أي شيء بخلاف المسار إلى الوظيفة ، فسيعطينا المترجم رسائل ودية لتوجيهنا في الاتجاه الصحيح.

 renderTrack : Track -> Html Msg renderTrack track = div [ class "track" ] [ p [ class "track-title" ] [ text track.name ] , div [ class "track-sequence" ] (renderSequence track.sequence) ]

قد يبدو الأمر واضحًا ، ولكن كل Elm هو Elm ، بما في ذلك كتابة HTML. لا توجد لغة قوالب أو تجريد من أجل كتابة HTML - كل شيء هو Elm. عناصر HTML هي وظائف Elm ، والتي تأخذ الاسم وقائمة السمات وقائمة بالأطفال. لذا فإن div [ class "track" ] [] مخرجات <div class="track"></div> . القوائم مفصولة بفواصل في Elm ، لذا فإن إضافة معرف إلى div سيبدو مثل div [ class "track", id "my-id" ] [] .

يمرر track-sequence التفاف div تسلسل المسار إلى renderSequence الثانية ، "تسلسل العرض". يستغرق تسلسلًا ويعيد قائمة بأزرار HTML. يمكننا الاحتفاظ renderSequence في renderTrack لتخطي الوظيفة الإضافية ، لكني أجد تقسيم الوظائف إلى أجزاء أصغر أسهل بكثير في التفكير فيها. بالإضافة إلى ذلك ، نحصل على فرصة أخرى لتحديد نوع التعليق التوضيحي الأكثر صرامة.

 renderSequence : Array Step -> List (Html Msg) renderSequence sequence = Array.indexedMap renderStep sequence |> Array.toList

نقوم بتعيين كل خطوة في التسلسل ونمررها إلى دالة renderStep . في تعيين جافا سكريبت مع فهرس سيتم كتابته مثل:

 sequence.map((node, index) => renderStep(index, node))

بالمقارنة مع JavaScript ، يتم عكس التعيين في Elm تقريبًا. نسمي Array.indexedMap ، والذي يأخذ وسيطين: الوظيفة التي سيتم تطبيقها في الخريطة ( renderStep ) ، والمصفوفة المطلوب تعيينها ( sequence ). renderStep هي آخر وظيفة لدينا وهي تحدد ما إذا كان الزر نشطًا أم غير نشط. نستخدم indexedMap لأننا نحتاج إلى تمرير فهرس الخطوة (الذي نستخدمه كمعرف) إلى الخطوة نفسها من أجل تمريرها إلى وظيفة التحديث.

 renderStep : Int -> Step -> Html Msg renderStep index step = let classes = if step == On then "step _active" else "step" in button [ class classes ] []

يقبل renderStep الفهرس باعتباره الوسيطة الأولى ، والخطوة على أنها الثانية ، ويعيد HTML الذي تم عرضه. باستخدام let...in block لتحديد الوظائف المحلية ، نقوم بتعيين الفئة _active إلى On Steps ، واستدعاء وظيفة الفئات الخاصة بنا في قائمة سمات الزر.

مسار الركلة الذي يحتوي على سلسلة من الخطوات
مسار الركلة الذي يحتوي على سلسلة من الخطوات

تحديث حالة التطبيق

في هذه المرحلة ، يعرض تطبيقنا 16 خطوة في تسلسل الركلة ، لكن النقر لا ينشط الخطوة. لتحديث حالة الخطوة ، نحتاج إلى تمرير رسالة ( Msg ) إلى وظيفة التحديث. نقوم بذلك عن طريق تحديد رسالة وإرفاقها بمعالج الحدث للزر الخاص بنا.

في Types.elm ، نحتاج إلى تحديد رسالتنا الأولى ، ToggleStep . سوف يستغرق الأمر Int لفهرس التسلسل Step . بعد ذلك ، في renderStep ، نرفق الرسالة ToggleStep الزر عند النقر ، جنبًا إلى جنب مع فهرس التسلسل والخطوة كوسيطات. سيؤدي هذا إلى إرسال الرسالة إلى وظيفة التحديث الخاصة بنا ، ولكن في هذه المرحلة ، لن يفعل التحديث أي شيء في الواقع.

 type Msg = ToggleStep Int Step renderStep index step = let ... in button [ onClick (ToggleStep index step) , class classes ] []

الرسائل من الأنواع العادية ، لكننا حددناها على أنها النوع الذي يسبب التحديثات ، وهو العرف في Elm. في Update.elm نتبع Elm Architecture للتعامل مع تغييرات حالة النموذج. ستأخذ وظيفة التحديث الخاصة بنا Msg الحالي ، Model نموذجًا جديدًا وربما أمرًا. تتعامل الأوامر مع الآثار الجانبية ، والتي سننظر فيها في الجزء الثاني. نعلم أنه سيكون لدينا العديد من أنواع Msg ، لذلك قمنا بإعداد كتلة حالة مطابقة النمط. هذا يجبرنا على التعامل مع جميع حالاتنا مع فصل تدفق الحالة أيضًا. وسيتأكد المترجم من أننا لن نفوت أي حالات يمكن أن تغير نموذجنا.

يتم تحديث سجل في Elm بشكل مختلف قليلاً عن تحديث كائن في JavaScript. لا يمكننا تغيير حقل في السجل مباشرة مثل record.field = * لأننا لا نستطيع استخدام this أو self ، لكن لدى Elm مساعدين مدمجين. بالنظر إلى سجل مثل brian = { name = "brian" } ، يمكننا تحديث حقل الاسم مثل { brian | name = "BRIAN" } { brian | name = "BRIAN" } . يتبع التنسيق { record | field = newValue } { record | field = newValue } .

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

  1. واحد لتبديل قيمة الخطوة
  2. واحد لإرجاع تسلسل جديد ، يحتوي على قيمة الخطوة المحدثة
  3. آخر لاختيار المسار الذي ينتمي إليه التسلسل
  4. ووظيفة أخيرة لإرجاع مسار جديد ، يحتوي على التسلسل المحدث الذي يحتوي على قيمة الخطوة المحدثة

نبدأ بـ ToggleStep لتبديل قيمة خطوة تسلسل المسار بين On و Off. نستخدم let...in block مرة أخرى لإنشاء وظائف أصغر داخل بيان الحالة. إذا كانت الخطوة معطلة بالفعل ، فإننا نجعلها قيد التشغيل ، والعكس صحيح.

 toggleStep = if step == Off then On else Off

سيتم استدعاء toggleStep من newSequence . البيانات غير قابلة للتغيير في اللغات الوظيفية ، لذلك بدلاً من تعديل التسلسل ، فإننا في الواقع نقوم بإنشاء تسلسل جديد بقيمة خطوة محدثة لتحل محل القديم.

 newSequence = Array.set index toggleStep selectedTrack.sequence

يستخدم newSequence Array.set للعثور على الفهرس الذي نريد تبديله ، ثم ينشئ التسلسل الجديد. إذا لم تعثر المجموعة على الفهرس ، فإنها ترجع نفس التسلسل. وهو يعتمد على selectedTrack.sequence لمعرفة التسلسل المراد تعديله. selectedTrack هي وظيفة مساعدنا الرئيسية المستخدمة حتى نتمكن من الوصول إلى سجلنا المتداخل. في هذه المرحلة ، الأمر بسيط بشكل مدهش لأن نموذجنا يحتوي على مسار واحد فقط.

 selectedTrack = model.track

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

 newTrack = { selectedTrack | sequence = newSequence }

يتم استدعاء newTrack خارج let...in block ، حيث نعيد نموذجًا جديدًا ، يحتوي على المسار الجديد ، والذي يعيد عرض العرض. نحن لا نمرر الآثار الجانبية ، لذلك نستخدم Cmd.none مرة أخرى. تبدو وظيفة update بالكامل كما يلي:

 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of ToggleStep index step -> let selectedTrack = model.track newTrack = { selectedTrack | sequence = newSequence } toggleStep = if step == Off then On else Off newSequence = Array.set index toggleStep selectedTrack.sequence in ( { model | track = newTrack } , Cmd.none )

عندما نقوم بتشغيل برنامجنا ، نرى مسارًا معروضًا بسلسلة من الخطوات. يؤدي النقر فوق أي من أزرار الخطوات إلى تشغيل ToggleStep ، والذي يضرب وظيفة التحديث الخاصة بنا لاستبدال حالة النموذج.

مسار الركلة يحتوي على سلسلة من الخطوات النشطة
مسار الركلة يحتوي على سلسلة من الخطوات النشطة

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

اخذ استراحة

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

في الجزء الثاني ، سنتعمق في واحدة من أفضل ميزات Elm: استخدام المترجم لإعادة تشكيل مُسلسِل الخطوات لدينا. بالإضافة إلى ذلك ، سنتعلم كيفية التعامل مع الأحداث المتكررة ، باستخدام أوامر الآثار الجانبية ، والتفاعل مع JavaScript. ابقوا متابعين!