การไหลของข้อมูลอย่างง่ายในแอป React โดยใช้ Flux และ Backbone: บทช่วยสอนพร้อมตัวอย่าง

เผยแพร่แล้ว: 2022-03-11

React.js เป็นห้องสมุดที่ยอดเยี่ยม บางครั้งดูเหมือนว่าจะเป็นสิ่งที่ดีที่สุดตั้งแต่หั่น Python อย่างไรก็ตาม React เป็นเพียงส่วนหนึ่งของแอปพลิเคชันส่วนหน้าเท่านั้น ไม่มีอะไรให้มากในการจัดการข้อมูลและสถานะ

Facebook ผู้ผลิต React ได้เสนอแนวทางในรูปแบบของ Flux Flux คือ "สถาปัตยกรรมแอปพลิเคชัน" (ไม่ใช่เฟรมเวิร์ก) ที่สร้างขึ้นจากโฟลว์ข้อมูลทางเดียวโดยใช้ React Views, Action Dispatcher และ Stores รูปแบบ Flux แก้ปัญหาหลักๆ บางอย่างโดยรวบรวมหลักการสำคัญของการควบคุมเหตุการณ์ ซึ่งทำให้แอปพลิเคชัน React สามารถให้เหตุผล พัฒนา และบำรุงรักษาได้ง่ายขึ้น

ในที่นี้ ฉันจะแนะนำตัวอย่าง Flux พื้นฐานของโฟลว์การควบคุม อภิปรายสิ่งที่ขาดหายไปสำหรับ Stores และวิธีใช้ Backbone Models and Collections เพื่อเติมเต็มช่องว่างด้วยวิธี "Flux-compliant"

(หมายเหตุ: ฉันใช้ CoffeeScript ในตัวอย่างของฉันเพื่อความสะดวกและกระชับ นักพัฒนาที่ไม่ใช่ CoffeeScript ควรจะสามารถปฏิบัติตาม และสามารถปฏิบัติต่อตัวอย่างต่างๆ เสมือนเป็นรหัสเทียม)

ข้อมูลเบื้องต้นเกี่ยวกับ Flux . ของ Facebook

Backbone เป็นห้องสมุดขนาดเล็กที่ยอดเยี่ยมและได้รับการตรวจสอบอย่างดี ซึ่งรวมถึง Views, Models, Collections และ Routes เป็นไลบรารีมาตรฐาน ตามพฤตินัย สำหรับแอปพลิเคชันส่วนหน้าที่มีโครงสร้าง และได้รับการจับคู่กับแอป React ตั้งแต่เปิดตัวในปี 2013 ตัวอย่างส่วนใหญ่ของ React นอก Facebook.com จนถึงขณะนี้ได้กล่าวถึง Backbone ที่ใช้ควบคู่กันไป

น่าเสียดายที่การพึ่งพา Backbone เพียงอย่างเดียวเพื่อจัดการโฟลว์แอปพลิเคชันทั้งหมดที่อยู่นอก React's Views ทำให้เกิดความยุ่งยากที่โชคร้าย เมื่อฉันเริ่มทำงานกับโค้ดแอปพลิเคชัน React-Backbone เป็นครั้งแรก "กลุ่มเหตุการณ์ที่ซับซ้อน" ที่ฉันได้อ่านมานั้นใช้เวลาไม่นานในการดูแลส่วนหัวที่เหมือนไฮดรา การส่งเหตุการณ์จาก UI ไปยังโมเดล จากนั้นจากโมเดลหนึ่งไปยังอีกโมเดลหนึ่งแล้วย้อนกลับอีกครั้ง ทำให้ยากที่จะติดตามว่าใครกำลังเปลี่ยนแปลงใคร ในลำดับใด และเพราะเหตุใด

บทช่วยสอน Flux นี้จะสาธิตวิธีที่รูปแบบ Flux จัดการกับปัญหาเหล่านี้ได้อย่างง่ายดายและเรียบง่ายอย่างน่าประทับใจ

ภาพรวม

สโลแกนของ Flux คือ "การไหลของข้อมูลแบบทิศทางเดียว" นี่คือไดอะแกรมที่มีประโยชน์จากเอกสาร Flux ที่แสดงให้เห็นว่าโฟลว์นั้นเป็นอย่างไร:

Facebook Flux ใช้โมเดล "การไหลของข้อมูลแบบทิศทางเดียว" ที่แตกต่างกันเล็กน้อยเมื่อจับคู่กับ React และ Backbone

บิตที่สำคัญคือสิ่งต่าง ๆ ไหลจาก React --> Dispatcher --> Stores --> React

