تدفق البيانات البسيط في تطبيقات React باستخدام Flux و Backbone: برنامج تعليمي مع أمثلة

نشرت: 2022-03-11

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

قدم Facebook ، صانعو React ، بعض الإرشادات هناك في شكل Flux. Flux عبارة عن "بنية تطبيقات" (وليس إطار عمل) مبنية حول تدفق بيانات أحادي الاتجاه باستخدام React Views و Action Dispatcher و Stores. يحل نمط Flux بعض المشكلات الرئيسية من خلال تجسيد مبادئ مهمة للتحكم في الأحداث ، مما يجعل تطبيقات React أسهل بكثير في التفكير فيها وتطويرها وصيانتها.

هنا ، سأقدم أمثلة Flux الأساسية لتدفق التحكم ، وسأناقش ما ينقص المتاجر ، وكيفية استخدام نماذج ومجموعات العمود الفقري لملء الفجوة بطريقة "متوافقة مع Flux".

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

مقدمة في Facebook Flux

Backbone هي مكتبة صغيرة ممتازة ومدققة جيدًا تتضمن طرق عرض ونماذج ومجموعات وطرق. إنها مكتبة قياسية بحكم الواقع لتطبيقات الواجهة الأمامية المهيكلة ، وقد تم إقرانها بتطبيقات React منذ طرح الأخير في عام 2013. تضمنت معظم أمثلة React خارج Facebook.com حتى الآن إشارات إلى Backbone المستخدمة جنبًا إلى جنب.

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

سيوضح هذا البرنامج التعليمي Flux كيف يتعامل نمط Flux مع هذه المشكلات بسهولة وبساطة مثيرة للإعجاب.

لمحة عامة

شعار Flux هو "تدفق البيانات أحادي الاتجاه". فيما يلي رسم تخطيطي مفيد من مستندات Flux يوضح كيف يبدو هذا التدفق:

يستخدم Facebook Flux نموذج "تدفق بيانات أحادي الاتجاه" يختلف قليلاً عند إقرانه بـ React و Backbone.

الشيء المهم هو أن الأشياء تتدفق من React --> Dispatcher --> Stores --> React .

لنلقِ نظرة على ماهية كل من المكونات الرئيسية وكيفية اتصالها:

تقدم المستندات أيضًا هذا التحذير المهم:

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

إذن فلوكس له ثلاثة مكونات:

  1. المشاهدات ( React = require('react') )
  2. المرسل ( Dispatcher = require('flux').Dispatcher )
  3. المتاجر ( EventEmitter = require('events').EventEmitter )
    • (أو ، كما سنرى قريبًا ، Backbone = require('backbone') )

المناظر

لن أصف React هنا ، حيث كُتب الكثير عنها ، بخلاف القول إنني أفضلها إلى حد كبير على Angular. لا أشعر أبدًا بالارتباك عند كتابة كود React ، على عكس Angular ، لكن بالطبع ، ستختلف الآراء.

المرسل

إن Flux Dispatcher هو مكان واحد حيث يتم التعامل مع جميع الأحداث التي تعدل متاجرك. لاستخدامه ، يجب أن يقوم كل متجر register رد اتصال واحد للتعامل مع جميع الأحداث. بعد ذلك ، متى أردت تعديل متجر ، فأنت dispatch حدثًا.

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

 # 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 وما إلى ذلك ، حيث يصبح تتبع الإجابات على هذه الأسئلة الأساسية أمرًا صعبًا حقًا.

يسمح لك المرسل أيضًا بتشغيل عمليات الاسترجاعات بالتتابع بطريقة بسيطة ومتزامنة باستخدام 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

في الممارسة العملية ، لقد صُدمت لمعرفة مدى نظافة الكود الخاص بي عند استخدام المرسل لتعديل متاجري ، حتى بدون استخدام waitFor .

المتاجر

لذلك تتدفق البيانات إلى المخازن من خلال المرسل. فهمتك. ولكن كيف تتدفق البيانات من المتاجر إلى العروض (أي React)؟ كما هو مذكور في مستندات Flux:

تستمع طريقة العرض [] للأحداث التي يتم بثها بواسطة المتاجر التي تعتمد عليها.

حسنا عظيم. تمامًا مثلما سجلنا عمليات الاسترجاعات في متاجرنا ، نسجل عمليات الاسترجاعات من خلال طرق العرض الخاصة بنا (وهي مكونات React). نقول لـ render أن تعيد عرضها كلما حدث تغيير في المتجر تم تمريره من خلال 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...

أزداد! لا بد لي من كتابة كل ذلك بنفسي ، في كل مرة أريد متجر بسيط؟ ما الذي من المفترض أن أستخدمه في كل مرة لدي معلومة أريد عرضها؟ حتما توجد طريقة افضل!

القطعة المفقودة

تحتوي نماذج ومجموعات Backbone بالفعل على كل ما تفعله المتاجر القائمة على EventEmitter في Flux.

من خلال إخبارك باستخدام EventEmitter الخام ، يوصي Flux بإعادة إنشاء 50-75٪ من نماذج ومجموعات Backbone في كل مرة تنشئ فيها متجرًا. يشبه استخدام EventEmitter لمتجرك استخدام Node.js عارية لخادمك عندما توجد بالفعل إطارات دقيقة مبنية جيدًا مثل Express.js أو ما يعادلها بالفعل للاهتمام بجميع الأساسيات والنماذج المعيارية.

