تحكم عالي المستوى مع إدارة حالة Redux: برنامج تعليمي لـ ClojureScript

نشرت: 2022-03-11

مرحبًا بك مرة أخرى في الإصدار الثاني المثير من Unearthing ClojureScript! في هذا المنشور ، سأغطي الخطوة الكبيرة التالية للتعامل بجدية مع ClojureScript: إدارة الحالة - في هذه الحالة ، باستخدام React.

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

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

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

بالمقارنة ، يمكن أن يؤدي وجود متغيرات عالمية (أو إصدارات ساذجة أخرى من الحالة) إلى مشكلات التزامن يصعب تتبعها ، مما يؤدي إلى عدم تحديث المكونات عندما تتوقعها ، أو العكس.

فكيف يمكن معالجة هذا؟ بالنسبة لأولئك الذين هم على دراية بـ React ، ربما تكون قد جربت Redux ، وهي حاوية حالة لتطبيقات JavaScript. ربما تكون قد وجدت هذا من إرادتك ، والبحث بجرأة عن نظام يمكن إدارته للحفاظ على الحالة. أو ربما تكون قد عثرت عليه للتو أثناء القراءة عن JavaScript وأدوات الويب الأخرى.

بغض النظر عن الطريقة التي ينظر بها الناس إلى Redux ، في تجربتي ينتهي بهم الأمر عمومًا بفكرتين:

  • "أشعر أنني يجب أن أستخدم هذا لأن الجميع يقول إن عليّ استخدامه."
  • "أنا لا أفهم تمامًا لماذا هذا أفضل."

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

لأولئك الجدد على Clojure

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

استرجع أحد أساسيات Clojure التي تنطبق على ClojureScript أيضًا: بشكل افتراضي ، البيانات غير قابلة للتغيير. يعد هذا أمرًا رائعًا للتطوير والحصول على ضمانات بأن ما تقوم بإنشائه في timestep N لا يزال كما هو في timestep> N. يوفر لنا ClojureScript أيضًا طريقة ملائمة للحصول على حالة قابلة للتغيير إذا احتجنا إليها ، عبر مفهوم atom .

تشبه atom في ClojureScript إلى حد كبير AtomicReference في Java: فهي توفر كائنًا جديدًا يقفل محتوياته بضمانات التزامن. تمامًا كما هو الحال في Java ، يمكنك وضع أي شيء تريده في هذا الكائن - من ذلك الحين فصاعدًا ، ستكون تلك الذرة مرجعًا ذريًا لما تريده.

بمجرد حصولك على atom ، يمكنك تعيين قيمة جديدة فيها تلقائيًا باستخدام reset! دالة (لاحظ ! في الوظيفة - في لغة Clojure ، يُستخدم هذا غالبًا للإشارة إلى أن العملية مجهولة أو غير نقية).

لاحظ أيضًا أنه - على عكس Java - لا يهتم Clojure بما تضعه في atom . يمكن أن تكون سلسلة أو قائمة أو كائنًا. الكتابة الديناميكية ، حبيبي!

 (def my-mutable-map (atom {})) ; recall that {} means an empty map in Clojure (println @my-mutable-map) ; You 'dereference' an atom using @ ; -> this prints {} (reset! my-mutable-map {:hello "there"}) ; atomically set the atom (reset! my-mutable-map "hello, there!") ; don't forget Clojure is dynamic :)

يوسع الكاشف هذا المفهوم atom بذرتها الخاصة. (إذا لم تكن على دراية بـ Reagent ، فتحقق من المنشور قبل ذلك.) هذا يتصرف بشكل مماثل مع atom ClojureScript ، إلا أنه يؤدي أيضًا إلى تشغيل أحداث تصيير في Reagent ، تمامًا مثل مخزن الحالة المدمج في React.

مثال:

 (ns example (:require [reagent.core :refer [atom]])) ; in this module, atom now refers ; to reagent's atom. (def my-atom (atom "world!")) (defn component [] [:div [:span "Hello, " @my-atom] [:input {:type "button" :value "Press Me!" :on-click #(reset! My-atom "there!")}]])