มาดูกันว่าองค์ประกอบหลักแต่ละอย่างคืออะไรและเชื่อมต่ออย่างไร:

เอกสารยังมีข้อแม้ที่สำคัญนี้:

ฟลักซ์มีรูปแบบมากกว่าเฟรมเวิร์ก และไม่มีการอ้างอิงแบบตายตัว อย่างไรก็ตาม เรามักใช้ EventEmitter เป็นพื้นฐานสำหรับ Stores และ React สำหรับ Views ของเรา Flux ชิ้นเดียวที่หาไม่ได้จากที่อื่นคือ Dispatcher โมดูลนี้มีให้ที่นี่เพื่อทำให้กล่องเครื่องมือ Flux ของคุณสมบูรณ์

ดังนั้น Flux จึงมีสามองค์ประกอบ:

  1. จำนวนการดู ( React = require('react') )
  2. Dispatcher ( Dispatcher = require('flux').Dispatcher )
  3. ร้านค้า ( EventEmitter = require('events').EventEmitter )
    • (หรืออย่างที่เราจะได้เห็นกันในไม่ช้า Backbone = require('backbone') )

มุมมอง

ฉันจะไม่อธิบาย React ที่นี่ เนื่องจากมีการเขียนเกี่ยวกับมันมากมาย นอกจากจะบอกว่าฉันชอบ Angular มากกว่า ฉันแทบไม่เคยรู้สึก สับสน เมื่อเขียนโค้ด React ซึ่งต่างจาก Angular แต่แน่นอนว่าความคิดเห็นจะแตกต่างกันไป

The Dispatcher

Flux Dispatcher เป็นที่เดียวที่จัดการกิจกรรมทั้งหมดที่ปรับเปลี่ยนร้านค้าของคุณ ในการใช้งาน คุณต้องให้ร้านค้าแต่ละแห่ง register การโทรกลับครั้งเดียวเพื่อจัดการเหตุการณ์ทั้งหมด จากนั้น เมื่อใดก็ตามที่คุณต้องการแก้ไขร้านค้า คุณจะต้อง dispatch กิจกรรม

เช่นเดียวกับ React Dispatcher ถือว่าฉันเป็นความคิดที่ดี นำไปปฏิบัติได้ดี ตัวอย่างเช่น แอปที่อนุญาตให้ผู้ใช้เพิ่มรายการลงในรายการสิ่งที่ต้องทำอาจรวมถึงสิ่งต่อไปนี้:

 # in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes!. module.exports = TodoDispatcher
 # in TodoStore.coffee TodoDispatcher = require("./TodoDispatcher") TodoStore = {items: []} TodoStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" TodoStore.items.push payload.item when "delete-last-item" TodoStore.items.pop() TodoStore.dispatchToken = TodoDispatcher.registerCallback(TodoStore.dispatchCallback) module.exports = TodoStore
 # in ItemAddComponent.coffee TodoDispatcher = require("./TodoDispatcher") ItemAddComponent = React.createClass handleAddItem: -> # note: you're NOT just pushing directly to the store! # (the restriction of moving through the dispatcher # makes everything much more modular and maintainable) TodoDispatcher.dispatch actionType: "add-item" item: "hello world" render: -> React.DOM.button { onClick: @handleAddItem }, "Add an Item!"

ทำให้ง่ายต่อการตอบคำถามสองข้อ:

  1. ถาม: เหตุการณ์ทั้งหมดที่แก้ไข MyStore คืออะไร
    • ตอบ: เพียงตรวจสอบเคสในคำสั่ง switch ใน MyStore.dispatchCallback
  2. ถาม: แหล่งที่มาของเหตุการณ์นั้นทั้งหมดมาจากอะไร
    • ตอบ: เพียงค้นหา actionType นั้น

ง่ายกว่ามาก ตัวอย่างเช่น การค้นหา MyModel.set และ MyModel.save และ MyCollection.add เป็นต้น ซึ่งการติดตามคำตอบของคำถามพื้นฐานเหล่านี้เป็นเรื่องยากมากอย่างรวดเร็ว

Dispatcher ยังอนุญาตให้คุณเรียกใช้การเรียกกลับตามลำดับในลักษณะที่เรียบง่ายและซิงโครนัสโดยใช้ waitFor ตัวอย่างเช่น:

 # in MessageStore.coffee MyDispatcher = require("./MyDispatcher") TodoStore = require("./TodoStore") MessageStore = {items: []} MessageStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" # synchronous event flow! MyDispatcher.waitFor [TodoStore.dispatchToken] MessageStore.items.push "You added an item! It was: " + payload.item module.exports = MessageStore

