تخطي إلى المحتوى
Corex
كل المقالات

عقلان: assigns في LiveView وآلات Zag

كل شاشة Corex فيها عقلان يعملان معاً. غالباً يتعاونان. القرارات المثيرة تحدث عندما لا يتفقان.

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

بعد أن ترى هذا الخطأ مرة، تراه في كل مكان. هذا ما يحدث عندما يظن نظامان أنهما يملكان جزءاً من الحالة ولا يعرف أحدهما بالآخر. في تطبيق Phoenix LiveView يستخدم Corex، لهذين النظامين أسماء. الأول عملية LiveView على الخادم، مع assigns وhandle_event. الثاني آلة حالة Zag في المتصفح، داخل hook. هذا المنشور عن أن تقرر، لكل جزء من الحالة، أيّهما تريد أن يفوز.

العقلان اللذان يشغّلان كل صفحة

عملية LiveView هي العقل الذي تعرفه. تستقبل الأحداث عبر WebSocket، تحدّث assigns في handle_event/3، وتعيد رسم أجزاء HEEx التي تغيّرت. هذه قصة Phoenix المعتادة، وهي رائعة فيما تفعله: حمل بيانات يكون الخادم مصدر الحقيقة عنها، ودفع patches صغيرة إلى العميل عندما تتحرك.

آلة Zag هي العقل الذي يمكنك تجاهله أسابيع، حتى يرمش شيء. تعيش داخل phx-hook على جذر كل مكوّن Corex. تعرف في أي حالة الأكورديون أو combobox: أي عنصر مميّز، أي لوحة مفتوحة، أين التركيز، أي aria-* يجب أن تكون على كل جزء الآن. تستمع لضغات المفاتيح والمؤشر مباشرة في المتصفح، بلا round trip.

كلا العقلين يقومان بعمل مفيد. المكمن أن لهما مفردات متداخلة. للأكورديون قيمة. لـ combobox قيمة. للـ select قيمة. وللـ assign على الخادم قيمة أيضاً. عندما ينقر المستخدم، تحدّث الآلة قيمتها فوراً. قد لا يسمع الخادم شيئاً. عندما يدفع الخادم HTML جديداً، قد يتضمن قيمة مختلفة عما نقره المستخدم للتو. وماذا الآن؟

ثلاث طبقات، في ملفات مختلفة

إذا رسمت شاشة Corex، تنقسم إلى ثلاث طبقات صادقة. تعيش في ملفات مختلفة، في runtimes مختلفة، وتجيب عن أسئلة مختلفة.

قالب HEEx هو البنية. يعلن الأجزاء، يضع ids، ويُسلسل البيانات في data-* لتقرأها الآلة عند mount. لا ينفّذ منطقاً. هو markup وتسميات.

الـ hook هو الجسر. يعمل في المتصفح، يتصل بالجذر عند mount، يشغّل آلة، يستمع للانتقالات، يستمع لـ patches من الخادم، ويمرّر أخبار كل جانب للآخر. قصير وليس له رأي خاص.

الآلة هي السلوك. تقرر ما يُميَّز تالياً عند ضغطة مفتاح. تدير التركيز. تكتب aria-expanded الصحيح. تفعل الجزء الذي لا أحد يستمتع بكتابته يدوياً، بنفس الطريقة في كل مرة.

للخادم لا شيء من ذلك وله شيء واحد: البيانات. ما العناصر الموجودة. ما المختار. ما الصالح. الحجة الحقيقية في هذا المنشور بين بيانات الخادم وسلوك الآلة.

غير controlled، افتراضياً

افتراضياً، كل مكوّن Corex غير controlled. يمكنك تمرير value أولية، لكن بعد أول render تحتفظ الآلة بالقيمة في الذاكرة. المستخدم ينقر. تتفاعل الآلة. تتحدّث الشاشة. لا يسمع الخادم إلا إذا طلبت ذلك صراحة.

يبدو ذلك تنازلاً. في الواقع هو الافتراض الصحيح لمعظم الصفحة. FAQ لا يحتاج أن يعرف الخادم أي لوحة مفتوحة. إفصاح في صفحة تسويق لا يحتاج round trip للتوسيع. تبويبات في إعدادات خاصة بهذه الصفحة. الحالة غير controlled هي الذاكرة المحلية للمكوّن، ومعظم حالة الواجهة محلية.

<.accordion id="faq" items={@topics} />

هذا الأكورديون له دعم لوحة مفاتيح كامل، إدارة تركيز كاملة، ARIA كاملة، وصفر حركة على WebSocket بينما يقرأ المستخدم. إن أردت ردّاً خفيفاً، اربط on_value_change بحدث خادم. الآلة ما زالت تقود. الخادم يستمع.

controlled، عندما يجب أن يكون الخادم مصدر الحقيقة

هناك شاشات لا يستطيع الخادم فيها الترك. نموذج مُتحقَّق. معالج متعدد الخطوات حيث تعتمد الخطوة الثالثة على اختيار في الأولى. لوحة يجب أن تنعكس عبر تبويبات عبر Phoenix Presence. أي شيء يكون فيه عدم التزامن، ولو لإطار واحد، خطأ.

