การควบคุมระดับบนสุดด้วยการจัดการสถานะ Redux: บทช่วยสอน ClojureScript
เผยแพร่แล้ว: 2022-03-11ยินดีต้อนรับกลับมาสำหรับภาคที่สองที่น่าตื่นเต้นของ Unearthing ClojureScript! ในโพสต์นี้ ฉันจะพูดถึงขั้นตอนใหญ่ถัดไปสำหรับการจริงจังกับ ClojureScript: การจัดการสถานะ—ในกรณีนี้คือการใช้ React
ด้วยซอฟต์แวร์ front-end การจัดการสถานะเป็นเรื่องใหญ่ มีสองวิธีในการจัดการสถานะใน React ที่พร้อมใช้งานทันที:
- รักษาสถานะไว้ที่ระดับบนสุด และส่งต่อ (หรือตัวจัดการสำหรับสถานะใดสถานะหนึ่ง) ลงไปที่องค์ประกอบย่อย
- โยนความบริสุทธิ์ออกไปนอกหน้าต่างและมีตัวแปรทั่วโลกหรือรูปแบบการพึ่งพา Lovecraftian บางรูปแบบ
โดยทั่วไปแล้วสิ่งเหล่านี้ไม่ได้ยอดเยี่ยม การรักษาสถานะไว้ที่ระดับบนสุดนั้นค่อนข้างง่าย แต่ก็มีค่าใช้จ่ายจำนวนมากในการส่งต่อสถานะของแอปพลิเคชันไปยังทุกองค์ประกอบที่ต้องการ
เมื่อเปรียบเทียบแล้ว การมีตัวแปรร่วม (หรือเวอร์ชันที่ไร้เดียงสาอื่นๆ) อาจส่งผลให้เกิดปัญหาการทำงานพร้อมกันที่ติดตามได้ยาก ส่งผลให้ส่วนประกอบไม่อัปเดตเมื่อคุณคาดหวัง หรือในทางกลับกัน
แล้วจะจัดการเรื่องนี้ได้อย่างไร? สำหรับผู้ที่คุ้นเคยกับ React คุณอาจลองใช้ Redux ซึ่งเป็นคอนเทนเนอร์สถานะสำหรับแอป JavaScript คุณอาจค้นพบสิ่งนี้ด้วยความตั้งใจของคุณเอง พยายามค้นหาระบบที่สามารถจัดการได้เพื่อรักษาสถานะ หรือคุณอาจเพิ่งพบเห็นขณะอ่านเกี่ยวกับ JavaScript และเครื่องมือเว็บอื่นๆ
ไม่ว่าผู้คนจะลงเอยที่ Redux อย่างไร จากประสบการณ์ของผม พวกเขามักจะจบลงด้วยความคิดสองประการ:
- “ฉันรู้สึกว่าฉันต้องใช้สิ่งนี้เพราะทุกคนบอกว่าฉันต้องใช้มัน”
- “ฉันไม่เข้าใจจริงๆ ว่าทำไมมันถึงดีกว่านี้”
โดยทั่วไป Redux ให้สิ่งที่เป็นนามธรรมที่ช่วยให้การจัดการสถานะเหมาะสมกับลักษณะ ปฏิกิริยา ของ React การนำความสมบูรณ์ทั้งหมดออกไปยังระบบอย่าง Redux คุณจะรักษา ความบริสุทธิ์ ของ React ไว้ได้ ดังนั้น คุณจะจบลงด้วยอาการปวดหัวน้อยลงและโดยทั่วไปแล้วจะเป็นเรื่องที่เข้าใจได้ง่ายกว่ามาก
สำหรับผู้ที่เพิ่งเริ่มใช้ Clojure
แม้ว่าสิ่งนี้อาจไม่ช่วยให้คุณเรียนรู้ ClojureScript ทั้งหมดตั้งแต่เริ่มต้น แต่อย่างน้อยที่นี่ฉันจะสรุปแนวคิดพื้นฐานบางประการใน Clojure[Script] อย่าลังเลที่จะข้ามส่วนเหล่านี้หากคุณเป็น Clojurian ที่ช่ำชองอยู่แล้ว!
ระลึกถึงหนึ่งในพื้นฐานของ Clojure ที่ใช้กับ ClojureScript ด้วย: โดยค่าเริ่มต้น ข้อมูลจะไม่เปลี่ยนรูป สิ่งนี้ยอดเยี่ยมสำหรับการพัฒนาและรับประกันว่าสิ่งที่คุณสร้าง ณ timestep N ยังคงเหมือนเดิมที่ timestep > N. ClojureScript ยังให้วิธีที่สะดวกแก่เราในการมีสถานะที่เปลี่ยนแปลงได้หากเราต้องการ โดยใช้แนวคิด atom
atom
ใน ClojureScript นั้นคล้ายกับ AtomicReference
ใน Java มาก: มีอ็อบเจ็กต์ใหม่ที่ล็อคเนื้อหาด้วยการรับประกันการทำงานพร้อมกัน เช่นเดียวกับใน Java คุณสามารถวางอะไรก็ได้ที่คุณชอบในวัตถุนี้ ตั้งแต่นั้นมา อะตอมนั้นจะเป็นการอ้างอิงอะตอมมิกไปยังสิ่งที่คุณต้องการ
เมื่อคุณมี atom
ของคุณแล้ว คุณสามารถตั้งค่าใหม่ให้เป็นอะตอมได้โดยใช้การ reset!
ฟังก์ชัน (หมายเหตุ !
ในฟังก์ชัน—ในภาษา Clojure มักใช้เพื่อแสดงว่าการดำเนินการนั้นเป็น stateful หรือไม่บริสุทธิ์)
โปรดทราบด้วยว่า Clojure ไม่เหมือนกับ Java ที่คุณใส่ลงใน 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 ให้ตรวจสอบโพสต์ก่อนหน้านี้) สิ่งนี้ทำงานเหมือนกับ ClojureScript atom
ยกเว้นว่าจะทริกเกอร์เหตุการณ์การเรนเดอร์ใน 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!"
. ซึ่งจะทำให้เกิดการวาดองค์ประกอบใหม่ ส่งผลให้ช่วงนั้นพูดว่า "สวัสดี ตรงนั้น!" แทนที่.
ดูเหมือนว่าจะง่ายพอสำหรับการกลายพันธุ์ระดับคอมโพเนนต์ในเครื่อง แต่ถ้าเรามีแอปพลิเคชันที่ซับซ้อนกว่าซึ่งมีนามธรรมหลายระดับล่ะ หรือถ้าเราต้องการแบ่งปันสถานะร่วมกันระหว่างหลายองค์ประกอบย่อยและองค์ประกอบย่อยของพวกเขา?
ตัวอย่างที่ซับซ้อนมากขึ้น
ลองสำรวจสิ่งนี้ด้วยตัวอย่าง ที่นี่เราจะใช้หน้าเข้าสู่ระบบแบบคร่าวๆ:
(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]])
เวิร์กโฟลว์ที่คาดหวังมีดังนี้:
- เรารอให้ผู้ใช้ป้อนชื่อผู้ใช้และรหัสผ่านแล้วกดส่ง
- สิ่งนี้จะทริกเกอร์ฟังก์ชัน
do-login-io
ของเราในองค์ประกอบหลัก - ฟังก์ชัน
do-login-io
ทำการดำเนินการ I/O บางอย่าง (เช่น การเข้าสู่ระบบบนเซิร์ฟเวอร์และการดึงโทเค็น)
หากการดำเนินการนี้ถูกบล็อก แสดงว่าเราประสบปัญหามากมาย เนื่องจากแอปพลิเคชันของเราถูกระงับ หากไม่ใช่ แสดงว่าเราต้องกังวล!
นอกจากนี้ ตอนนี้เราจำเป็นต้องจัดเตรียมโทเค็นนี้ให้กับส่วนประกอบย่อยทั้งหมดของเราที่ต้องการสอบถามไปยังเซิร์ฟเวอร์ของเรา การปรับโครงสร้างโค้ดทำได้ยากขึ้นมาก!
สุดท้ายนี้ คอมโพเนนต์ของเราไม่มี ปฏิกิริยาตอบสนอง อย่างหมดจดอีกต่อไปแล้ว — ตอนนี้ซับซ้อนในการจัดการสถานะของส่วนที่เหลือของแอปพลิเคชัน ทริกเกอร์ I/O และโดยทั่วไปแล้วจะทำให้เกิดความรำคาญเล็กน้อย
บทช่วยสอน ClojureScript: ป้อน Redux
Redux เป็นไม้กายสิทธิ์ที่ทำให้ความฝันของรัฐของคุณเป็นจริง นำไปใช้อย่างเหมาะสม ทำให้เกิดนามธรรมการแบ่งปันสถานะที่ปลอดภัย รวดเร็ว และใช้งานง่าย
ผลงานภายในของ Redux (และทฤษฎีเบื้องหลัง) ค่อนข้างจะนอกขอบเขตของบทความนี้ แต่ฉันจะดำดิ่งลงไปในตัวอย่างการทำงานกับ ClojureScript ซึ่งหวังว่าจะเป็นวิธีที่แสดงให้เห็นถึงความสามารถบางอย่าง!
ในบริบทของเรา Redux ถูกใช้งานโดยหนึ่งในไลบรารี ClojureScript ที่มีอยู่มากมาย อันนี้เรียกว่า re-frame มันมีเสื้อคลุม Clojure-ified รอบ Redux ซึ่ง (ในความคิดของฉัน) ทำให้มันน่ายินดีอย่างยิ่งที่จะใช้
พื้นฐาน
Redux ดึงสถานะแอปพลิเคชันของคุณออก ปล่อยให้ส่วนประกอบของคุณมีน้ำหนักเบา องค์ประกอบ Reduxified ต้องคำนึงถึง:
- หน้าตาเป็นยังไง
- ใช้ข้อมูลอะไร
- เกิดเหตุการณ์อะไรขึ้น
ส่วนที่เหลือจะได้รับการจัดการเบื้องหลัง
เพื่อเน้นประเด็นนี้ ให้ Reduxify หน้าเข้าสู่ระบบของเราด้านบน
ฐานข้อมูล
สิ่งแรกเลย: เราต้องตัดสินใจว่ารูปแบบแอปพลิเคชันของเราจะเป็นอย่างไร เราทำสิ่งนี้โดยกำหนด รูปร่าง ของข้อมูลของเรา ข้อมูลที่จะเข้าถึงได้ทั่วทั้งแอป

