Vue 3'te Paylaşılan Durumu Yönetme

Yayınlanan: 2022-03-10
Hızlı özet ↬ Büyük ölçekli Vue uygulamaları yazmak zor olabilir. Vue 3 uygulamalarınızda paylaşılan durumu kullanmak, bu karmaşıklığı azaltmak için bir çözüm olabilir. Durumu çözmek için bir dizi ortak çözüm vardır. Bu yazıda fabrikalar, paylaşılan nesneler ve Vuex kullanma gibi yaklaşımların artılarını ve eksilerini inceleyeceğim. Ayrıca, Vue 3'te paylaşılan durumu kullanma şeklimizi değiştirebilecek Vuex 5'te neler olacağını size göstereceğim.

Devlet zor olabilir. Basit bir Vue projesine başladığımızda, çalışma durumumuzu belirli bir bileşen üzerinde tutmak basit olabilir:

 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 }; },

Projeniz verileri gösteren tek bir sayfaysa (belki de sıralamak veya filtrelemek için), bu zorlayıcı olabilir. Ancak bu durumda, bu bileşen her istekte veri alacaktır. Peki ya onu etrafta tutmak istersen? İşte burada devlet yönetimi devreye giriyor. Ağ bağlantıları genellikle pahalı ve bazen de güvenilmez olduğundan, bir uygulamada gezinirken bu durumu korumak daha iyi olur.

Diğer bir konu da bileşenler arasında iletişim kurmaktır. Doğrudan çocuklar-ebeveynlerle iletişim kurmak için olayları ve aksesuarları kullanabilseniz de, görüntülemelerinizin/sayfalarınızın her biri bağımsız olduğunda hata işleme ve meşgul bayrakları gibi basit durumları ele almak zor olabilir. Örneğin, hata ve yükleme animasyonu göstermek için bir üst düzey kontrole sahip olduğunuzu hayal edin:

 // 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>

Bu durumu ele almanın etkili bir yolu olmadan, bir yayınla/abone ol sistemi önerebilir, ancak aslında birçok durumda veri paylaşımı daha basittir. Paylaşılan bir duruma sahip olmak istiyorsanız, bunu nasıl yapacaksınız? Bunu yapmanın bazı yaygın yollarına bakalım.

Not : Bu bölümün kodunu GitHub'daki örnek projenin “ana” dalında bulacaksınız.

Vue 3'te Paylaşılan Durum

Vue 3'e geçtiğimden beri, Kompozisyon API'sini kullanmaya tamamen geçtim. Makale için ayrıca TypeScript kullanıyorum, ancak size gösterdiğim örnekler için bu gerekli değil. Durumu istediğiniz şekilde paylaşabilirsiniz, ancak size en sık kullanılan kalıpları bulduğum birkaç teknik göstereceğim. Her birinin kendi artıları ve eksileri vardır, bu yüzden burada bahsettiğim hiçbir şeyi dogma olarak algılamayın.

Teknikler şunları içerir:

  • fabrikalar,
  • Paylaşılan Singleton'lar,
  • görsel 4,
  • Vuex 5.

Not : Vuex 5, bu yazı yazılırken RFC (Yorum Talebi) aşamasında olduğu için sizi Vuex'in nereye gittiğine hazırlamak istiyorum ancak şu anda bu seçeneğin çalışan bir versiyonu yok.

Hadi kazalım…

fabrikalar

Not : Bu bölümün kodu GitHub'daki örnek projenin “Fabrikalar” dalındadır.

Fabrika modeli, ilgilendiğiniz durumun bir örneğini oluşturmakla ilgilidir. Bu modelde, Kompozisyon API'sindeki başlatma işlevine çok benzeyen bir işlev döndürürsünüz. Bir kapsam yaratır ve aradığınız şeyin bileşenlerini oluşturursunuz. Örneğin:

 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 }; }

İhtiyacınız olan, fabrikada oluşturulan nesnelerin yalnızca parçalarını isteyebilirsiniz:

 // In Home.vue const { books, loadBooks } = BookFactory();

Ağ isteği gerçekleştiğinde göstermek için bir isBusy bayrağı eklersek, yukarıdaki kod değişmez, ancak isBusy nerede göstereceğinize karar verebilirsiniz:

 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 }; }