ในทางปฏิบัติ ฉันตกใจมากที่เห็นว่าโค้ดของฉันสะอาดขึ้นเพียงใดเมื่อใช้ Dispatcher เพื่อแก้ไขร้านค้าของฉัน แม้จะไม่ได้ใช้ waitFor

ร้านค้า

ดังนั้นข้อมูลจึงไหล เข้าสู่ Stores ผ่าน Dispatcher เข้าใจแล้ว. แต่การไหลของข้อมูลจาก Stores ไปยัง Views (เช่น React) เป็นอย่างไร? ตามที่ระบุไว้ในเอกสาร Flux:

[The] รับฟังเหตุการณ์ที่ออกอากาศโดยร้านค้าที่ขึ้นอยู่กับ

โอเค ดีมาก เช่นเดียวกับที่เราลงทะเบียนการโทรกลับกับร้านค้าของเรา เราลงทะเบียนการโทรกลับด้วย Views ของเรา (ซึ่งเป็นส่วนประกอบ React) เราบอกให้ React render ใหม่ทุกครั้งที่มีการเปลี่ยนแปลงเกิดขึ้นใน Store ซึ่งถูกส่งผ่านผ่าน props

ตัวอย่างเช่น:

 # in TodoListComponent.coffee React = require("react") TodoListComponent = React.createClass componentDidMount: -> @props.TodoStore.addEventListener "change", => @forceUpdate() , @ componentWillUnmount: -> # remove the callback render: -> # show the items in a list. React.DOM.ul {}, @props.TodoStore.items.map (item) -> React.DOM.li {}, item

สุดยอด!