หลักการที่ดีคือถ้าจำเป็นต้องใช้ข้อมูลในองค์ประกอบ 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[Script] - สำหรับตัวอย่างนี้ เราติดตามเฉพาะโทเค็นส่วนกลางที่จะเป็นตัวแทนของผู้ใช้ของเราเมื่อพวกเขาเข้าสู่ระบบแล้ว โทเค็นนี้เป็นสตริงธรรมดา
- อย่างไรก็ตาม ก่อนที่ผู้ใช้จะเข้าสู่ระบบ เราจะไม่มีโทเค็น ซึ่งแสดงโดยคีย์เวิร์ด
:opt-un
ซึ่งย่อมาจาก “optional, unqualified” (ใน 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 ( มี ผลข้างเคียง) ของการออกไปและขอโทเค็นของเราผ่านการโต้ตอบกับลูกค้าบางส่วน
เราจะเริ่มต้นด้วยวิธีที่ง่าย Re-frame ยังมีฟังก์ชันสำหรับเหตุการณ์ประเภทนี้อย่างแน่นอน:
(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 เลย! นี่เป็นสิ่งสำคัญมากในการรักษาความศักดิ์สิทธิ์ของกระบวนการ Redux อันศักดิ์สิทธิ์ อย่าทำให้มันไม่บริสุทธิ์ เกรงว่าคุณต้องการให้ Redux โกรธคุณ
สุดท้าย เราต้องการกิจกรรมการเข้าสู่ระบบของเรา เราจะวางไว้ใต้ส่วนอื่นๆ:
(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
effect ซึ่งกำหนดไว้ด้านล่าง เอฟเฟกต์ที่ใช้ได้อีกอย่างหนึ่งคือ:dispatch
ซึ่งเรียกเหตุการณ์อื่นได้ง่าย
เมื่อเอฟเฟกต์ของเราถูกส่งไปแล้ว เอฟเฟกต์ :request-token
ของเราจะถูกเรียก ซึ่งดำเนินการเข้าสู่ระบบ I/O ที่ใช้เวลานาน เมื่อเสร็จสิ้น มันจะส่งผลลัพธ์กลับเข้าไปในลูปของเหตุการณ์อย่างมีความสุข ซึ่งจะทำให้รอบเสร็จสมบูรณ์!
บทช่วยสอน 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])] ; ... )
วางมันทั้งหมดเข้าด้วยกัน:
ไม่เอะอะไม่มี must.
การแยกส่วนประกอบด้วย Redux/Re-frame หมายถึงการจัดการสถานะที่สะอาด
การใช้ Redux (ผ่านเฟรมใหม่) เราแยกองค์ประกอบมุมมองของเราออกจากความยุ่งเหยิงของการจัดการสถานะได้สำเร็จ การขยายความเป็นนามธรรมของเราตอนนี้เป็นเรื่องง่าย!
Redux ใน ClojureScript นั้น ง่ายมาก คุณไม่มีข้อแก้ตัวที่จะไม่ลองใช้เลย
หากคุณพร้อมที่จะแคร็ก ฉันขอแนะนำให้ตรวจสอบเอกสารการเฟรมใหม่ที่ยอดเยี่ยมและตัวอย่างการทำงานง่ายๆ ของเรา ฉันหวังว่าจะได้อ่านความคิดเห็นของคุณเกี่ยวกับบทช่วยสอน ClojureScript ด้านล่างนี้ ขอให้โชคดี!