Başka bir görünümde (vue?), fabrikanın geri kalanının nasıl çalıştığını bilmek zorunda kalmadan isBusy bayrağını isteyebilirsiniz:

 // App.vue export default defineComponent({ setup() { const { isBusy } = BookFactory(); return { isBusy } }, })

Ancak bir sorun fark etmiş olabilirsiniz; fabrikayı her aradığımızda, tüm nesnelerin yeni bir örneğini alıyoruz. Bir fabrikanın yeni örnekleri iade etmesini istediğiniz zamanlar vardır, ancak bizim durumumuzda durumu paylaşmaktan bahsediyoruz, bu yüzden yaratımı fabrikanın dışına taşımamız gerekiyor:

 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 }; }

Şimdi fabrika bize paylaşılan bir örnek veya isterseniz bir singleton veriyor. Bu model çalışırken, her seferinde yeni bir örnek oluşturmayan bir işlevi döndürmek kafa karıştırıcı olabilir.

Altta yatan nesneler const olarak işaretlendiğinden, bunları değiştirememelisiniz (ve tekil doğayı bozamazsınız). Yani bu kod şikayet etmelidir:

 // In Home.vue const { books, loadBooks } = BookFactory(); books = []; // Error, books is defined as const

Bu nedenle, değişken durumun güncellenebildiğinden emin olmak önemli olabilir (örneğin, kitapları atamak yerine books.splice() kullanarak).

Bunu ele almanın başka bir yolu da paylaşılan örnekleri kullanmaktır.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Paylaşılan Örnekler

Bu bölümün kodu GitHub'daki örnek projenin “SharedState” dalındadır.

Durumu paylaşacaksanız, devletin bir singleton olduğu konusunda net olabilirsiniz. Bu durumda, sadece statik bir nesne olarak alınabilir. Örneğin, reaktif nesne olarak içe aktarılabilen bir nesne oluşturmayı seviyorum:

 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; } });

Bu durumda, yalnızca nesneyi içe aktarırsınız (bu örnekte mağaza olarak adlandırıyorum):

 // Home.vue import state from "@/state"; export default defineComponent({ setup() { // ... onMounted(async () => { if (state.books.length === 0) state.loadBooks(); }); return { state, bookTopics, }; }, });

O zaman devlete bağlanmak kolaylaşır:

 <!-- 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>

Diğer kalıplar gibi, bu örneği görünümler arasında paylaşabilme avantajını elde edersiniz:

 // App.vue import state from "@/state"; export default defineComponent({ setup() { return { state }; }, })

O zaman bu, aynı nesneye bağlanabilir ( Home.vue ebeveyni veya yönlendiricideki başka bir sayfa olsun):

 <!-- 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>

İster fabrika modelini ister paylaşılan örneği kullanın, her ikisinin de ortak bir sorunu vardır: değiştirilebilir durum. İstemediğinizde, bağlamaların veya kod değiştirme durumunun yanlışlıkla yan etkileri olabilir. Burada kullandığım gibi önemsiz bir örnekte, endişelenecek kadar karmaşık değil. Ancak gitgide daha büyük uygulamalar geliştirirken durum mutasyonu hakkında daha dikkatli düşünmek isteyeceksiniz. Vuex'in kurtarmaya gelebileceği yer burasıdır.

Vuex 4

Bu bölümün kodu GitHub'daki örnek projenin “Vuex4” dalındadır.

Vuex, Vue için eyalet yöneticisidir. Ayrı bir proje olarak yönetilse de çekirdek ekip tarafından yapılmıştır. Vuex'in amacı, devlete yapmak istediğiniz eylemlerden durumu ayırmaktır. Tüm durum değişikliklerinin Vuex'ten geçmesi gerekir, bu da daha karmaşık olduğu anlamına gelir, ancak kazara durum değişikliğine karşı koruma alırsınız.

Vuex'in fikri, öngörülebilir bir devlet yönetimi akışı sağlamaktır. Görünümler, sırayla Görünümü güncelleyen Durumu değiştirmek için Mutasyonları kullanan Eylemlere akar. Durum değişikliği akışını sınırlayarak, uygulamalarınızın durumunu değiştiren daha az yan etkiye sahip olursunuz; bu nedenle daha büyük uygulamalar oluşturmak daha kolay olacaktır. Vuex'in bir öğrenme eğrisi var, ancak bu karmaşıklıkla tahmin edilebilirlik elde edersiniz.

