Mengelola Status Bersama Di Vue 3
Diterbitkan: 2022-03-10Negara bisa sulit. Saat kita memulai proyek Vue sederhana, akan mudah untuk hanya mempertahankan status kerja kita pada komponen tertentu:
setup() { let books: Work[] = reactive([]); onMounted(async () => { // Call the API const response = await bookService.getScienceBooks(); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } }); return { books }; },
Ketika proyek Anda adalah satu halaman yang menampilkan data (mungkin untuk mengurutkan atau memfilternya), ini bisa menarik. Namun dalam hal ini, komponen ini akan mendapatkan data pada setiap permintaan. Bagaimana jika Anda ingin menyimpannya? Di situlah manajemen negara berperan. Karena koneksi jaringan seringkali mahal dan terkadang tidak dapat diandalkan, akan lebih baik untuk menjaga keadaan ini saat Anda menavigasi aplikasi.
Masalah lainnya adalah komunikasi antar komponen. Meskipun Anda dapat menggunakan acara dan alat peraga untuk berkomunikasi dengan orang tua-anak langsung, menangani situasi sederhana seperti penanganan kesalahan dan tanda sibuk dapat menjadi sulit ketika setiap tampilan/halaman Anda independen. Misalnya, bayangkan Anda memiliki kontrol tingkat atas yang terhubung untuk menampilkan kesalahan dan memuat animasi:
// App.vue <template> <div class="container mx-auto bg-gray-100 p-1"> <router-link to="/"><h1>Bookcase</h1></router-link> <div class="alert" v-if="error">{{ error }}</div> <div class="alert bg-gray-200 text-gray-900" v-if="isBusy"> Loading... </div> <router-view :key="$route.fullPath"></router-view> </div> </template>
Tanpa cara yang efektif untuk menangani keadaan ini, ini mungkin menyarankan sistem terbitkan/berlangganan, tetapi pada kenyataannya berbagi data lebih mudah dalam banyak kasus. Jika ingin berbagi status, bagaimana caranya? Mari kita lihat beberapa cara umum untuk melakukan ini.
Catatan : Anda akan menemukan kode untuk bagian ini di cabang "utama" dari contoh proyek di GitHub.
Status Bersama Di Vue 3
Sejak pindah ke Vue 3, saya telah bermigrasi sepenuhnya menggunakan Composition API. Untuk artikel, saya juga menggunakan TypeScript meskipun itu tidak diperlukan untuk contoh yang saya tunjukkan kepada Anda. Meskipun Anda dapat berbagi status dengan cara apa pun yang Anda inginkan, saya akan menunjukkan kepada Anda beberapa teknik yang menurut saya merupakan pola yang paling umum digunakan. Masing-masing memiliki pro dan kontranya sendiri, jadi jangan menganggap apa pun yang saya bicarakan di sini sebagai dogma.
Tekniknya meliputi:
- Pabrik,
- Lajang Bersama,
- Vuex 4,
- Vuex 5.
Catatan : Vuex 5, pada saat penulisan artikel ini, sedang dalam tahap RFC (Permintaan untuk Komentar) jadi saya ingin Anda siap ke mana Vuex akan pergi, tetapi saat ini tidak ada versi yang berfungsi dari opsi ini.
Mari kita menggali…
Pabrik
Catatan : Kode untuk bagian ini ada di cabang “Factories” dari contoh proyek di GitHub.
Pola pabrik hanya tentang membuat instance dari keadaan yang Anda pedulikan. Dalam pola ini, Anda mengembalikan fungsi yang mirip dengan fungsi awal di Composition API. Anda akan membuat ruang lingkup dan membangun komponen dari apa yang Anda cari. Sebagai contoh:
export default function () { const books: Work[] = reactive([]); async function loadBooks(val: string) { const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } return { loadBooks, books }; }
Anda dapat meminta hanya bagian dari objek yang dibuat pabrik yang Anda butuhkan seperti:
// In Home.vue const { books, loadBooks } = BookFactory();
Jika kita menambahkan flag isBusy
untuk ditampilkan saat permintaan jaringan terjadi, kode di atas tidak berubah, tetapi Anda dapat memutuskan di mana Anda akan menampilkan isBusy
:
export default function () { const books: Work[] = reactive([]); const isBusy = ref(false); async function loadBooks(val: string) { isBusy.value = true; const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } return { loadBooks, books, isBusy }; }
Di tampilan lain (vue?) Anda bisa meminta flag isBusy tanpa harus tahu tentang cara kerja pabrik lainnya:
// App.vue export default defineComponent({ setup() { const { isBusy } = BookFactory(); return { isBusy } }, })
Tetapi Anda mungkin telah memperhatikan suatu masalah; setiap kali kami memanggil pabrik, kami mendapatkan contoh baru dari semua objek. Ada kalanya Anda ingin mengembalikan instance baru ke pabrik, tetapi dalam kasus kami, kami berbicara tentang berbagi status, jadi kami perlu memindahkan kreasi ke luar pabrik:
const books: Work[] = reactive([]); const isBusy = ref(false); async function loadBooks(val: string) { isBusy.value = true; const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } export default function () { return { loadBooks, books, isBusy }; }
Sekarang pabrik memberi kami contoh bersama, atau singleton jika Anda mau. Meskipun pola ini berfungsi, mungkin membingungkan untuk mengembalikan fungsi yang tidak membuat instance baru setiap saat.
Karena objek yang mendasarinya ditandai sebagai const
, Anda seharusnya tidak dapat menggantinya (dan merusak sifat tunggal). Jadi kode ini harus mengeluh:
// In Home.vue const { books, loadBooks } = BookFactory(); books = []; // Error, books is defined as const
Jadi, penting untuk memastikan status yang dapat diubah dapat diperbarui (misalnya menggunakan books.splice()
alih-alih menetapkan buku).
Cara lain untuk menangani ini adalah dengan menggunakan instance bersama.
Instance yang Dibagikan
Kode untuk bagian ini ada di cabang “SharedState” dari contoh proyek di GitHub.
Jika Anda akan berbagi status, mungkin juga jelas tentang fakta bahwa negara bagian adalah lajang. Dalam hal ini, itu hanya dapat diimpor sebagai objek statis. Misalnya, saya suka membuat objek yang dapat diimpor sebagai objek reaktif:
export default reactive({ books: new Array<Work>(), isBusy: false, async loadBooks() { this.isBusy = true; const response = await bookService.getBooks(this.currentTopic, this.currentPage); if (response.status === 200) { this.books.splice(0, this.books.length, ...response.data.works); } this.isBusy = false; } });
Dalam hal ini, Anda cukup mengimpor objek (yang saya sebut toko dalam contoh ini):
// Home.vue import state from "@/state"; export default defineComponent({ setup() { // ... onMounted(async () => { if (state.books.length === 0) state.loadBooks(); }); return { state, bookTopics, }; }, });
Maka menjadi mudah untuk mengikat ke negara:
<!-- Home.vue --> <div class="grid grid-cols-4"> <div v-for="book in state.books" :key="book.key" class="border bg-white border-grey-500 m-1 p-1" > <router-link :to="{ name: 'book', params: { id: book.key } }"> <BookInfo :book="book" /> </router-link> </div>
Seperti pola lainnya, Anda mendapatkan manfaat bahwa Anda dapat membagikan instans ini di antara tampilan:
// App.vue import state from "@/state"; export default defineComponent({ setup() { return { state }; }, })
Kemudian ini dapat mengikat ke objek yang sama (apakah itu induk dari Home.vue
atau halaman lain di router):
<!-- App.vue --> <div class="container mx-auto bg-gray-100 p-1"> <router-link to="/"><h1>Bookcase</h1></router-link> <div class="alert bg-gray-200 text-gray-900" v-if="state.isBusy">Loading...</div> <router-view :key="$route.fullPath"></router-view> </div>
Baik Anda menggunakan pola pabrik atau instans bersama, keduanya memiliki masalah yang sama: status yang dapat diubah. Anda dapat memiliki efek samping yang tidak disengaja dari binding atau status perubahan kode saat Anda tidak menginginkannya. Dalam contoh sepele seperti yang saya gunakan di sini, itu tidak cukup rumit untuk dikhawatirkan. Tetapi saat Anda sedang membangun aplikasi yang lebih besar dan lebih besar, Anda akan ingin memikirkan mutasi status dengan lebih hati-hati. Di situlah Vuex bisa datang untuk menyelamatkan.
Vuex 4
Kode untuk bagian ini ada di cabang “Vuex4” dari contoh proyek di GitHub.
Vuex adalah manajer negara bagian untuk Vue. Itu dibangun oleh tim inti meskipun dikelola sebagai proyek terpisah. Tujuan Vuex adalah untuk memisahkan status dari tindakan yang ingin Anda lakukan terhadap status. Semua perubahan status harus melalui Vuex yang berarti lebih kompleks, tetapi Anda mendapatkan perlindungan dari perubahan status yang tidak disengaja.
Ide Vuex adalah untuk menyediakan aliran manajemen negara yang dapat diprediksi. Tampilan mengalir ke Tindakan yang, pada gilirannya, menggunakan Mutasi untuk mengubah Status yang, pada gilirannya, memperbarui Tampilan. Dengan membatasi aliran perubahan status, Anda seharusnya memiliki lebih sedikit efek samping yang mengubah status aplikasi Anda; oleh karena itu akan lebih mudah untuk membangun aplikasi yang lebih besar. Vuex memiliki kurva belajar, tetapi dengan kompleksitas itu Anda mendapatkan prediktabilitas.
Selain itu, Vuex mendukung alat waktu pengembangan (melalui Alat Vue) untuk bekerja dengan manajemen status termasuk fitur yang disebut perjalanan waktu. Ini memungkinkan Anda untuk melihat riwayat status dan bergerak maju mundur untuk melihat bagaimana pengaruhnya terhadap aplikasi.
Ada kalanya juga, ketika Vuex juga penting.
Untuk menambahkannya ke proyek Vue 3 Anda, Anda dapat menambahkan paket ke proyek:
> npm i vuex
Atau, sebagai alternatif, Anda dapat menambahkannya dengan menggunakan Vue CLI:
> vue add vuex
Dengan menggunakan CLI, ini akan membuat titik awal untuk toko Vuex Anda, jika tidak, Anda harus menghubungkannya secara manual ke proyek. Mari kita telusuri bagaimana ini bekerja.
Pertama, Anda memerlukan objek status yang dibuat dengan fungsi createStore Vuex:
import { createStore } from 'vuex' export default createStore({ state: {}, mutations: {}, actions: {}, getters: {} });
Seperti yang Anda lihat, toko membutuhkan beberapa properti untuk didefinisikan. Status hanyalah daftar data yang ingin Anda berikan akses aplikasi Anda ke:
import { createStore } from 'vuex' export default createStore({ state: { books: [], isBusy: false }, mutations: {}, actions: {} });
Perhatikan bahwa negara tidak boleh menggunakan ref atau pembungkus reaktif . Data ini adalah jenis data berbagi yang sama yang kami gunakan dengan Instans atau Pabrik Bersama. Toko ini akan menjadi singleton di aplikasi Anda, oleh karena itu data dalam status juga akan dibagikan.
Selanjutnya, mari kita lihat tindakan. Tindakan adalah operasi yang ingin Anda aktifkan yang melibatkan keadaan. Sebagai contoh:
actions: { async loadBooks(store) { const response = await bookService.getBooks(store.state.currentTopic, if (response.status === 200) { // ... } } },
Tindakan melewati sebuah instance dari toko sehingga Anda bisa mendapatkan di negara bagian dan operasi lainnya. Biasanya, kami hanya akan merusak bagian yang kami butuhkan:
actions: { async loadBooks({ state }) { const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { // ... } } },
Bagian terakhir dari ini adalah Mutasi. Mutasi adalah fungsi yang dapat mengubah keadaan. Hanya mutasi yang dapat mempengaruhi keadaan. Jadi, untuk contoh ini, kita membutuhkan mutasi yang mengubah status:
mutations: { setBusy: (state) => state.isBusy = true, clearBusy: (state) => state.isBusy = false, setBooks(state, books) { state.books.splice(0, state.books.length, ...books); } },
Fungsi mutasi selalu melewati objek status sehingga Anda dapat mengubah status tersebut. Dalam dua contoh pertama, Anda dapat melihat bahwa kami secara eksplisit menyetel status. Namun pada contoh ketiga, kita memberikan state ke set. Mutasi selalu mengambil dua parameter: status dan argumen saat memanggil mutasi.
Untuk memanggil mutasi, Anda akan menggunakan fungsi komit di toko. Dalam kasus kami, saya hanya akan menambahkannya ke destructuring:
actions: { async loadBooks({ state, commit }) { commit("setBusy"); const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { commit("setBooks", response.data); } commit("clearBusy"); } },
Apa yang akan Anda lihat di sini adalah bagaimana komit memerlukan nama tindakan. Ada trik untuk membuat ini tidak hanya menggunakan senar ajaib, tetapi saya akan melewatkannya untuk saat ini. Penggunaan string ajaib ini adalah salah satu batasan penggunaan Vuex.
Saat menggunakan komit mungkin tampak seperti pembungkus yang tidak perlu, ingatlah bahwa Vuex tidak akan membiarkan Anda mengubah status kecuali di dalam mutasi, oleh karena itu hanya panggilan melalui komit yang akan.
Anda juga dapat melihat bahwa panggilan ke setBooks membutuhkan argumen kedua. Ini adalah argumen kedua yang menyebut mutasi. Jika Anda membutuhkan lebih banyak informasi, Anda perlu mengemasnya ke dalam satu argumen (keterbatasan lain dari Vuex saat ini). Dengan asumsi Anda perlu memasukkan buku ke dalam daftar buku, Anda bisa menyebutnya seperti ini:
commit("insertBook", { book, place: 4 }); // object, tuple, etc.
Kemudian Anda bisa merusak struktur menjadi bagian-bagian yang Anda butuhkan:
mutations: { insertBook(state, { book, place }) => // ... }
Apakah ini elegan? Tidak juga, tapi itu berhasil.
Sekarang setelah tindakan kita bekerja dengan mutasi, kita harus dapat menggunakan toko Vuex dalam kode kita. Sebenarnya ada dua cara untuk mendapatkan di toko. Pertama, dengan mendaftarkan toko dengan aplikasi (misalnya main.ts/js), Anda akan memiliki akses ke toko terpusat yang dapat Anda akses di mana saja di aplikasi Anda:
// main.ts import store from './store' createApp(App) .use(store) .use(router) .mount('#app')
Perhatikan bahwa ini tidak menambahkan Vuex, tetapi toko Anda yang sebenarnya yang Anda buat. Setelah ini ditambahkan, Anda bisa memanggil useStore
untuk mendapatkan objek store:
import { useStore } from "vuex"; export default defineComponent({ components: { BookInfo, }, setup() { const store = useStore(); const books = computed(() => store.state.books); // ...
Ini berfungsi dengan baik, tetapi saya lebih suka mengimpor toko secara langsung:
import store from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const books = computed(() => store.state.books); // ...
Sekarang setelah Anda memiliki akses ke objek toko, bagaimana Anda menggunakannya? Untuk status, Anda harus membungkusnya dengan fungsi yang dihitung sehingga perubahan akan diterapkan ke binding Anda:
export default defineComponent({ setup() { const books = computed(() => store.state.books); return { books }; }, });
Untuk memanggil tindakan, Anda perlu memanggil metode pengiriman :
export default defineComponent({ setup() { const books = computed(() => store.state.books); onMounted(async () => await store.dispatch("loadBooks")); return { books }; }, });
Tindakan dapat memiliki parameter yang Anda tambahkan setelah nama metode. Terakhir, untuk mengubah status, Anda harus memanggil komit seperti yang kami lakukan di dalam Tindakan. Misalnya, saya memiliki properti paging di toko, dan kemudian saya dapat mengubah status dengan commit :
const incrementPage = () => store.commit("setPage", store.state.currentPage + 1); const decrementPage = () => store.commit("setPage", store.state.currentPage - 1);
Perhatikan, memanggilnya seperti ini akan menimbulkan kesalahan (karena Anda tidak dapat mengubah status secara manual):
const incrementPage = () => store.state.currentPage++; const decrementPage = () => store.state.currentPage--;
Ini adalah kekuatan sebenarnya di sini, kami ingin kontrol di mana keadaan diubah dan tidak memiliki efek samping yang menghasilkan kesalahan lebih jauh dalam pengembangan.
Anda mungkin kewalahan dengan jumlah bagian yang bergerak di Vuex, tetapi ini benar-benar dapat membantu mengelola status dalam proyek yang lebih besar dan lebih kompleks. Saya tidak akan mengatakan Anda membutuhkannya dalam setiap kasus, tetapi akan ada proyek besar yang membantu Anda secara keseluruhan.
Masalah besar dengan Vuex 4 adalah bekerja dengannya dalam proyek TypeScript meninggalkan banyak hal yang diinginkan. Anda tentu saja dapat membuat tipe TypeScript untuk membantu pengembangan dan pembuatan, tetapi membutuhkan banyak bagian yang bergerak.
Di situlah Vuex 5 dimaksudkan untuk menyederhanakan cara kerja Vuex di TypeScript (dan dalam proyek JavaScript secara umum). Mari kita lihat bagaimana itu akan bekerja setelah dirilis berikutnya.
Vuex 5
Catatan : Kode untuk bagian ini ada di cabang “Vuex5” dari contoh proyek di GitHub.
Pada saat artikel ini dibuat, Vuex 5 tidak nyata. Ini adalah RFC (Permintaan Komentar). Ini adalah rencana. Ini adalah titik awal untuk diskusi. Jadi banyak dari apa yang saya jelaskan di sini kemungkinan akan sedikit berubah. Tetapi untuk mempersiapkan Anda menghadapi perubahan di Vuex, saya ingin memberi Anda gambaran tentang ke mana arahnya. Karena itu, kode yang terkait dengan contoh ini tidak dibuat.
Konsep dasar tentang cara kerja Vuex agak tidak berubah sejak awal. Dengan diperkenalkannya Vue 3, Vuex 4 dibuat untuk memungkinkan Vuex bekerja di proyek-proyek baru. Tetapi tim sedang mencoba untuk melihat titik-titik sakit yang sebenarnya dengan Vuex dan menyelesaikannya. Untuk tujuan ini mereka merencanakan beberapa perubahan penting:
- Tidak ada lagi mutasi: tindakan dapat mengubah status (dan mungkin siapa saja).
- Dukungan TypeScript yang lebih baik.
- Fungsionalitas multi-toko yang lebih baik.
Jadi bagaimana ini akan berhasil? Mari kita mulai dengan membuat toko:
export default createStore({ key: 'bookStore', state: () => ({ isBusy: false, books: new Array<Work>() }), actions: { async loadBooks() { try { this.isBusy = true; const response = await bookService.getBooks(); if (response.status === 200) { this.books = response.data.works; } } finally { this.isBusy = false; } } }, getters: { findBook(key: string): Work | undefined { return this.books.find(b => b.key === key); } } });
Perubahan pertama yang harus dilihat adalah bahwa setiap toko sekarang membutuhkan kuncinya sendiri. Ini untuk memungkinkan Anda mengambil beberapa toko. Selanjutnya Anda akan melihat bahwa objek status sekarang menjadi pabrik (mis. pengembalian dari suatu fungsi, tidak dibuat saat penguraian). Dan tidak ada lagi bagian mutasi. Terakhir, di dalam tindakan, Anda dapat melihat kami mengakses status hanya sebagai properti pada penunjuk this
. Tidak ada lagi harus lulus dalam keadaan dan berkomitmen untuk tindakan. Ini membantu tidak hanya dalam menyederhanakan pengembangan, tetapi juga memudahkan untuk menyimpulkan tipe untuk TypeScript.
Untuk mendaftarkan Vuex ke dalam aplikasi Anda, Anda akan mendaftarkan Vuex alih-alih toko global Anda:
import { createVuex } from 'vuex' createApp(App) .use(createVuex()) .use(router) .mount('#app')
Terakhir, untuk menggunakan toko, Anda akan mengimpor toko lalu membuat turunannya:
import bookStore from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const store = bookStore(); // Generate the wrapper // ...
Perhatikan bahwa apa yang dikembalikan dari toko adalah objek pabrik yang mengembalikan contoh toko ini, tidak peduli berapa kali Anda memanggil pabrik. Objek yang dikembalikan hanyalah objek dengan tindakan, status, dan pengambil sebagai warga kelas satu (dengan informasi tipe):
onMounted(async () => await store.loadBooks()); const incrementPage = () => store.currentPage++; const decrementPage = () => store.currentPage--;
Apa yang akan Anda lihat di sini adalah status itu (misalnya currentPage
) hanyalah properti sederhana. Dan tindakan (misalnya loadBooks
) hanyalah fungsi. Fakta bahwa Anda menggunakan toko di sini adalah efek samping. Anda dapat memperlakukan objek Vuex hanya sebagai objek dan melanjutkan pekerjaan Anda. Ini adalah peningkatan yang signifikan dalam API.
Perubahan lain yang penting untuk ditunjukkan adalah bahwa Anda juga dapat membuat toko Anda menggunakan sintaks mirip API Komposisi:
export default defineStore("another", () => { // State const isBusy = ref(false); const books = reactive(new Array≷Work>()); // Actions async function loadBooks() { try { this.isBusy = true; const response = await bookService.getBooks(this.currentTopic, this.currentPage); if (response.status === 200) { this.books = response.data.works; } } finally { this.isBusy = false; } } findBook(key: string): Work | undefined { return this.books.find(b => b.key === key); } // Getters const bookCount = computed(() => this.books.length); return { isBusy, books, loadBooks, findBook, bookCount } });
Ini memungkinkan Anda membangun objek Vuex seperti yang Anda lakukan pada tampilan Anda dengan Composition API dan bisa dibilang lebih sederhana.
Salah satu kelemahan utama dalam desain baru ini adalah Anda kehilangan status non-mutabilitas. Ada diskusi yang terjadi seputar kemampuan untuk mengaktifkan ini (hanya untuk pengembangan, seperti Vuex 4) tetapi tidak ada konsensus betapa pentingnya ini. Saya pribadi berpikir ini adalah manfaat utama untuk Vuex, tetapi kita harus melihat bagaimana hasilnya.
Di mana kita?
Mengelola status bersama dalam aplikasi satu halaman adalah bagian penting dari pengembangan untuk sebagian besar aplikasi. Memiliki rencana permainan tentang bagaimana Anda ingin melakukannya di Vue adalah langkah penting dalam merancang solusi Anda. Dalam artikel ini, saya telah menunjukkan kepada Anda beberapa pola untuk mengelola status bersama termasuk apa yang akan datang untuk Vuex 5. Semoga sekarang Anda memiliki pengetahuan untuk membuat keputusan yang tepat untuk proyek Anda sendiri.