Home > Article > Web Front-end > In-depth analysis of how to manage shared state in Vue 3? 【translate】
How to manage shared state in Vue 3? The following article will discuss it with you, I hope it will be helpful to you!
Quick Summary↬ Writing large applications can be a challenge. Project complexity can be solved using shared state in Vue3 applications. There are many common state solutions, and in this article I'll dive into the pros and cons of using methods like factories, shared objects, and Vuex. I'll also show you some things in Vuex5 that might change how we use shared state in Vue3. [Related recommendations: vue.js video tutorial]
state
may be difficult, when we start a simple Vue project, we only need to keep the specific components The working state is simple:
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 }; },
This can be attractive when your project is a single page that displays data (perhaps for sorting or filtering the data). But in this case, the component will get the data for every request. What if you want to keep it? This is where state management comes into play. Since network connections are often expensive and occasionally unreliable. So it's best to keep some state while browsing the app.
Another issue is communication between components. Although you can communicate directly with child
-parents
using events
and props
, when each of your views/pages is independent , handling simple cases like error interception and busy flags becomes difficult. For example, suppose you use a top-level control to display errors and load animations:
// 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>
If there is no efficient way to handle this state, it may suggest using a publish/subscribe system, but in practice Sharing data is more straightforward in many cases. If you want to use shared state, how do you do it? Let's look at some common methods.
Note: You will find the code for this section in the "main" branch of the example project on GitHub.
#Since migrating to Vue 3, I have completely migrated to using the Composition API, which I also used in this article TypeScript
Although this is not required in the example I showed. While you can share status in any way, I'm going to show you a few of the most common technical patterns I've found, each with their own pros and cons, so don't take anything I say here as only.
Technologies include:
Note: At the time of writing this article, Vuex 5 It's still in the RFC (request for comments) stage, so I want to prepare you for the arrival of Vuex5, but there is no working version of this option yet.
Let’s take a closer look...
Note: The code for this section is located in the "Factories" branch of the example project on GitHub.
The factory pattern only cares about the instances of the state you create. In this mode, you will return a function very similar to the start function in the Composition API. You'll create a scope and build the components you're looking for. For example:
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 }; }
You can ask the factory to create the part of the object you need:
// In Home.vue const { books, loadBooks } = BookFactory();
If we add a isBusy
tag to show when the network request occurs , the above code will not change, but you can decide where to display 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 }; }
In another view (vue?), you can just isBusy
Tags without knowing how other parts of the factory work:
// 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
更多编程相关知识,请访问:编程入门!!
The above is the detailed content of In-depth analysis of how to manage shared state in Vue 3? 【translate】. For more information, please follow other related articles on the PHP Chinese website!