在Vue 3中如何管理共享狀態?以下這篇文章就來帶大家探討一下,希望對大家有幫助!
快速總結↬ 編寫大型應用程式可能是一個挑戰。在Vue3應用程式中使用共享狀態可以解決專案複雜性。有很多常見的狀態解決方案, 在本文中,我將深入探討使用工廠、共享物件和Vuex等方法的優缺點。我也會向你展示Vuex5中可能改變我們在Vue3中使用共享狀態的一些內容。 【相關推薦:vue.js影片教學】
state
可能很難,當我們開始一個簡單的Vue專案時,只需要我們保持特定元件的工作狀態就很簡單:
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 }; },
當你的專案是顯示資料的單一頁面(可能是為了排序或過濾資料)時,這可能很有吸引力。但是在本例中,該元件將獲得每個請求的資料。如果你想保留它呢?這時候狀態管理就可以發揮作用了。由於網路連線通常代價很高且偶爾不太可靠。因此在瀏覽應用程式時最好保持一些狀態。
另一個問題是元件之間的通訊。雖然您可以使用events
和props
直接與child
-parents
通信, 但是當你的每個views/pages都是獨立的時候,處理諸如錯誤攔截和忙碌標誌之類的簡單情況會變得很困難。舉個例子, 假設你使用了一個頂級控制項來顯示error和載入動畫:
// 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>
如果沒有有效的方法來處理這種狀態,它可能會建議使用發布/訂閱系統,但實際上在許多情況下共享資料更為直接。如果你想使用共享狀態,你該怎麼做呢?讓我們看看一些常見的方法。
注意:你將在 GitHub 上範例專案的「main」分支中找到此部分的程式碼。
#自從遷移到Vue 3 後,我已經完全遷移到使用Composition API,這篇文章我還使用了TypeScript
儘管在我展示的範例中這不是必需的。雖然你可以以任何方式共享狀態,但我將向你展示我發現的幾種最常用的技術模式,每一種都有自己的優點和缺點,所以不要把我在這裡所說的任何東西當作唯一。
技術包括:
#注意: 在撰寫本文時,Vuex 5還處於RFC(徵求意見)階段,所以我想讓你為Vuex5的到來做好準備,但目前還沒有該選項的工作版本。
讓我們深入了解…
#注意
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 }; }###你可以要求工廠建立你所需要的那部分物件:###
// In Home.vue const { books, loadBooks } = BookFactory();###如果我們新增一個###isBusy###標記來顯示網路請求何時發生,上面的程式碼不會改變,但你可以決定在哪裡顯示###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 }; }###在另一個視圖(vue?)中,你可以只要###isBusy####標記,而不需要知道工廠其他部分是如何運作的:###
// App.vue export default defineComponent({ setup() { const { isBusy } = BookFactory(); return { isBusy } }, })
但是你可能注意到了一个问题;每次我们调用工厂时,我们都会获得所有对象的新实例。有时,你希望工厂返回新实例,但在我们的例子中,我们讨论的是共享状态,因此我们需要将创建移到工厂之外:
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 }; }
现在,工厂给了我们一个共享实例,或者一个单例。虽然这种模式可以工作,但返回一个每次都不创建新实例的函数可能会令人困惑。
因为底层对象被标记为const
,所以不能替换它们(也不能破坏单例的性质)。所以这段代码应该会报错:
// In Home.vue const { books, loadBooks } = BookFactory(); books = []; // Error, books is defined as const
因此,确保可变状态能够被更新是很重要的(例如,使用books.splice()
而不是设置books)。另一种处理方法是使用共享实例
本节的代码在GitHub上示例项目的“Sharedstate”分支中。
如果你使用共享状态,最好清楚状态是一个单例的事实。在本例中,它可以作为一个静态对象导入。例如,我喜欢创建一个可以作为响应式对象导入的对象:
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; } });
在这种情况下,你只需导入对象(在本例中我将其称为商店):
// Home.vue import state from "@/state"; export default defineComponent({ setup() { // ... onMounted(async () => { if (state.books.length === 0) state.loadBooks(); }); return { state, bookTopics, }; }, });
然后很容易绑定到状态:
<!-- 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>
与其他模式一样,你可以在视图之间共享此实例:
// App.vue import state from "@/state"; export default defineComponent({ setup() { return { state }; }, })
然后它可以绑定到同一个对象(无论它是Home.Vue
的父页面)或路由到其他页面):
<!-- 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>
无论使用工厂模式还是共享实例,它们都有一个共同的问题:可变状态。当你不希望绑定或代码更改状态时,可能会产生意外的副作用。在本文中我使用简单示例中,它还没有那么复杂,因此不用担心。但是当你构建的应用程序越大越大时,将需要更仔细地考虑状态的变化。这时候Vuex就可以伸出援手了。
本节的代码位于GitHub上示例项目的“Vuex4”分支中。
Vuex是vue的状态管理器。它是由核心团队构建的,但它作为一个单独的项目进行管理。Vuex的目的是将状态与状态执行操作分离开来。所有状态更改都必须通过 Vuex,这意味着它更复杂,但你可以防止意外状态更改。
Vuex 的想法是提供可预测的状态管理流程。视图流向 Actions,Actions 反过来使用 Mutations 来改变状态,从而更新视图。通过限制状态更改的流程,可以减少改变应用程序状态的副作用;因此更容易构建更大的应用程序。Vuex 有一个学习曲线,但有了这种复杂性,你就可以获得可预测性。
此外,Vuex 还支持程序调试(通过 Vue Tools)来处理状态管理,包括一个称为time-travel
的功能。这允许你查看状态的历史记录并前后移动以查看它如何影响应用程序的。
有时,Vuex 也很重要。
要将其添加到你的 Vue 3 项目中,你可以将包添加到项目中:
> npm i vuex
或者,你也可以使用 Vue CLI 添加它:
> vue add vuex
通过使用CLI,它将为你的 Vuex Store 创建初始的起点, 否则你需要手动将其接入到项目。让我们来看看这是如何工作的。
首先,你需要使用 Vuex 的 createStore 函数创建一个状态对象:
import { createStore } from 'vuex' export default createStore({ state: {}, mutations: {}, actions: {}, getters: {} });
如你所见,Store需要定义多个属性。State只是一个想要授予应用程序访问权限的数据列表:
import { createStore } from 'vuex' export default createStore({ state: { books: [], isBusy: false }, mutations: {}, actions: {} });
注意state不应使用ref or reactive 包装。这里的数据与我们在共享实例或工厂中使用的共享数据类型相同。这个store在你的应用程序中将作为一个单例,因此状态中的数据也会被共享
然后,让我们看看actions
。 Actions是可以授予你改变state中数据的一个操作台,举个例子:
actions: { async loadBooks(store) { const response = await bookService.getBooks(store.state.currentTopic, if (response.status === 200) { // ... } } },
Actions 会传递一个 store 的实例,以便你可以获取状态和其他操作。通常,我们只会解构我们需要的部分:
actions: { async loadBooks({ state }) { const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { // ... } } },
最后一个是Mutations
。Mutations是能够改变状态的函数。只有Mutations才能影响状态。在这个例子中,我们需要Mutations来改变状态:
mutations: { setBusy: (state) => state.isBusy = true, clearBusy: (state) => state.isBusy = false, setBooks(state, books) { state.books.splice(0, state.books.length, ...books); } },
Mutation函数总是传入状态对象,以便你可以改变状态。在前两个示例中,你可以看到我们显式地设置了状态。但在第三个例子中,我们传入了set的状态。调用mutation时,mutation总是接收两个参数:state和实参。
要调用mutation, 你需要在store中使用commit函数。在我们的例子中,我只是将它添加到解构中:
actions: { async loadBooks({ state, commit }) { commit("setBusy"); const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { commit("setBooks", response.data); } commit("clearBusy"); } },
你将在此处看到commit如何要求action的命名。有一些技巧使它不仅仅是有魔力的字符串。但是我现在跳过它们。这种有魔力字符串是限制Vuex的使用方式之一。
虽然使用 commit 可能看起来像是一个不必要的包装器,但请记住,Vuex 不会让你改变状态,除非在mutation内部,因此只有通过commit调用才会。
你还可以看到对setBooks的调用采用了第二个参数。这是调用mutation的第二个参数。如果你需要更多信息,则需要将其打包成一个参数(目前 Vuex 的另一个限制)。假设你需要将一本书插入到图书列表中,你可以这样称呼它:
commit("insertBook", { book, place: 4 }); // object, tuple, etc.
然后你可以解构成你需要的部分:
mutations: { insertBook(state, { book, place }) => // ... }
这优雅吗?并不是,但它有效。
现在我们的 action 已经处理了mutations,我们需要能够在我们的代码中使用 Vuex 存储。有两种方法可以得到store。首先,通过使用应用程序(例如 main.ts/js)注册store,你将可以访问一个集中式store, 你可以在应用程序的任何地方访问它:
// main.ts import store from './store' createApp(App) .use(store) .use(router) .mount('#app')
注意,这并不是在添加Vuex,而是你实际上正在创建的商店。一旦添加后, 你只需要调用useStore
即可获取store对象:
import { useStore } from "vuex"; export default defineComponent({ components: { BookInfo, }, setup() { const store = useStore(); const books = computed(() => store.state.books); // ...
这很好用,但我更喜欢直接导入store:
import store from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const books = computed(() => store.state.books); // ...
既然你可以访问 store 对象,那么如何使用它呢?对于state,你需要使用计算函数包装它们,以便将它们更改并传递到你的绑定上:
export default defineComponent({ setup() { const books = computed(() => store.state.books); return { books }; }, });
要调用actions,你需要调用dispatch 方法:
export default defineComponent({ setup() { const books = computed(() => store.state.books); onMounted(async () => await store.dispatch("loadBooks")); return { books }; }, });
Actions可以在方法名称后添加的参数。最后,要更改状态,你需要像我们在 Actions 中所做的那样去调用 commit。例如,我在 store 中有一个 分页属性,然后我可以使用commit更改状态:
const incrementPage = () => store.commit("setPage", store.state.currentPage + 1); const decrementPage = () => store.commit("setPage", store.state.currentPage - 1);
注意, 这样调用可能会抛出一个异常(因为你不能手动改变state):
const incrementPage = () => store.state.currentPage++; const decrementPage = () => store.state.currentPage--;
这是Vuex真正强大的地方,我们想要控制状态改变的地方并且不会造成在开发过程中产生进一步错误。
你可能对 Vuex 中大量改变状态的方式感到不知所措,但它确实可以帮助管理更大、更复杂的项目中的状态。我不会说你在每种情况下都需要它,但是会有一些大型项目在总体上对你有帮助。
这就是 Vuex 5 旨在简化 Vuex 在 TypeScript(以及一般的 JavaScript 项目)中的工作方式的地方。让我们看看它在下一次发布后将如何运作。
注意: 此部分的代码位于GitHub 上示例项目的“Vuex5”分支中。
在撰写本文时,Vuex 5 还没有出现。这是一个 RFC(征求意见)。这是计划。是讨论的起点。所以我在这里说明的很多东西可能会发生变化。但是为了让你们对 Vuex 的变化有所准备,我想让你们了解一下它的发展方向。因此,与此示例关联的代码不会生成。
Vuex工作原理的基本概念从一开始就没有改变过。随着Vue 3的引入,Vuex 4的创建主要允许Vuex在新项目中工作。但该团队正试图找到Vuex的真正痛点并解决它们。为此,他们正在计划一些重要的改变:
那么它是如何工作的呢?让我们从创建store开始:
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); } } });
首先要看到的是,每个store现在都需要有自己的key。这允许你检索多个store。接下来,你会注意到state对象现在是一个工厂(例如,从一个函数返回,不是在解析时创建的)。没有mutations部分了。最后,在actions内部,你可以看到我们访问的状态只是this
指向的属性。不需要再通过传递state来commit Actions。这不仅有助于简化开发,还使TypeScript更容易推断类型。
要将 Vuex 注册到你的应用程序中,你将注册 Vuex 而不是你的全局store:
import { createVuex } from 'vuex' createApp(App) .use(createVuex()) .use(router) .mount('#app')
最后,要使用store,你将导入store然后创建它的一个实例:
import bookStore from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const store = bookStore(); // Generate the wrapper // ...
请注意,从商店返回的是一个工厂对象,无论你调用工厂多少次,它都会返回该商店的实例。返回只是一个具有单一类行为的actions、state和getters对象(带有类型信息):
onMounted(async () => await store.loadBooks()); const incrementPage = () => store.currentPage++; const decrementPage = () => store.currentPage--;
你将在此处看到 state(例如currentPage
)只是简单的属性。而actions(例如loadBooks
)只是函数。实际上你在这里使用的store是一个副作用。你可以将 Vuex 对象视为一个对象并继续工作。这是 API 的重大改进。
另一个需要指出的重要变化是,你也可以使用类似于Composition API的语法来生成你的store:
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 } });
这允许你像使用 Composition API 构建视图那样去构建 Vuex 对象,并且可以说它更简单。
这种新设计的一个主要缺点是失去了状态的不可变性。围绕能够启用此功能(仅用于开发,就像 Vuex 4 一样)进行了讨论,但对此的重要性尚未达成共识。我个人认为这是 Vuex 的主要有点,但我们必须看看它是如何发挥作用的。
在单页面应用程序中管理共享状态是大多数应用程序开发的关键部分。在设计解决方案时,制定一个关于如何使用Vue的策略是非常重要的一步。在本文中,我向你展示了几种管理共享状态的模式,包括即将在Vuex 5中推出的模式。希望你现在拥有为自己的项目做出正确决策的知识。
原文地址:https://www.smashingmagazine.com/2021/06/managing-shared-state-vue3/
原文作者:Shawn Wildermuth
更多编程相关知识,请访问:编程入门!!
以上是深入解析在Vue 3中如何管理共享狀態? 【翻譯】的詳細內容。更多資訊請關注PHP中文網其他相關文章!