سيعرض هذا <div> واحدًا يحتوي على <span> يقول "مرحبًا ، أيها العالم!" وزر ، كما قد تتوقع. الضغط على هذا الزر سيغير ذريتي my-atom لاحتواء "there!" . سيؤدي ذلك إلى إعادة رسم المكون ، مما ينتج عنه امتداد عبارة "مرحبًا ، هناك!" في حين أن.

نظرة عامة على إدارة الحالة كما تم التعامل معها بواسطة Redux و Reagent.

يبدو هذا بسيطًا بما يكفي للطفرة المحلية على مستوى المكونات ، ولكن ماذا لو كان لدينا تطبيق أكثر تعقيدًا يحتوي على مستويات متعددة من التجريد؟ أو إذا كنا بحاجة إلى مشاركة الحالة المشتركة بين مكونات فرعية متعددة ومكوناتها الفرعية؟

مثال أكثر تعقيدًا

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

 (ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [on-login] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(on-login @username @password)}]])

سنستضيف بعد ذلك مكون تسجيل الدخول هذا داخل app.cljs الرئيسي لدينا ، مثل:

 (ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- STATE (def token (atom nil)) ;; -- LOGIC -- (defn- do-login-io [username password] (let [t (complicated-io-login-operation username password)] (reset! token t))) ;; -- VIEW -- (defn component [] [:div [login/component do-login-io]])

وبالتالي فإن سير العمل المتوقع هو:

  1. ننتظر حتى يقوم المستخدم بإدخال اسم المستخدم وكلمة المرور الخاصة به والضغط على إرسال.
  2. سيؤدي هذا إلى تشغيل وظيفة do-login-io بنا في المكون الرئيسي.
  3. تقوم وظيفة do-login-io ببعض عمليات الإدخال / الإخراج (مثل تسجيل الدخول على الخادم واسترداد رمز مميز).

إذا كانت هذه العملية محظورة ، فنحن بالفعل في كومة من المشاكل ، حيث تم تجميد تطبيقنا - إذا لم يكن الأمر كذلك ، فعندئذٍ لدينا مشكلة غير متزامنة!

بالإضافة إلى ذلك ، نحتاج الآن إلى توفير هذا الرمز المميز لجميع مكوناتنا الفرعية التي تريد إجراء استعلامات لخادمنا. إعادة هيكلة الكود أصبحت أكثر صعوبة!

أخيرًا ، لم يعد مكوننا الآن تفاعليًا بحتًا - فهو الآن متواطئ في إدارة حالة بقية التطبيق ، مما يؤدي إلى تشغيل الإدخال / الإخراج ويكون بشكل عام مصدر إزعاج قليلاً.

دروس ClojureScript: أدخل Redux

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

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

في سياقنا ، يتم تنفيذ Redux بواسطة إحدى مكتبات ClojureScript العديدة المتاحة ؛ هذا واحد يسمى إعادة الإطار. إنه يوفر غلافًا Clojure-ified حول Redux والذي (في رأيي) يجعله متعة مطلقة للاستخدام.

أساسيات

يرفع Redux حالة التطبيق الخاص بك ، مما يترك مكوناتك خفيفة الوزن. يحتاج المكون المعاد تنشيطه فقط إلى التفكير في:

  • ما يبدو عليه
  • ما هي البيانات التي تستهلكها
  • ما الأحداث التي تثيرها

يتم التعامل مع الباقي خلف الكواليس.

للتأكيد على هذه النقطة ، دعنا نعيد إضافة صفحة تسجيل الدخول أعلاه.

قاعدة البيانات

أول الأشياء أولاً: نحتاج إلى تحديد الشكل الذي سيبدو عليه نموذج التطبيق الخاص بنا. نقوم بذلك من خلال تحديد شكل بياناتنا والبيانات التي يمكن الوصول إليها في جميع أنحاء التطبيق.

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

لنقم بإنشاء نموذج مرجعي لقاعدة البيانات الخاصة بنا وحدد الرمز المميز الخاص بنا:

 (ns unearthing-clojurescript.state.db (:require [cljs.spec.alpha :as s] [re-frame.core :as re-frame])) (s/def ::token string?) (s/def ::db (s/keys :opt-un [::token])) (def default-db {:token nil})

هناك بعض النقاط المثيرة للاهتمام الجديرة بالملاحظة هنا:

  • نستخدم مكتبة spec في Clojure لوصف كيف من المفترض أن تبدو بياناتنا. هذا مناسب بشكل خاص في لغة ديناميكية مثل Clojure [سيناريو].
  • في هذا المثال ، نحن فقط نتتبع رمزًا مميزًا عالميًا سيمثل مستخدمنا بمجرد تسجيل الدخول. هذا الرمز المميز عبارة عن سلسلة بسيطة.
  • ومع ذلك ، قبل أن يقوم المستخدم بتسجيل الدخول ، لن يكون لدينا رمز مميز. يتم تمثيل ذلك بالكلمة الرئيسية :opt-un ، والتي تعني "اختياري ، غير مؤهل". (في Clojure ، قد تكون الكلمة الرئيسية العادية شيئًا مثل :cat ، في حين أن الكلمة الرئيسية المؤهلة قد تكون شيئًا مثل :animal/cat . يحدث التأهيل عادةً على مستوى الوحدة - وهذا يمنع الكلمات الرئيسية في وحدات مختلفة من ضرب بعضها البعض.)
  • أخيرًا ، نحدد الحالة الافتراضية لقاعدة البيانات الخاصة بنا ، وهي كيفية تهيئتها.

في أي وقت ، يجب أن نكون على ثقة من أن البيانات الموجودة في قاعدة البيانات الخاصة بنا تطابق المواصفات الخاصة بنا هنا.

الاشتراكات

الآن بعد أن وصفنا نموذج البيانات الخاص بنا ، نحتاج إلى عكس الطريقة التي تُظهر بها وجهة نظرنا تلك البيانات. لقد وصفنا بالفعل كيف تبدو وجهة نظرنا في مكون Redux - والآن نحتاج ببساطة إلى ربط وجهة نظرنا بقاعدة البيانات الخاصة بنا.

مع Redux ، لا يمكننا الوصول إلى قاعدة البيانات الخاصة بنا مباشرة - قد يؤدي ذلك إلى مشكلات تتعلق بدورة الحياة والتزامن. بدلاً من ذلك ، نسجل علاقتنا بواجهة من قاعدة البيانات من خلال الاشتراكات .

يخبر الاشتراك إعادة الإطار (والكاشف) بأننا نعتمد على جزء من قاعدة البيانات ، وإذا تم تغيير هذا الجزء ، فيجب إعادة تقديم مكون Redux الخاص بنا.

الاشتراكات سهلة التحديد:

 (ns unearthing-clojurescript.state.subs (:require [re-frame.core :refer [reg-sub]])) (reg-sub :token ; <- the name of the subscription (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any token)) ; args passed to the subscribe function (not used here)

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

هناك الكثير والكثير الذي يمكنك فعله مع الاشتراكات - مثل تحديد طرق العرض على الأقسام الفرعية من قاعدة البيانات لنطاق أضيق لإعادة العرض - لكننا سنبقي الأمر بسيطًا في الوقت الحالي!

الأحداث

لدينا قاعدة بياناتنا ، ولدينا وجهة نظرنا في قاعدة البيانات. الآن نحن بحاجة إلى إطلاق بعض الأحداث! في هذا المثال ، لدينا نوعان من الأحداث:

  • الحدث الخالص (ليس له أي آثار جانبية) لكتابة رمز جديد في قاعدة البيانات.
  • حدث I / O ( له تأثير جانبي) للخروج وطلب رمزنا المميز من خلال بعض تفاعل العميل.

سنبدأ مع السهل. حتى أن إعادة الإطار توفر وظيفة بالضبط لهذا النوع من الأحداث:

 (ns unearthing-clojurescript.state.events (:require [re-frame.core :refer [reg-event-db reg-event-fx reg-fx] :as rf] [unearthing-clojurescript.state.db :refer [default-db]])) ; our start up event that initialises the database. ; we'll trigger this in our core.cljs (reg-event-db :initialise-db (fn [_ _] default-db)) ; a simple event that places a token in the database (reg-event-db :store-login (fn [db [_ token]] (assoc db :token token)))

مرة أخرى ، الأمر بسيط جدًا هنا - لقد حددنا حدثين. الأول هو تهيئة قاعدة البيانات الخاصة بنا. (انظر كيف تتجاهل كلتا الوسيطتين؟ نحن دائمًا نهيئ قاعدة البيانات باستخدام default-db بنا!) والثاني هو تخزين الرمز الخاص بنا بمجرد أن نحصل عليه.

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

أخيرًا ، نحتاج إلى حدث تسجيل الدخول الخاص بنا. سنضعه تحت الآخرين:

 (reg-event-fx :login (fn [{:keys [db]} [_ credentials]] {:request-token credentials})) (reg-fx :request-token (fn [{:keys [username password]}] (let [token (complicated-io-login-operation username password)] (rf/dispatch [:store-login token]))))

تتشابه وظيفة reg-event-fx إلى حد كبير مع reg-event-db ، على الرغم من وجود بعض الاختلافات الدقيقة.

  • الحجة الأولى لم تعد مجرد قاعدة البيانات نفسها. يحتوي على العديد من الأشياء الأخرى التي يمكنك استخدامها لإدارة حالة التطبيق.
  • الحجة الثانية تشبه إلى حد كبير في reg-event-db .
  • بدلاً من مجرد إرجاع db الجديد ، نعيد بدلاً من ذلك خريطة تمثل جميع التأثيرات ("fx") التي يجب أن تحدث لهذا الحدث. في هذه الحالة ، نسمي ببساطة :request-token تأثير ، والذي تم تعريفه أدناه. أحد التأثيرات الصالحة الأخرى هو :dispatch ، والذي يستدعي ببساطة حدثًا آخر.

بمجرد إرسال تأثيرنا ، يتم استدعاء تأثير :request-token ، والذي يؤدي عملية تسجيل الدخول إلى الإدخال / الإخراج طويلة المدى. بمجرد الانتهاء من ذلك ، فإنه يعيد إرسال النتيجة مرة أخرى إلى حلقة الحدث ، وبالتالي إكمال الدورة!

دروس ClojureScript: النتيجة النهائية

وبالتالي! لقد حددنا تجريد التخزين لدينا. كيف يبدو المكون الآن؟

 (ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]] [re-frame.core :as rf])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(rf/dispatch [:login {:username @username :password @password]})}]])

ومكون التطبيق لدينا:

 (ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])

وأخيرًا ، يعد الوصول إلى الرمز المميز الخاص بنا في بعض المكونات البعيدة أمرًا بسيطًا مثل:

 (let [token @(rf/subscribe [:token])] ; ... )

ضع كل شيء معا:

كيف تعمل الحالة المحلية والحالة العالمية (Redux) في مثال تسجيل الدخول.

لا ضجة ولا فوضى.

فصل المكونات عن طريق الإعادة / إعادة الإطار يعني إدارة الحالة النظيفة

باستخدام Redux (عبر إعادة الإطار) ، نجحنا في فصل مكونات رؤيتنا عن فوضى معالجة الحالة. تمديد تجريد دولتنا هو الآن قطعة من الكعكة!

يعد Redux في ClojureScript بهذه السهولة حقًا - ليس لديك عذر لعدم تجربته.

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

الموضوعات ذات الصلة: إدارة الحالة في Angular باستخدام Firebase