แล้วเราจะปล่อยเหตุการณ์ "change" นั้นออกไปได้อย่างไร? Flux แนะนำให้ใช้ EventEmitter จากตัวอย่างอย่างเป็นทางการ:

 var MessageStore = merge(EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, /** * @param {function} callback */ addChangeListener: function(callback) { this.on(CHANGE_EVENT, callback); }, get: function(id) { return _messages[id]; }, getAll: function() { return _messages; }, // etc...

ทั้งหมด! ฉันต้องเขียนเองทั้งหมดทุกครั้งที่ต้องการ Store ง่ายๆ? ฉันควรใช้ทุกครั้งที่มีข้อมูลที่ต้องการแสดง ต้องมีวิธีที่ดีกว่านี้!

ชิ้นส่วนที่หายไป

โมเดลและคอลเล็กชันของ Backbone มีทุกอย่างที่ร้านค้าตาม EventEmitter ของ Flux ดูเหมือนจะทำอยู่แล้ว

ด้วยการบอกให้คุณใช้ EventEmitter ดิบ Flux แนะนำให้คุณสร้างแบบจำลองและคอลเลกชั่นของ Backbone ประมาณ 50-75% ทุกครั้งที่คุณสร้าง Store การใช้ EventEmitter สำหรับร้านค้าของคุณนั้นเหมือนกับการใช้ Bare Node.js สำหรับเซิร์ฟเวอร์ของคุณเมื่อมีไมโครเฟรมเวิร์กที่สร้างขึ้นมาอย่างดี เช่น Express.js หรือเทียบเท่าอยู่แล้วเพื่อดูแลพื้นฐานและต้นแบบทั้งหมด

เช่นเดียวกับ Express.js ที่สร้างบน Node.js โมเดลและคอลเลกชั่นของ Backbone นั้นสร้างขึ้นบน EventEmitter และมีทุกสิ่งที่คุณต้องการอยู่เสมอ: Backbone ปล่อยเหตุการณ์ change และมีวิธีการค้นหา getters และ setters และทุกอย่าง นอกจากนี้ Jeremy Ashkenas แห่ง Backbone และกองทัพของเขาที่มีผู้ร่วมสนับสนุน 230 คนยังทำงานได้ดีขึ้นมากในสิ่งเหล่านั้นทั้งหมดมากกว่าที่ฉันจะทำได้

เป็นตัวอย่างสำหรับบทช่วยสอน Backbone นี้ ฉันแปลงตัวอย่าง MessageStore จากด้านบนเป็นเวอร์ชัน Backbone

เป็นรหัสที่เป็นกลางน้อยกว่า (ไม่จำเป็นต้องทำซ้ำงาน) และมีความชัดเจนและรัดกุมมากขึ้นตามอัตวิสัย (เช่น this.add(message) แทนที่จะเป็น _messages[message.id] = message )

มาใช้ Backbone for Stores กันเถอะ!

รูปแบบ FluxBone: Flux Stores โดย Backbone

บทช่วยสอนนี้เป็นพื้นฐานของวิธีการที่ฉันได้ขนานนามว่า FluxBone ซึ่งเป็นสถาปัตยกรรม Flux ที่ใช้ Backbone for Stores อย่างภาคภูมิใจ นี่คือรูปแบบพื้นฐานของสถาปัตยกรรม FluxBone:

  1. ร้านค้าคือโมเดล Backbone หรือคอลเลกชั่นที่สร้างอินสแตนซ์ ซึ่งได้ลงทะเบียนการโทรกลับกับ Dispatcher แล้ว โดยทั่วไปแล้ว นี่หมายความว่าพวกมันเป็นซิงเกิลตัน
  2. ดูส่วนประกอบ ไม่เคย แก้ไข Stores โดยตรง (เช่น ไม่มี .set() ) คอมโพเนนต์จะส่งการดำเนินการไปยัง Dispatcher แทน
  3. ดูการสืบค้นส่วนประกอบ จัดเก็บและผูกกับเหตุการณ์เพื่อทริกเกอร์การอัปเดต

บทช่วยสอน Backbone นี้ออกแบบมาเพื่อดูวิธีที่ Backbone และ Flux ทำงานร่วมกันในแอปพลิเคชัน React

ลองใช้ตัวอย่าง Backbone และ Flux เพื่อดูแต่ละส่วนตามลำดับ:

1. ร้านค้าคือโมเดล Backbone หรือคอลเลกชั่นที่สร้างอินสแตนซ์ ซึ่งได้ลงทะเบียนการโทรกลับกับ Dispatcher แล้ว

 # in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes! module.exports = TodoDispatcher
 # in stores/TodoStore.coffee Backbone = require("backbone") TodoDispatcher = require("../dispatcher") TodoItem = Backbone.Model.extend({}) TodoCollection = Backbone.Collection.extend model: TodoItem url: "/todo" # we register a callback with the Dispatcher on init. initialize: -> @dispatchToken = TodoDispatcher.register(@dispatchCallback) dispatchCallback: (payload) => switch payload.actionType # remove the Model instance from the Store. when "todo-delete" @remove payload.todo when "todo-add" @add payload.todo when "todo-update" # do stuff... @add payload.todo, merge: true # ... etc # the Store is an instantiated Collection; a singleton. TodoStore = new TodoCollection() module.exports = TodoStore

2. ส่วนประกอบ จะไม่ แก้ไข Stores โดยตรง (เช่น ไม่มี .set() ) คอมโพเนนต์จะส่งการดำเนินการไปยัง Dispatcher แทน

 # components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the Dispatcher TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... (see below) ... module.exports = TodoListComponent

3. แบบสอบถามคอมโพเนนต์ จัดเก็บและผูกกับเหตุการณ์เพื่อทริกเกอร์การอัปเดต

 # components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the dispatcher. #flux TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... componentDidMount: -> # the Component binds to the Store's events @props.TodoStore.on "add remove reset", => @forceUpdate() , @ componentWillUnmount: -> # turn off all events and callbacks that have this context @props.TodoStore.off null, null, this render: -> React.DOM.ul {}, @props.TodoStore.items.map (todoItem) -> # TODO: TodoItemComponent, which would bind to # `this.props.todoItem.on('change')` TodoItemComponent { todoItem: todoItem } module.exports = TodoListComponent

ฉันได้ใช้แนวทาง Flux และ Backbone นี้กับโปรเจ็กต์ของฉันเอง และเมื่อฉันออกแบบแอปพลิเคชัน React ของฉันใหม่เพื่อใช้รูปแบบนี้ บิตที่น่าเกลียดเกือบทั้งหมดก็หายไป เป็นเรื่องอัศจรรย์เล็กน้อย ทีละชิ้น โค้ดที่ทำให้ฉันขบเขี้ยวเคี้ยวฟันเพื่อมองหาวิธีที่ดีกว่า ถูกแทนที่ด้วยความลื่นไหลที่สมเหตุสมผล และความราบรื่นที่ Backbone ดูเหมือนจะรวมเข้ากับรูปแบบนี้เป็นสิ่งที่น่าทึ่ง: ฉันไม่รู้สึกว่าฉันกำลังต่อสู้กับ Backbone, Flux หรือ React เพื่อที่จะรวมเข้าด้วยกันในแอปพลิเคชันเดียว

ตัวอย่าง Mixin

การเขียน this.on(...) และ this.off(...) ทุกครั้งที่คุณเพิ่ม FluxBone Store ไปยังส่วนประกอบอาจเก่าไปหน่อย

ต่อไปนี้คือตัวอย่าง React Mixin ที่แม้จะไร้เดียงสาอย่างยิ่ง แต่ก็จะทำให้การวนซ้ำอย่างรวดเร็วง่ายยิ่งขึ้นไปอีกอย่างแน่นอน:

 # in FluxBoneMixin.coffee module.exports = (propName) -> componentDidMount: -> @props[propName].on "all", => @forceUpdate() , @ componentWillUnmount: -> @props[propName].off "all", => @forceUpdate() , @
 # in HelloComponent.coffee React = require("react") UserStore = require("./stores/UserStore") TodoStore = require("./stores/TodoStore") FluxBoneMixin = require("./FluxBoneMixin") MyComponent = React.createClass mixins: [ FluxBoneMixin("UserStore"), FluxBoneMixin("TodoStore"), ] render: -> React.DOM.div {}, "Hello, #{ @props.UserStore.get('name') }, you have #{ @props.TodoStore.length } things to do." React.renderComponent( MyComponent { UserStore: UserStore TodoStore: TodoStore } , document.body.querySelector(".main") )

การซิงค์กับ Web API

ในไดอะแกรม Flux ดั้งเดิม คุณจะโต้ตอบกับ Web API ผ่าน ActionCreators เท่านั้น ซึ่งต้องการการตอบสนองจากเซิร์ฟเวอร์ก่อนที่จะส่งการดำเนินการไปยัง Dispatcher ที่ไม่เคยนั่งกับฉัน Store ไม่ควรเป็นคนแรกที่รู้เกี่ยวกับการเปลี่ยนแปลงก่อนเซิร์ฟเวอร์ใช่หรือไม่

ฉันเลือกที่จะพลิกส่วนนั้นของไดอะแกรมไปรอบๆ: Stores โต้ตอบโดยตรงกับ RESTful CRUD API ผ่านการ sync() ของ Backbone วิธีนี้สะดวกอย่างยิ่ง อย่างน้อยหากคุณกำลังทำงานกับ RESTful CRUD API จริง

ความสมบูรณ์ของข้อมูลจะคงอยู่โดยไม่มีปัญหา เมื่อคุณ .set() คุณสมบัติใหม่ เหตุการณ์ change จะทริกเกอร์ React re-render โดยแสดงข้อมูลใหม่ในแง่ดี เมื่อคุณพยายาม .save() ไปยังเซิร์ฟเวอร์ เหตุการณ์ request จะแจ้งให้คุณทราบว่าจะแสดงไอคอนการโหลด เมื่อสิ่งต่างๆ ผ่านไป เหตุการณ์การ sync จะแจ้งให้คุณทราบเพื่อลบไอคอนการโหลด หรือเหตุการณ์ error ช่วยให้คุณทราบว่าจะเปลี่ยนเป็นสีแดง คุณสามารถดูแรงบันดาลใจได้ที่นี่

นอกจากนี้ยังมีการตรวจสอบ (และเหตุการณ์ที่ invalid ที่เกี่ยวข้อง) สำหรับการป้องกันชั้นแรก และ .fetch() เพื่อดึงข้อมูลใหม่จากเซิร์ฟเวอร์

สำหรับงานที่มีมาตรฐานน้อยกว่า การโต้ตอบผ่าน ActionCreators อาจเหมาะสมกว่า ฉันสงสัยว่า Facebook ไม่ได้ทำอะไร "แค่ CRUD" มากนัก ซึ่งในกรณีนี้ก็ไม่น่าแปลกใจที่พวกเขาจะไม่ใส่ Stores เป็นอันดับแรก

บทสรุป

ทีมวิศวกรรมของ Facebook ได้ทำงานอย่างน่าทึ่งเพื่อผลักดันเว็บส่วนหน้าไปข้างหน้าด้วย React และการเปิดตัว Flux นั้นช่วยให้มองเห็นสถาปัตยกรรมที่กว้างขึ้นซึ่งปรับขนาดได้อย่างแท้จริง: ไม่ใช่แค่ในแง่ของเทคโนโลยี แต่ในด้านวิศวกรรมด้วย การใช้ Backbone อย่างชาญฉลาดและระมัดระวัง (ตามตัวอย่างของบทช่วยสอนนี้) สามารถเติมเต็มช่องว่างใน Flux ได้ ทำให้ง่ายอย่างน่าอัศจรรย์สำหรับทุกคนตั้งแต่ร้านอินดี้ไปจนถึงบริษัทขนาดใหญ่ ในการสร้างและรักษาแอปพลิเคชันที่น่าประทับใจ

ที่เกี่ยวข้อง: ส่วนประกอบ React ทำให้การทดสอบ UI เป็นเรื่องง่าย