编写大型 Vue 应用程序可能是一个挑战。
在 Vue 3 应用程序中使用共享状态可以是降低这种复杂性的解决方案。有许多常见的解决状态的解决方案。工厂、共享对象和使用 Vuex 等方法的优缺点。
Vuex 5 中可能会改变我们在 Vue 3 中使用共享状态的方式。
当我们开始一个简单的 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
};
},
当项目是显示数据的单页时(可能是对其进行排序或过滤),这可能会引人注目。但在这种情况下,该组件将获取每个请求的数据。如果你想保留它怎么办?这就是状态管理发挥作用的地方。由于网络连接通常很昂贵且有时不可靠,因此在浏览应用程序时最好保持这种状态。
另一个问题是组件之间的通信。虽然您可以使用事件和道具与直接的孩子-父母进行交流,但是当您的每个视图/页面都是独立的时,处理错误处理和忙碌标志等简单情况可能会很困难。例如,假设您连接了一个顶级控件来显示错误和加载动画:
// 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 上示例项目的“主”分支中找到此部分的代码。
Vue 3 中的共享状态
自从迁移到 Vue 3 后,我已经完全迁移到使用 Composition API。对于本文,我也使用了 TypeScript,尽管我向您展示的示例不需要它。虽然您可以以任何方式共享状态,但我将向您展示我发现最常用模式的几种技术。每个人都有自己的优点和缺点,所以不要把我在这里谈论的任何东西都当成教条。
工厂
注意:此部分的代码位于GitHub 上示例项目的“工厂”分支中。
工厂模式只是创建一个你关心的状态的实例。在此模式中,您返回一个与 Composition API 中的start函数非常相似的函数。您将创建一个范围并构建您正在寻找的组件。例如:
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()
而不是分配书籍)。
共享实例
本节的代码位于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 可以解救的地方。
VUEX 4
本节的代码位于GitHub 上示例项目的“Vuex4”分支中。
Vuex 是 Vue 的状态管理器。它是由核心团队构建的,但它作为一个单独的项目进行管理。Vuex 的目的是将状态与您想要对状态执行的操作分开。所有状态更改都必须通过 Vuex,这意味着它更复杂,但您可以防止意外状态更改。
Vuex 的想法是提供可预测的状态管理流程。Views 流向 Actions,Actions 反过来使用 Mutations 来改变 State,进而更新 View。通过限制状态更改的流程,您应该减少更改应用程序状态的副作用;因此更容易构建更大的应用程序。Vuex 有一个学习曲线,但有了这种复杂性,你就可以获得可预测性。
此外,Vuex 确实支持开发时间工具(通过 Vue 工具)与状态管理一起工作,包括一个称为时间旅行的功能。这允许您查看状态的历史记录并前后移动以查看它如何影响应用程序。
有时,Vuex 也很重要。
要将其添加到您的 Vue 3 项目中,您可以将包添加到项目中:
> npm i vuex
复制
或者,您也可以使用 Vue CLI 添加它:
> vue add vuex
通过使用 CLI,它将为您的 Vuex 商店创建一个起点,否则您需要手动将其连接到项目。让我们来看看这是如何工作的。
首先,您需要一个使用 Vuex 的 createStore 函数创建的状态对象:
import { createStore } from 'vuex'
export default createStore({
state: {},
mutations: {},
actions: {},
getters: {}
});
如您所见,商店需要定义多个属性。状态只是您希望授予应用程序访问权限的数据列表:
import { createStore } from 'vuex'
export default createStore({
state: {
books: [],
isBusy: false
},
mutations: {},
actions: {}
});
请注意,状态不应使用ref或反应式包装器。此数据与我们在共享实例或工厂中使用的共享数据类型相同。此存储将是您的应用程序中的单例,因此状态中的数据也将被共享。
接下来,让我们看一下操作。操作是您要启用的涉及状态的操作。例如:
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: {
setBusy: (state) => state.isBusy = true,
clearBusy: (state) => state.isBusy = false,
setBooks(state, books) {
state.books.splice(0, state.books.length, ...books);
}
},
变异函数总是传入状态对象,以便你可以改变该状态。在前两个示例中,您可以看到我们明确设置了状态。但是在第三个例子中,我们传入了要设置的状态。突变总是有两个参数:状态和调用突变时的参数。
要调用突变,您需要在存储中使用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如何要求操作的名称。有一些技巧可以使这不仅仅是使用魔术字符串,但我现在要跳过它。这种魔术字符串的使用是使用 Vuex 的限制之一。
虽然使用 commit 可能看起来像是一个不必要的包装器,但请记住,Vuex 不会让您改变状态,除非在突变内部,因此只有通过commit调用才会。
您还可以看到对setBooks的调用采用了第二个参数。这是调用突变的第二个参数。如果您需要更多信息,则需要将其打包成一个参数(目前 Vuex 的另一个限制)。假设您需要将一本书插入到图书列表中,您可以这样称呼它:
commit("insertBook", { book, place: 4 }); // object, tuple, etc.
复制
然后你可以解构成你需要的部分:
mutations: {
insertBook(state, { book, place }) => // ...
}
这优雅吗?不是真的,但它有效。
现在我们的 action 已经处理了变化,我们需要能够在我们的代码中使用 Vuex 存储。有两种方法可以进入商店。首先,通过使用应用程序(例如 main.ts/js)注册商店,您将可以访问一个集中式商店,您可以在应用程序的任何地方访问它:
// main.ts
import store from './store'
createApp(App)
.use(store)
.use(router)
.mount('#app')
请注意,这不是添加 Vuex,而是您正在创建的实际商店。添加后,您只需调用useStore
即可获取商店对象:
import { useStore } from "vuex";
export default defineComponent({
components: {
BookInfo,
},
setup() {
const store = useStore();
const books = computed(() => store.state.books);
// ...
复制
这很好用,但我更喜欢直接导入商店:
import store from "@/store";
export default defineComponent({
components: {
BookInfo,
},
setup() {
const books = computed(() => store.state.books);
// ...
复制
既然您可以访问 store 对象,那么如何使用它呢?对于状态,您需要使用计算函数包装它们,以便将更改传播到您的绑定:
export default defineComponent({
setup() {
const books = computed(() => store.state.books);
return {
books
};
},
});
要调用操作,您需要调用dispatch方法:
export default defineComponent({
setup() {
const books = computed(() => store.state.books);
onMounted(async () => await store.dispatch("loadBooks"));
return {
books
};
},
});
操作可以具有您在方法名称后添加的参数。最后,要更改状态,您需要像我们在 Actions 中所做的那样调用 commit。例如,我在 store 中有一个 paging 属性,然后我可以使用commit更改状态:
const incrementPage = () =>
store.commit("setPage", store.state.currentPage + 1);
const decrementPage = () =>
store.commit("setPage", store.state.currentPage - 1);
请注意,像这样调用它会引发错误(因为您无法手动更改状态):
const incrementPage = () => store.state.currentPage++;
const decrementPage = () => store.state.currentPage--;
这是这里的真正力量,我们希望控制状态更改的位置,并且不会产生在开发过程中进一步产生错误的副作用。
您可能对 Vuex 中大量移动的部分感到不知所措,但它确实可以帮助管理更大、更复杂的项目中的状态。我不会说你在每种情况下都需要它,但是会有一些大型项目在总体上对你有帮助。
Vuex 4 的一个大问题是在 TypeScript 项目中使用它还有很多不足之处。您当然可以制作 TypeScript 类型来帮助开发和构建,但这需要大量的移动部分。
这就是 Vuex 5 旨在简化 Vuex 在 TypeScript(以及一般的 JavaScript 项目)中的工作方式的地方。让我们看看它在下一次发布后将如何运作。
-
[[图片上传失败...(image-4bd89e-1622728607209)]
一个 让开发者的生活更轻松的 Angular Grid?
是的,有一个:Angular Grid 的 Kendo UI。得到一个网格!开始](https://cats.smashing.services/ball?uri=http://bit.ly/progress_panel_2)
VUEX 5
注意:此部分的代码位于GitHub 上示例项目的“Vuex5”分支中。
在撰写本文时,Vuex 5 还不是真的。这是一个 RFC(征求意见)。这是一个计划。这是讨论的起点。所以我在这里解释的很多内容可能会有所改变。但是为了让您为 Vuex 的变化做好准备,我想让您了解它的发展方向。因此,与此示例关联的代码不会构建。
Vuex 工作原理的基本概念自成立以来一直没有改变。随着 Vue 3 的引入,Vuex 4 的创建主要允许 Vuex 在新项目中工作。但是该团队正试图通过 Vuex 来查看真正的痛点并解决它们。为此,他们正在计划一些重要的改变:
- 没有更多的变化:动作可以改变状态(也可能是任何人)。
- 更好的 TypeScript 支持。
- 更好的多商店功能。
那么这将如何运作?让我们从创建商店开始:
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);
}
}
});
首先要看到的变化是每个商店现在都需要自己的密钥。这是为了允许您检索多个商店。接下来您会注意到状态对象现在是一个工厂(例如从函数返回,而不是在解析时创建)。并且不再有突变部分。最后,在动作内部,您可以看到我们正在访问状态作为this
指针上的属性。不再需要传递状态并承诺采取行动。这不仅有助于简化开发,还可以更轻松地推断 TypeScript 的类型。
要将 Vuex 注册到您的应用程序中,您将注册 Vuex 而不是您的全局商店:
import { createVuex } from 'vuex'
createApp(App)
.use(createVuex())
.use(router)
.mount('#app')
最后,要使用商店,您将导入商店然后创建它的一个实例:
import bookStore from "@/store";
export default defineComponent({
components: {
BookInfo,
},
setup() {
const store = bookStore(); // Generate the wrapper
// ...
请注意,从商店返回的是一个工厂对象,无论您调用工厂多少次,它都会返回该商店的实例。返回的对象只是一个具有作为一等公民的动作、状态和获取器的对象(带有类型信息):
onMounted(async () => await store.loadBooks());
const incrementPage = () => store.currentPage++;
const decrementPage = () => store.currentPage--;
您将在此处看到 state(例如currentPage
)只是简单的属性。而动作(例如loadBooks
)只是函数。您在这里使用商店的事实是一个副作用。您可以将 Vuex 对象视为一个对象并继续您的工作。这是 API 的重大改进。
需要指出的另一个重要变化是,您还可以使用类似 Composition API 的语法生成您的商店:
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 即将推出的内容。希望您现在拥有为自己的项目做出正确决策的知识。
网友评论