Aliran Data Sederhana di Aplikasi React Menggunakan Flux dan Backbone: Tutorial dengan Contoh
Diterbitkan: 2022-03-11React.js adalah perpustakaan yang fantastis. Terkadang sepertinya hal terbaik sejak mengiris Python. Namun, React hanyalah satu bagian dari tumpukan aplikasi front-end. Tidak banyak yang bisa ditawarkan dalam hal mengelola data dan status.
Facebook, pembuat React, telah menawarkan beberapa panduan di sana dalam bentuk Flux. Flux adalah "Arsitektur Aplikasi" (bukan kerangka kerja) yang dibangun di sekitar aliran data satu arah menggunakan React Views, Action Dispatcher, dan Stores. Pola Flux memecahkan beberapa masalah utama dengan mewujudkan prinsip-prinsip penting dari event control, yang membuat aplikasi React lebih mudah untuk dipikirkan, dikembangkan, dan dipelihara.
Di sini, saya akan memperkenalkan contoh Flux dasar dari aliran kontrol, membahas apa yang hilang untuk Toko, dan cara menggunakan Model dan Koleksi Backbone untuk mengisi celah dengan cara yang "sesuai dengan Fluks".
(Catatan: Saya menggunakan CoffeeScript dalam contoh saya untuk kenyamanan dan singkatnya. Pengembang non-CoffeeScript harus dapat mengikuti, dan dapat memperlakukan contoh sebagai kodesemu.)
Pengantar Flux Facebook
Backbone adalah perpustakaan kecil yang sangat baik dan diperiksa dengan baik yang mencakup Tampilan, Model, Koleksi, dan Rute. Ini adalah pustaka standar de facto untuk aplikasi front-end terstruktur, dan telah dipasangkan dengan aplikasi React sejak yang terakhir diperkenalkan pada 2013. Sebagian besar contoh React di luar Facebook.com sejauh ini telah menyertakan penyebutan Backbone yang digunakan bersama-sama.
Sayangnya, bersandar pada Backbone saja untuk menangani seluruh aliran aplikasi di luar React's Views menghadirkan komplikasi yang tidak menguntungkan. Ketika saya pertama kali mulai mengerjakan kode aplikasi React-Backbone, "rantai peristiwa kompleks" yang telah saya baca tidak membutuhkan waktu lama untuk mengangkat kepala mereka yang seperti hydra. Mengirim peristiwa dari UI ke Model, lalu dari satu Model ke Model lain, lalu kembali lagi, mempersulit pelacakan siapa yang mengubah siapa, dalam urutan apa, dan mengapa.
Tutorial Flux ini akan menunjukkan bagaimana pola Flux menangani masalah ini dengan kemudahan dan kesederhanaan yang mengesankan.
Gambaran
Slogan Flux adalah "aliran data searah". Berikut adalah diagram praktis dari dokumen Flux yang menunjukkan seperti apa aliran itu:
Yang penting adalah barang mengalir dari React --> Dispatcher --> Stores --> React
.
Mari kita lihat apa masing-masing komponen utama dan bagaimana mereka terhubung:
Dokumen juga menawarkan peringatan penting ini:
Flux lebih merupakan pola daripada kerangka kerja, dan tidak memiliki ketergantungan yang keras. Namun, kami sering menggunakan EventEmitter sebagai dasar untuk Menyimpan dan Bereaksi untuk Tampilan kami. Satu-satunya bagian dari Flux yang tidak tersedia di tempat lain adalah Dispatcher. Modul ini tersedia di sini untuk melengkapi kotak peralatan Flux Anda.
Jadi Flux memiliki tiga komponen:
- Tampilan (
React = require('react')
) - Dispatcher (
Dispatcher = require('flux').Dispatcher
) - Toko (
EventEmitter = require('events').EventEmitter
)- (atau, seperti yang akan segera kita lihat,
Backbone = require('backbone')
)
- (atau, seperti yang akan segera kita lihat,
Pemandangan
Saya tidak akan menjelaskan React di sini, karena begitu banyak yang telah ditulis tentangnya, selain untuk mengatakan bahwa saya lebih memilihnya daripada Angular. Saya hampir tidak pernah merasa bingung ketika menulis kode React, tidak seperti Angular, tetapi tentu saja pendapat akan berbeda-beda.
pengirim
Flux Dispatcher adalah satu tempat di mana semua peristiwa yang mengubah Toko Anda ditangani. Untuk menggunakannya, Anda meminta setiap Store register
satu panggilan balik untuk menangani semua acara. Kemudian, kapan pun Anda ingin mengubah Toko, Anda dispatch
acara.
Seperti React, Dispatcher menurut saya ide yang bagus, diimplementasikan dengan baik. Sebagai contoh, aplikasi yang memungkinkan pengguna untuk menambahkan item ke daftar tugas mungkin menyertakan hal berikut:
# 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!"
Ini membuatnya sangat mudah untuk menjawab dua pertanyaan:
- Q: Apa saja event yang mengubah
MyStore
?- A: Cukup periksa kasus di pernyataan
switch
diMyStore.dispatchCallback
.
- A: Cukup periksa kasus di pernyataan
- T: Apa saja kemungkinan sumber dari peristiwa itu?
- J: Cukup cari
actionType
itu.
- J: Cukup cari
Ini jauh lebih mudah daripada, misalnya, mencari MyModel.set
dan MyModel.save
dan MyCollection.add
dll, di mana melacak jawaban atas pertanyaan dasar ini menjadi sangat sulit dengan sangat cepat.
Dispatcher juga memungkinkan Anda menjalankan callback secara berurutan dengan cara yang sederhana dan sinkron, menggunakan waitFor
. Sebagai contoh:
# 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
Dalam praktiknya, saya terkejut melihat betapa lebih bersihnya kode saya saat menggunakan Dispatcher untuk memodifikasi Toko saya, bahkan tanpa menggunakan waitFor
.
Toko-toko
Jadi data mengalir ke Toko melalui Dispatcher. Mengerti. Tapi bagaimana data mengalir dari Stores ke Views (yaitu, React)? Seperti yang dinyatakan dalam dokumen Flux:
Tampilan [The] mendengarkan acara yang disiarkan oleh toko yang bergantung padanya.
Oke, bagus. Sama seperti kami mendaftarkan panggilan balik dengan Toko kami, kami mendaftarkan panggilan balik dengan Tampilan kami (yang merupakan Komponen Bereaksi). Kami memberi tahu React untuk render
setiap kali perubahan terjadi di Store yang diteruskan melalui props
-nya.
Sebagai contoh:
# 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
Luar biasa!
Jadi bagaimana kita memancarkan peristiwa "change"
itu? Nah, Flux merekomendasikan menggunakan EventEmitter
. Dari contoh resmi:
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...
Bruto! Saya harus menulis semua itu sendiri, setiap kali saya ingin Toko sederhana? Mana yang harus saya gunakan setiap kali saya memiliki informasi yang ingin saya tampilkan? Harus ada cara yang lebih baik!
Bagian yang Hilang
Model dan Koleksi Backbone sudah memiliki semua yang tampaknya dilakukan oleh Toko berbasis EventEmitter milik Flux.

Dengan memberi tahu Anda untuk menggunakan EventEmitter mentah, Flux merekomendasikan agar Anda membuat ulang mungkin 50-75% Model & Koleksi Backbone setiap kali Anda membuat Toko. Menggunakan EventEmitter untuk toko Anda seperti menggunakan Node.js kosong untuk server Anda ketika kerangka kerja mikro yang dibangun dengan baik seperti Express.js atau yang setara sudah ada untuk menangani semua dasar dan boilerplate.
Sama seperti Express.js dibangun di atas Node.js, Model dan Koleksi Backbone dibangun di EventEmitter. Dan ia memiliki semua hal yang selalu Anda butuhkan: Backbone memancarkan peristiwa change
dan memiliki metode kueri, pengambil dan penyetel, dan semuanya. Plus, Jeremy Ashkenas dari Backbone dan pasukannya yang terdiri dari 230 kontributor melakukan pekerjaan yang jauh lebih baik dalam semua hal itu daripada yang mungkin bisa saya lakukan.
Sebagai contoh untuk tutorial Backbone ini, saya mengonversi contoh MessageStore dari atas ke versi Backbone.
Ini secara obyektif lebih sedikit kode (tidak perlu menduplikasi pekerjaan) dan secara subyektif lebih jelas dan ringkas (misalnya, this.add(message)
daripada _messages[message.id] = message
).
Jadi mari kita gunakan Backbone untuk Toko!
Pola FluxBone: Penyimpanan Flux oleh Backbone
Tutorial ini adalah dasar dari pendekatan yang dengan bangga saya beri nama FluxBone , arsitektur Flux menggunakan Backbone for Stores. Berikut adalah pola dasar arsitektur FluxBone:
- Toko adalah Model atau Koleksi Backbone yang dipakai, yang telah mendaftarkan panggilan balik dengan Dispatcher. Biasanya, ini berarti mereka lajang.
- Lihat komponen tidak pernah secara langsung memodifikasi Toko (misalnya, tidak ada
.set()
). Sebagai gantinya, komponen mengirimkan Tindakan ke Dispatcher. - Lihat kueri komponen Simpan dan ikat ke peristiwanya untuk memicu pembaruan.
Mari kita gunakan contoh Backbone dan Flux untuk melihat masing-masing bagian secara bergantian:
1. Toko adalah Model atau Koleksi Backbone yang dipakai, yang telah mendaftarkan panggilan balik dengan 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. Komponen tidak pernah secara langsung memodifikasi Toko (misalnya, tidak ada .set()
). Sebagai gantinya, komponen mengirimkan Tindakan ke 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. Query Components Menyimpan dan mengikat event mereka untuk memicu pembaruan.
# 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
Saya telah menerapkan pendekatan Flux dan Backbone ini ke proyek saya sendiri, dan setelah saya merancang ulang aplikasi React saya untuk menggunakan pola ini, hampir semua bit jelek menghilang. Itu sedikit ajaib: satu per satu, potongan kode yang membuatku menggertakkan gigi mencari cara yang lebih baik digantikan oleh aliran yang masuk akal. Dan kehalusan yang tampaknya diintegrasikan dengan Backbone dalam pola ini luar biasa: Saya tidak merasa seperti sedang melawan Backbone, Flux, atau React untuk menyatukan mereka dalam satu aplikasi.
Contoh Campuran
Menulis kode this.on(...)
dan this.off(...)
setiap kali Anda menambahkan Toko FluxBone ke komponen bisa menjadi agak lama.
Berikut adalah contoh React Mixin yang, meskipun sangat naif, pasti akan membuat iterasi dengan cepat menjadi lebih mudah:
# 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") )
Menyinkronkan dengan Web API
Dalam diagram Flux asli, Anda berinteraksi dengan Web API hanya melalui ActionCreators, yang memerlukan respons dari server sebelum mengirim tindakan ke Dispatcher. Itu tidak pernah cocok dengan saya; bukankah seharusnya Toko yang pertama tahu tentang perubahan, sebelum server?
Saya memilih untuk membalik bagian diagram itu: Toko berinteraksi langsung dengan RESTful CRUD API melalui Backbone's sync()
. Ini sangat nyaman, setidaknya jika Anda bekerja dengan RESTful CRUD API yang sebenarnya.
Integritas data dipertahankan tanpa masalah. Saat Anda .set()
properti baru, peristiwa change
memicu rendering ulang React, yang secara optimis menampilkan data baru. Saat Anda mencoba .save()
ke server, kejadian request
memberi tahu Anda untuk menampilkan ikon pemuatan. Saat semuanya berjalan, acara sync
memberi tahu Anda untuk menghapus ikon pemuatan, atau acara error
memberi tahu Anda untuk mengubah segalanya menjadi merah. Anda bisa melihat inspirasinya di sini.
Ada juga validasi (dan kejadian invalid
terkait) untuk lapisan pertahanan pertama, dan metode .fetch()
untuk menarik informasi baru dari server.
Untuk tugas yang kurang standar, berinteraksi melalui ActionCreators mungkin lebih masuk akal. Saya menduga Facebook tidak melakukan banyak "hanya CRUD", dalam hal ini tidak mengherankan mereka tidak mengutamakan Toko.
Kesimpulan
Tim Teknik di Facebook telah melakukan pekerjaan luar biasa untuk mendorong web front-end ke depan dengan React, dan pengenalan Flux memberikan gambaran tentang arsitektur yang lebih luas yang benar-benar berskala: tidak hanya dalam hal teknologi, tetapi juga rekayasa. Penggunaan Backbone yang cerdas dan hati-hati (sesuai contoh tutorial ini) dapat mengisi kekosongan di Flux, membuatnya sangat mudah bagi siapa saja dari toko indie satu orang hingga perusahaan besar untuk membuat dan memelihara aplikasi yang mengesankan.