Ek olarak, Vuex, zaman yolculuğu adı verilen bir özellik de dahil olmak üzere durum yönetimi ile çalışmak için geliştirme zamanı araçlarını (Vue Araçları aracılığıyla) destekler. Bu, durumun geçmişini görüntülemenize ve uygulamayı nasıl etkilediğini görmek için ileri geri gitmenize olanak tanır.

Vuex'in de önemli olduğu zamanlar da vardır.

Vue 3 projenize eklemek için paketi projeye ekleyebilirsiniz:

 > npm i vuex

Veya alternatif olarak Vue CLI'yi kullanarak ekleyebilirsiniz:

 > vue add vuex

CLI'yi kullanarak, Vuex mağazanız için bir başlangıç ​​noktası oluşturacaktır, aksi takdirde onu projeye manuel olarak bağlamanız gerekecektir. Bunun nasıl çalıştığını inceleyelim.

İlk olarak, Vuex'in createStore işleviyle oluşturulmuş bir durum nesnesine ihtiyacınız olacak:

 import { createStore } from 'vuex' export default createStore({ state: {}, mutations: {}, actions: {}, getters: {} });

Gördüğünüz gibi, mağaza tanımlanmak için birkaç özelliği gerektiriyor. Durum, uygulamanıza erişim izni vermek istediğiniz verilerin yalnızca bir listesidir:

 import { createStore } from 'vuex' export default createStore({ state: { books: [], isBusy: false }, mutations: {}, actions: {} });

Durumun ref veya reaktif sarmalayıcı kullanmaması gerektiğini unutmayın. Bu veriler, Paylaşılan Örnekler veya Fabrikalar ile kullandığımız aynı türde paylaşım verileridir. Bu mağaza, uygulamanızda bir singleton olacak, bu nedenle durumdaki veriler de paylaşılacak.