تمامًا مثل Express.js المبني على Node.js ، فإن نماذج ومجموعات Backbone مبنية على EventEmitter. ويحتوي على كل الأشياء التي تحتاجها دائمًا: العمود الفقري ينبعث من أحداث change ولديه طرق استعلام وحسابات ومحددات وكل شيء. بالإضافة إلى ذلك ، قام Jeremy Ashkenas من Backbone وجيشه المكون من 230 مساهمًا بعمل أفضل بكثير في كل هذه الأشياء مما قد أكون قادرًا على القيام به.

كمثال لهذا البرنامج التعليمي Backbone ، قمت بتحويل مثال MessageStore من أعلى إلى إصدار Backbone.

إنه رمز أقل من الناحية الموضوعية (لا حاجة لتكرار العمل) وهو أكثر وضوحًا وإيجازًا بشكل شخصي (على سبيل المثال ، this.add(message) بدلاً من _messages[message.id] = message ).

لذلك دعونا نستخدم العمود الفقري للمخازن!

نمط FluxBone: مخازن الجريان بواسطة العمود الفقري

هذا البرنامج التعليمي هو أساس نهج أطلقته بفخر على FluxBone ، بنية Flux باستخدام Backbone for Stores. هذا هو النمط الأساسي لبنية FluxBone:

  1. المتاجر عبارة عن نماذج أو مجموعات أساسية تم إنشاء مثيل لها ، والتي سجلت رد اتصال مع المرسل. عادة ، هذا يعني أنهم فرديون.
  2. عرض المكونات لا يقوم بتعديل المتاجر بشكل مباشر (على سبيل المثال ، no .set() ). بدلاً من ذلك ، ترسل المكونات الإجراءات إلى المرسل.
  3. عرض مخازن استعلام المكونات والربط بأحداثها لتشغيل التحديثات.

تم تصميم هذا البرنامج التعليمي Backbone لإلقاء نظرة على الطريقة التي يعمل بها Backbone و Flux معًا في تطبيقات React.

دعنا نستخدم أمثلة Backbone و Flux لإلقاء نظرة على كل جزء من ذلك بدوره:

1. المتاجر عبارة عن نماذج أو مجموعات أساسية تم إنشاؤها ، والتي سجلت رد اتصال مع المرسل.

 # 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. لا تقوم المكونات بتعديل المتاجر بشكل مباشر (على سبيل المثال ، no .set() ). بدلاً من ذلك ، ترسل المكونات الإجراءات إلى المرسل.

 # 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") )

المزامنة مع واجهة برمجة تطبيقات الويب

في مخطط Flux الأصلي ، تتفاعل مع Web API من خلال ActionCreators فقط ، الأمر الذي يتطلب استجابة من الخادم قبل إرسال الإجراءات إلى المرسل. هذا لم يجلس معي بشكل صحيح ؛ ألا يجب أن يكون المتجر أول من يعلم بالتغييرات قبل السيرفر؟

اخترت قلب هذا الجزء من الرسم التخطيطي حول: تتفاعل المتاجر مباشرة مع RESTful CRUD API من خلال sync() . هذا مريح بشكل رائع ، على الأقل إذا كنت تعمل باستخدام واجهة برمجة تطبيقات RESTful CRUD فعلية.

يتم الحفاظ على سلامة البيانات دون أي مشكلة. عندما تقوم .set() خاصية جديدة ، يؤدي حدث change إلى إعادة تصيير React ، وعرض البيانات الجديدة بشكل متفائل. عندما تحاول .save() على الخادم ، يتيح لك حدث request معرفة كيفية عرض أيقونة التحميل. عندما تمر الأمور ، يتيح لك حدث sync معرفة إزالة أيقونة التحميل ، أو يتيح لك حدث error معرفة تحويل الأشياء إلى اللون الأحمر. يمكنك أن ترى الإلهام هنا.

هناك أيضًا تحقق (وحدث invalid مطابق) للطبقة الأولى من الدفاع ، وطريقة .fetch() لسحب معلومات جديدة من الخادم.

بالنسبة للمهام الأقل قياسية ، قد يكون التفاعل عبر ActionCreators أكثر منطقية. أظن أن Facebook لا يفعل الكثير "مجرد CRUD" ، وفي هذه الحالة ليس من المستغرب أنهم لا يضعون المتاجر في المرتبة الأولى.

خاتمة

قامت فرق الهندسة في Facebook بعمل رائع لدفع شبكة الويب الأمامية إلى الأمام باستخدام React ، وإدخال Flux يعطي نظرة خاطفة على بنية أوسع تتوسع حقًا: ليس فقط من حيث التكنولوجيا ، ولكن الهندسة أيضًا. يمكن أن يؤدي الاستخدام الذكي والحذر لـ Backbone (وفقًا لمثال هذا البرنامج التعليمي) إلى سد الفجوات في Flux ، مما يجعل من السهل بشكل مذهل على أي شخص بدءًا من المتاجر المستقلة المكونة من شخص واحد وحتى الشركات الكبيرة إنشاء تطبيقات رائعة وصيانتها.

الموضوعات ذات الصلة: كيف تجعل مكونات React اختبار واجهة المستخدم أمرًا سهلاً