لهذه الشاشات تختار الوضع controlled. تمرّر controlled، تربط القيمة الحالية بـ assign، وتتعامل مع كل تغيير على الخادم.

<.accordion
id="faq"
controlled
value={@open}
on_value_change="faq_value_change"
items={@topics}
/>
def handle_event("faq_value_change", %{"value" => value}, socket) do
 {:noreply, assign(socket, :open, Corex.Accordion.validate_value!(value))}
end

الحلقة نفسها في كل مرة. المستخدم يتصرّف. الـ hook يدفع الحدث للخادم. الخادم يحدّث assign ويعيد الرسم. LiveView يعدّل DOM. الـ hook يكتشف patch ويخبر الآلة أن القيمة الآن كذا. الآلة تقبل. يتحدّث ARIA. تعكس الشاشة الحقيقة.

الكلمة المهمة في هذه الفقرة هي تقبل. الآلة يمكنها أخذ props جديدة بعد أن بدأت. لم تكن دائماً قادرة على ذلك، واليوم أصبحت قادرة هو اليوم الذي تحوّل فيه Phoenix Corex من مسودة إلى إصدار. هناك منشور كامل عن هذا التغيير وحده.

العقد: كاتب واحد لكل جزء من الحالة

سبب تقسيم controlled/uncontrolled هو ضمان أن واحداً فقط من العقلين يكتب كل جزء من الحالة. إن كتب الاثنان، يومض الواجهة. إن لم يكتب أحد، لا يحدث شيء. تقريباً كل خطأ إنتاج رأيته في Corex يعيش في شاشات ترك فيها الكاتب غامضاً.

اختر الكاتب لكل جزء من الحالة، لا لكل مكوّن. combobox يمكن أن يكون controlled لقيمته المختارة (النموذج يحتاج أن يعرف) وغير controlled لحالة الفتح (المستخدم لا يحتاج إذن الخادم لفتح القائمة). حوار يمكن أن يكون controlled لكونه مفتوحاً (تحليلات، روابط عميقة) وغير controlled لأي تبويب مركّز داخله. معظم المكوّنات تسمح بالمزج.

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

كيف تتعايش patches وكتابات الآلة

هناك plumbing يعمل بهدوء ويجعل كل هذا ممكناً. عندما يعدّل LiveView DOM، لا يكتب فوق كل سمة بلا تمييز. جذر Corex يعلّم السمات التي تملكها الآلة (data-state، aria-expanded، وغيرها) لتُترك أثناء patch. الـ hook يقرأ patch، يمرّر props الجديدة للآلة، والآلة تعيد كتابة سماتها.

إن لم تكن تعرف أن هذا موجوداً، يبدو الأمر وكأن الأمور تعمل. إن عرفت، فهو ما يقف بينك وبين وميض مستمر في كل مرة يتنفّس فيها الخادم. Corex يطبّق JS.ignore_attributes على كل جذر عند mount. diffs الخادم تندمج مع مخرجات الآلة بدل أن تتجادل معها.

عندما يبدو شيء خاطئاً

إن رأيت مكوّن Corex يتصرف بغرابة بعد تغيير من الخادم، ثلاثة أسئلة عادة تجده.

القيمة التي اختارها المستخدم للتو: هل يُحدَّث assign فعلاً في handle_event؟ نسيان assign(socket, :open, …) هو الجواب الممل، وهو الصحيح في أغلب الأحيان.

patch DOM: هل شغّل LiveView فعلاً على هذا التغيير؟ إن تحركت assigns غير مرتبطة فقط، قد يقرر change tracking أن لا شيء تغيّر في هذه المنطقة. هذا Phoenix يعمل كما يجب، وليس خطأ.

الـ id على العنصر المربوط: هل هو ثابت عبر إعادة الرسم؟ إن تغيّر id، يُفكّ mount الـ hook ويُعاد، تبدأ الآلة من جديد، وتفقد حالة التفاعل. ids ثابتة أهم هنا من أي مكان آخر في LiveView.

النموذج الذهني

أعود دائماً لنفس الصورة. الخادم مصدر الحقيقة للبيانات. الآلة مصدر الحقيقة للتفاعل. الـ hook رسول صغير مهذّب بينهما. التشريح هو HTML الذي يجد فيه الرسول طريقه. التصميم (في تصميم Corex) يلوّن ما تحافظ عليه الآلة. الكتالوجات المغذّاة من الخادم (كما في تسعة آلاف مطار) هي الحالة التي البيانات فيها أكبر من أن تُشحن والرسول يعمل بجد.

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


التالي في السلسلة، الـ hook نفسه، في آلة Vanilla JS التي لا تحتاج إطار عمل. ثم تصميم Corex للطبقة التي فوق هذين العقلين. ثم تسعة آلاف مطار، مئة صف للشاشة التي تضغط العقد بأقصى ما يمكن.