Ardından, eylemlere bakalım. Eylemler, durumu içeren etkinleştirmek istediğiniz işlemlerdir. Örneğin:

 actions: { async loadBooks(store) { const response = await bookService.getBooks(store.state.currentTopic, if (response.status === 200) { // ... } } },

Eylemler, duruma ve diğer işlemlere ulaşabilmeniz için mağazanın bir örneğine geçirilir. Normalde, sadece ihtiyacımız olan parçaları yok ederiz:

 actions: { async loadBooks({ state }) { const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { // ... } } },

Bunun son parçası Mutasyonlardır. Mutasyonlar, durumu değiştirebilen fonksiyonlardır. Sadece mutasyonlar durumu etkileyebilir. Dolayısıyla, bu örnek için durumu değiştiren mutasyonlara ihtiyacımız var:

 mutations: { setBusy: (state) => state.isBusy = true, clearBusy: (state) => state.isBusy = false, setBooks(state, books) { state.books.splice(0, state.books.length, ...books); } },

Mutasyon işlevleri her zaman durum nesnesine geçer, böylece o durumu değiştirebilirsiniz. İlk iki örnekte, durumu açıkça belirlediğimizi görebilirsiniz. Ama üçüncü örnekte, küme durumuna geçiyoruz. Mutasyonlar her zaman iki parametre alır: mutasyonu çağırırken durum ve argüman.

Bir mutasyon çağırmak için mağazadaki taahhüt işlevini kullanırsınız. Bizim durumumuzda, onu sadece yıkıma ekleyeceğim:

 actions: { async loadBooks({ state, commit }) { commit("setBusy"); const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { commit("setBooks", response.data); } commit("clearBusy"); } },

Burada göreceğiniz şey, taahhütün eylemin adını nasıl gerektirdiğidir. Bunu sadece sihirli dizeler kullanmakla kalmayıp bunu yapmanın püf noktaları var, ama şimdilik bunu atlayacağım. Sihirli dizelerin bu kullanımı, Vuex kullanmanın sınırlamalarından biridir.

Taahhüt kullanmak gereksiz bir sarmalayıcı gibi görünse de, Vuex'in mutasyonun dışında durumu değiştirmenize izin vermeyeceğini unutmayın, bu nedenle yalnızca taahhüt yoluyla çağrılar olacaktır.

Ayrıca setBooks çağrısının ikinci bir argüman aldığını da görebilirsiniz. Bu, mutasyonu çağıran ikinci argümandır. Daha fazla bilgiye ihtiyacınız olsaydı, onu tek bir argümanda toplamanız gerekirdi (şu anda Vuex'in başka bir sınırlaması). Kitap listesine bir kitap eklemeniz gerektiğini varsayarsak, buna şöyle diyebilirsiniz:

 commit("insertBook", { book, place: 4 }); // object, tuple, etc.

O zaman ihtiyacınız olan parçalara zarar verebilirsiniz:

 mutations: { insertBook(state, { book, place }) => // ... }

Bu zarif mi? Pek değil, ama işe yarıyor.

Artık mutasyonlarla çalışan eylemimiz olduğuna göre, kodumuzda Vuex mağazasını kullanabilmemiz gerekiyor. Mağazaya ulaşmanın gerçekten iki yolu var. İlk olarak, mağazayı uygulamaya kaydettirerek (örn. main.ts/js), uygulamanızın her yerinden erişebileceğiniz merkezi bir mağazaya erişiminiz olur:

 // main.ts import store from './store' createApp(App) .use(store) .use(router) .mount('#app')

Bunun Vuex'i değil, oluşturduğunuz gerçek mağazanızı eklediğini unutmayın. Bu eklendikten sonra, mağaza nesnesini almak için useStore çağırmanız yeterlidir:

 import { useStore } from "vuex"; export default defineComponent({ components: { BookInfo, }, setup() { const store = useStore(); const books = computed(() => store.state.books); // ...

Bu iyi çalışıyor, ancak mağazayı doğrudan içe aktarmayı tercih ediyorum:

 import store from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const books = computed(() => store.state.books); // ...

Artık mağaza nesnesine erişiminiz olduğuna göre, onu nasıl kullanıyorsunuz? Durum için, değişikliklerin bağlamalarınıza yayılması için bunları hesaplanmış işlevlerle sarmanız gerekir:

 export default defineComponent({ setup() { const books = computed(() => store.state.books); return { books }; }, });

Eylemleri çağırmak için gönderme yöntemini çağırmanız gerekir:

 export default defineComponent({ setup() { const books = computed(() => store.state.books); onMounted(async () => await store.dispatch("loadBooks")); return { books }; }, });

Eylemler, yöntem adından sonra eklediğiniz parametrelere sahip olabilir. Son olarak, durumu değiştirmek için, Eylemler'de yaptığımız gibi commit'i çağırmanız gerekecek. Örneğin, mağazada bir sayfalama özelliğim var ve ardından durumu commit ile değiştirebilirim:

 const incrementPage = () => store.commit("setPage", store.state.currentPage + 1); const decrementPage = () => store.commit("setPage", store.state.currentPage - 1);

Bu şekilde çağırmanın bir hata vereceğini unutmayın (çünkü durumu manuel olarak değiştiremezsiniz):

 const incrementPage = () => store.state.currentPage++; const decrementPage = () => store.state.currentPage--;

Buradaki gerçek güç budur, durumun nerede değiştirildiğini kontrol etmek isteriz ve geliştirme aşamasında hata üreten yan etkilerin olmamasını isteriz.

Vuex'teki hareketli parçaların sayısı sizi bunaltabilir, ancak daha büyük, daha karmaşık projelerde durumu yönetmenize gerçekten yardımcı olabilir. Her durumda buna ihtiyacınız olduğunu söyleyemem ama genel olarak size yardımcı olduğu büyük projeler olacak.

Vuex 4 ile ilgili en büyük sorun, onunla bir TypeScript projesinde çalışmanın arzulanan çok şey bırakmasıdır. Geliştirmeye ve derlemeye yardımcı olması için TypeScript türlerini kesinlikle yapabilirsiniz, ancak çok sayıda hareketli parça gerektirir.

Vuex 5'in, Vuex'in TypeScript'te (ve genel olarak JavaScript projelerinde) nasıl çalıştığını basitleştirmesi amaçlandığı yer burasıdır. Bakalım bir dahaki sefere piyasaya sürüldüğünde bu nasıl çalışacak.

Vuex 5

Not : Bu bölümün kodu GitHub'daki örnek projenin “Vuex5” dalındadır.

Bu makalenin yazıldığı sırada Vuex 5 gerçek değil. Bu bir RFC'dir (Yorum İsteği). Bu bir plan. Tartışma için bir başlangıç ​​noktasıdır. Yani burada açıklayabileceğim şeylerin çoğu muhtemelen biraz değişecek. Ama sizi Vuex'teki değişime hazırlamak için, nereye gittiğine dair bir fikir vermek istedim. Bu nedenle, bu örnekle ilişkili kod oluşturulmaz.

Vuex'in nasıl çalıştığına dair temel kavramlar, başlangıcından bu yana biraz değişmedi. Vue 3'ün tanıtılmasıyla birlikte Vuex 4, çoğunlukla Vuex'in yeni projelerde çalışmasına izin vermek için oluşturuldu. Ancak ekip, Vuex ile gerçek acı noktalarına bakmaya ve bunları çözmeye çalışıyor. Bu amaçla bazı önemli değişiklikler planlıyorlar:

  • Artık mutasyon yok: eylemler durumu (ve muhtemelen herhangi birini) değiştirebilir.
  • Daha iyi TypeScript desteği.
  • Daha iyi çoklu mağaza işlevselliği.

Peki bu nasıl çalışacak? Mağazayı oluşturmaya başlayalım:

 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); } } });

Görülmesi gereken ilk değişiklik, artık her mağazanın kendi anahtarına ihtiyaç duymasıdır. Bu, birden fazla mağazayı almanıza izin vermek içindir. Ardından, durum nesnesinin artık bir fabrika olduğunu fark edeceksiniz (örneğin, ayrıştırma sırasında oluşturulmayan bir işlevden gelen dönüşler). Ve artık mutasyonlar bölümü yok. Son olarak, eylemlerin içinde, this işaretçide duruma yalnızca özellikler olarak eriştiğimizi görebilirsiniz. Artık durumu geçmek ve eylemlerde bulunmak zorunda değilsiniz. Bu, yalnızca geliştirmeyi basitleştirmeye yardımcı olmakla kalmaz, aynı zamanda TypeScript için türler çıkarmayı da kolaylaştırır.

Vuex'i uygulamanıza kaydettirmek için global mağazanız yerine Vuex'i kaydettireceksiniz:

 import { createVuex } from 'vuex' createApp(App) .use(createVuex()) .use(router) .mount('#app')

Son olarak, mağazayı kullanmak için mağazayı içe aktaracak ve ardından bir örneğini oluşturacaksınız:

 import bookStore from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const store = bookStore(); // Generate the wrapper // ...

Mağazadan döndürülen şeyin, fabrikayı kaç kez ararsanız arayın, mağazanın bu örneğini döndüren bir fabrika nesnesi olduğuna dikkat edin. Döndürülen nesne, yalnızca birinci sınıf vatandaşlar olarak (tür bilgisiyle birlikte) eylemlere, duruma ve alıcılara sahip bir nesnedir:

 onMounted(async () => await store.loadBooks()); const incrementPage = () => store.currentPage++; const decrementPage = () => store.currentPage--;

Burada göreceğiniz şey, durumun (örneğin currentPage ) yalnızca basit özellikler olduğudur. Ve eylemler (örneğin loadBooks ) sadece işlevlerdir. Burada bir mağaza kullanıyor olmanız bir yan etki. Vuex nesnesine sadece bir nesne gibi davranabilir ve işinize devam edebilirsiniz. Bu, API'de önemli bir gelişmedir.

Belirtilmesi önemli olan bir diğer değişiklik, mağazanızı Composition API benzeri bir sözdizimi kullanarak da oluşturabilmenizdir:

 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 } });

Bu, Vuex nesnenizi tıpkı Composition API ile yaptığınız gibi oluşturmanıza olanak tanır ve muhtemelen daha basittir.

Bu yeni tasarımın bir ana dezavantajı, durumun değişmezliğini kaybetmenizdir. Bunu etkinleştirebilme konusunda tartışmalar var (sadece geliştirme için, tıpkı Vuex 4 gibi), ancak bunun ne kadar önemli olduğu konusunda fikir birliği yok. Şahsen bunun Vuex için önemli bir avantaj olduğunu düşünüyorum, ancak bunun nasıl sonuçlandığını görmemiz gerekecek.

Neredeyiz?

Tek sayfalı uygulamalarda paylaşılan durumu yönetmek, çoğu uygulama için geliştirmenin çok önemli bir parçasıdır. Vue'da nasıl ilerlemek istediğinize dair bir oyun planına sahip olmak, çözümünüzü tasarlamada önemli bir adımdır. Bu makalede, Vuex 5'e gelecek olanlar da dahil olmak üzere, paylaşılan durumu yönetmek için size birkaç model gösterdim. Umarım artık kendi projeleriniz için doğru kararı verecek bilgiye sahip olursunuz.