如何整合Vuex和TypeScript

如何整合Vuex和TypeScript

Vuex 是 Vue 众所周知的状态管理库,而 TypeScript 为代码添加了数据类型以检测和避免错误,因此将二者结合使用是非常合理的,本文将向你展示如何做到这一点。

Vuex 是专为 Vue.js 设计的官方状态管理库。随着应用程序的扩展和组件数量的增加,处理共享状态变得越来越具有挑战性。为了应对这种复杂性,Vuex 应运而生。它提供了一种统一的方法来管理和更新状态,确保变更的一致性和可追溯性。

Vuex 的创建受到了其他生态系统的状态管理模式和实践的影响,如 React 社区的 Flux,但它是专门为与 Vue 无缝集成而构建的。

TypeScript 本质上是在 JavaScript 的基础上提供了一套有益的工具。它是由微软开发的 JavaScript 的强类型超集。TypeScript 在 JavaScript 中引入了静态类型,这意味着你可以指定一个变量只能保存特定的原始类型,如字符串、布尔、数字等。如果你指定了一个未指定的类型,TypeScript 编译器就会抛出一个错误。它还允许定义更复杂的类型,如接口和枚举。

编译时类型检查还有一个重要的优点,即在编译时而不是运行时会发现更多错误,这也意味着在生产中出现的错误更少。大多数 JavaScript 库也支持并兼容 TypeScript,包括增强集成开发环境(IDE)和代码编辑器的功能,为它们提供静态类型系统的信息。

TypeScript 还提供了其他丰富的功能,例如集成开发环境中的自动完成功能,以及将鼠标悬停在变量或函数上时显示的类型信息、预期参数、返回类型等。

与 TypeScript 集成的集成开发环境具有重构的额外优势。例如,当变量名称发生变化时,新名称会通过 TypeScript 类型检查在整个代码库中更新。

TypeScript 改善了开发人员的体验,Vuex 尤其受益于它使用定义的类型帮助塑造和构造状态,从而改善了整体状态管理体验。

设置环境

要将 Vuex 与 TypeScript 集成,您需要安装 Vue(如果尚未安装),然后使用以下命令创建一个新的 Vue 项目:

# Install Vue CLI globally
npm install -g @vue/cli
# Create a new project
vue create my-vue-ts-project

系统会提示你选择 Vue 项目所需的功能。选择 “Manually Select features” 选项,然后选择 Vuex 和 TypeScript。这将自动引导你的应用程序使用 TypeScript,并在运行中为你初始化一个 Vuex 存储。

继续安装后,用以下命令导航到你的项目:

# Install Vue CLI globally
cd my-vue-ts-project

您可以在自己选择的任何集成开发环境中打开新创建的文件夹。

TypeScript 基础知识

在继续将 TypeScript 与 Vue 结合使用之前,了解 TypeScript 的一些基本概念至关重要。TypeScript 与基础 JavaScript 的语法相似,但增加了静态类型等额外功能。这意味着变量的类型是在初始化时定义的。这有助于防止在编写代码时出错。下面将对一些基本概念进行解释:

自定义类型

通过 TypeScript,您可以在应用程序中定义自定义类型。这可确保您的对象严格类型化为您创建的任何自定义类型。例如:

type Person = {
name: string;
age: number;
};
const personA: Person = {};
// Type '{}' is missing the following properties from type 'Person': name, age

在此,您创建了一个自定义类型 Person,并发现给 Person 类型的变量赋值会导致错误,因为空对象不具有 nameage 属性。正确的代码如下所示:

type Person = {
name: string;
age: number;
};
const personA: Person = {
name: "John",
age: 20,
};
console.log(personA.name, personA.age); // John, 20

如果 Person 类型的变量同时具有 nameage 属性,TypeScript 不会抛出任何错误。

接口

接口与类型类似,但主要区别在于接口可用于定义类,而类型不可。下面是一个使用 TypeScript 接口的示例:

interface Person {
name: string;
age: number;
getName(): string;
}
class Student implements Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
getAge() {
return this.age;
}
}
const personA: Student = new Student("Nana", 20);
console.log(personA.getName(), personA.getAge());

在本例中,接口 Person 定义了类 Student。在此,您创建了 Student 类的实例,并使用其方法打印了 name 和 age 属性。

TypeScript 泛型

通过泛型,您可以编写可重复使用的代码,这些代码可用于具有相同形状的不同类型。下面是一个示例:

interface Shape {
length: number;
width: number;
}
class Rectangle implements Shape {
length: number;
width: number;
constructor(length: number, width: number) {
this.length = length;
this.width = width;
}
}
class Square implements Shape {
length: number;
width: number;
constructor(length: number) {
this.length = length;
this.width = length;
}
}
function getArea<T extends Shape>(shape: T): number {
return shape.length * shape.width;
}
const rectangle: Shape = new Rectangle(10, 5);
const rectangleArea = getArea(rectangle);
const square: Shape = new Square(7);
const squareArea = getArea(square);
console.log(rectangleArea, squareArea); // 50, 49

在上述代码中,定义了一个接口 Shape 。通用函数 getArea 用于计算任何类型 Shape 的面积。我们创建了两个独立的类 RectangleSquare,它们都实现了 Shape 接口(它们都是 Shape 的类型)。因此,只需使用一个 getArea 泛型函数,就能计算出 RectangleSquare 实例的面积。

现在,您已经了解了 TypeScript 的一些基本概念,接下来将开始应用这些概念,通过 Vuex 状态管理构建 Vue 应用程序。

开始

Vue-CLI 会自动为你创建一个 store 空间(如果你在添加项目时选择了 Vuex 作为附加功能)。否则,请在 src 目录中创建一个存储并添加一个 index.ts 文件。使用 npm i vuex 安装 Vuex。用以下代码替换 index.ts 的内容:

import { createStore } from "vuex";
export interface State {
count: number;
}
export default createStore<State>({
state: { count: 0 },
getters: {},
mutations: {},
actions: {},
modules: {},
});

上述代码创建了一个名为 State 的接口。它定义了我们在 createStore 函数中使用的状态对象的形状。Vuex 中的 createStore 函数代表全局状态,以及如何在整个应用程序中访问该状态。请注意,通用的 createStore<State> 允许你定义状态的形状。删除 count:0 会导致错误,因为 state 对象与 State 接口不匹配。

要通过 Options API 使用 store,请转到 main.ts 并添加以下代码:

import { createApp } from "vue";
import App from "./App.vue";
import store, { State } from "./store";
import { Store } from "vuex";
declare module "@vue/runtime-core" {
interface ComponentCustomProperties {
$store: Store<State>;
}
}
createApp(App).use(store).mount("#app");

declare module 重新定义了 Vue 运行时的 ComponentCustomProperties 。这是访问 Vue 组件中 $store 属性所必需的。

用以下代码替换 HelloWorld.vueApp.vue 组件:

HelloWorld.vue

<template>
<div class="hello">
<p>count: {{ count }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
computed: {
count(): number {
return this.$store.state.count;
},
},
});
</script>

App.vue

<template>
<HelloWorld />
</template>
<script lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import { defineComponent } from "vue";
export default defineComponent({
components: { HelloWorld },
});
</script>

使用 npm run serve 运行服务器,并显示状态中的 count 属性(当前为 0)。

Vuex 突变

突变可更改存储在 Vuex 状态中的数据值。突变是一组可以访问状态数据并对其进行更改的函数。请注意,在 store/index.ts 中,你有一个 mutations 对象,目前是空的。

要使用 mutations,请将 store/index.ts 代码调整如下:

import { createStore } from "vuex";
export interface State {
count: number;
}
export default createStore<State>({
state: { count: 0 },
getters: {},
mutations: {
increment(state: State) {
state.count++;
},
},
actions: {},
modules: {},
});

上面的代码添加了一个 increment 突变,并将 State 接口作为参数。调用突变会更新状态的 count 属性。要在 HelloWorld.vue 组件中使用该代码,请将其替换为以下代码:

<template>
<div class="hello">
<p>count: {{ count }}</p>
<button @click="increment">Increase me</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
computed: {
count(): number {
return this.$store.state.count;
},
},
methods: {
increment() {
this.$store.commit("increment");
},
},
});
</script>

HelloWorld.vue 组件的 increment 方法会在调用时提交 Vuex 存储的 increment 突变。您将此方法附加到了模板中按钮的 click 事件。只要点击按钮,存储中的 count 属性值就会更新。

Vuex 动作

Vuex 动作是一组方法,可让您异步更新 Vuex 存储的值。Vuex 突变的设计是同步的,因此 Vuex 突变中的函数不宜是异步的。要创建 Vuex 操作,请在 store/index.ts 中输入以下代码:

import { createStore } from "vuex";
export interface State {
count: number;
}
export default createStore<State>({
state: { count: 0 },
getters: {},
mutations: {
increment(state: State) {
state.count++;
},
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => commit("increment"), 1000);
},
},
modules: {},
});

这将为 actions 对象添加一个 incrementAsync 函数。它使用 setTimeout 在一秒后调用 increment 操作。 { commit } 解构了提供给 Vuex 动作的 store 参数。这样就能以更短的方式提交状态。

要使用该操作,请用以下代码替换 HelloWorld.vue 组件:

<template>
<div class="hello">
<p>count: {{ count }}</p>
<button @click="increment">Increase me</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
computed: {
count(): number {
return this.$store.state.count;
},
},
methods: {
increment() {
this.$store.dispatch("incrementAsync");
},
},
});
</script>

您替换了 increment 函数,使用 Vuex 操作而不是直接提交状态。您会发现,点击按钮后,状态中的 count 会在 1 秒后更新。

Vuex 获取器

Vuex 获取器允许我们从原始状态计算派生状态。它们是只读的辅助函数,可让我们获取有关原始状态的更多信息。要使用 Vuex 获取器,请在 store/index.ts 中添加以下代码:

import { GetterTree, createStore } from "vuex";
export interface Getters extends GetterTree<State, State> {
doubleCount(state: State): number;
isEven(state: State): boolean;
}
const getters: Getters = {};
// Type '{}' is missing the following properties from type 'Getters': doubleCount, isEven

上面的代码为您的获取器定义了一个接口。它利用了 TypeScript 的强类型,以确保正确定义获取器。由于 getters 对象尚未完全实现以匹配 getters 接口,因此会出现错误。用以下代码完成代码:

import { GetterTree, createStore } from "vuex";
export interface State {
count: number;
}
export interface Getters extends GetterTree<State, State> {
doubleCount(state: State): number;
isEven(state: State): boolean;
}
const getters: Getters = {
doubleCount(state: State) {
return state.count * 2;
},
isEven(state: State) {
return state.count % 2 == 0;
},
};
export default createStore({
state: { count: 0 },
getters,
mutations: {
increment(state: State) {
state.count++;
},
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => commit("increment"), 1000);
},
},
modules: {},
});

代码实现了 getters 对象,并将其设置为 createStore 获取器中的 Vuex 获取器。继续在 HelloWorld.vue 组件中使用它,代码如下所示:

<template>
<div class="hello">
<p>count: {{ count }}</p>
<p>is even: {{ isEven }}</p>
<p>double of count: {{ double }}</p>
<button @click="increment">Increase me</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
computed: {
count(): number {
return this.$store.state.count;
},
isEven(): boolean {
return this.$store.getters.isEven;
},
double(): number {
return this.$store.getters.doubleCount;
},
},
methods: {
increment() {
this.$store.commit("increment");
},
},
});
</script>

isEven 用于确定 count 状态是否为偶数,而 doubleCount 用于计算计数值的两倍。

Vuex 模块

模块允许分离状态的各个部分,并允许分割不同的逻辑。它还能防止状态对象变得庞大而难以维护。要使用 Vuex 模块,请看下面的示例:

假设您想创建一个最小的社交媒体应用程序。为了管理用户、帖子和评论的状态,你可以使用如下的 Vuex 配置:

import { createStore } from "vuex";
interface User {
name: string;
}
interface Post {
id: string;
title: string;
content: string;
}
interface Comment {
postId: string;
comment: string;
}
export interface State {
user: User | null;
posts: Post[];
comments: Comment[];
}
export default createStore<State>({
state: { user: null, posts: [], comments: [] },
getters: {},
mutations: {
setUser(state: State, user: User) {
state.user = user;
},
addPost(state: State, post: Post) {
state.posts.push(post);
},
addComment(state: State, comment: Comment) {
state.comments.push(comment);
},
},
actions: {
login({ commit }, user) {
// Simulate user login
commit("setUser", user);
},
createPost({ commit }, post) {
// Simulate creating a post
commit("addPost", post);
},
createComment({ commit }, comment) {
// Simulate creating a comment
commit("addComment", comment);
},
},
});

即使没有真正实现, stateactions, 和 mutations 也已经显得很笨重。Vuex 模块有助于解决这一问题。使用 Vuex 模块重构的代码如下所示:

import { Module, createStore } from "vuex";
interface User {
name: string;
}
interface Post {
id: string;
title: string;
content: string;
}
interface Comment {
postId: string;
comment: string;
}
export interface State {
user: User | null;
posts: Post[];
comments: Comment[];
}
export interface UserModuleState {
user: User | null;
}
export interface PostModuleState {
posts: Post[];
}
export interface CommentModuleState {
comments: Comment[];
}
const userModule: Module<UserModuleState, State> = {
state: () => ({ user: null }),
mutations: {
setUser(state: UserModuleState, user: User) {
state.user = user;
},
},
actions: {
login({ commit }, user) {
// Simulate user login
commit("setUser", user);
},
},
};
const postModule: Module<PostModuleState, State> = {
state: () => ({ posts: [] }),
mutations: {
addPost(state: PostModuleState, post: Post) {
state.posts.push(post);
},
},
actions: {
createPost({ commit }, post) {
// Simulate creating a post
commit("addPost", post);
},
},
};
const commentModule: Module<CommentModuleState, State> = {
state: () => ({ comments: [] }),
mutations: {
addComment(state: CommentModuleState, comment: Comment) {
state.comments.push(comment);
},
},
actions: {
createComment({ commit }, comment) {
// Simulate creating a comment
commit("addComment", comment);
},
},
};
export default createStore<State>({
modules: {
userModule,
postModule,
commentModule,
},
});

您会发现, userpost, 和 comments 的逻辑被分成了不同的 Modules。每个模块都有自己的 stateactions, 和 mutations

建议将每个模块存储在各自独立的文件中,以便更好地分隔关注点,使每个模块的代码更小、更紧凑。

Vuex 模块还可以包含内部模块,在 Vuex 官方文档中可以探索到很多关于这一强大功能的内容。

Vuex 中使用的常见模式

探索一些最佳实践和实用策略,以增强您的 TypeScript 代码。这些技巧将指导您进行更易于维护的 TypeScript 开发。

辅助函数

主 store 中不必包含 actions 和 mutations 的函数。可以将 actionsmutation, 或 getters 的辅助函数分离到不同的模块中,然后从这些模块中导入。

Vuex 映射器

Vuex 提供的辅助函数可将 actionsmutations, 或 getters 直接映射到组件的 methods 或 computed中,而不是在组件中为每个动作或突变添加 methods。在前面的示例中,我们在组件的 methods 或计算 object 中调用了存储的 dispatch 或 commit 方法。

import { createStore } from "vuex";
export interface State {
count: 0;
}
export default createStore<State>({
state: { count: 0 },
mutations: {
increment(state: State) {
state.count++;
},
},
});

本代码是之前设置 Vuex 商店的示例,但您将在 HelloWorld.vue 组件中使用名为 mapMutationsmapState 的 Vuex 助手,如下所示:

<template>
<div class="hello">
<p>count: {{ count }}</p>
<button @click="increment">Increase me</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { mapMutations, mapState } from "vuex";
export default defineComponent({
computed: {
...mapState(["count"]),
},
methods: {
...mapMutations(["increment"]),
},
});
</script>

你没有创建一个计算属性来访问 this.$store.state 中的状态,而是使用了一个名为 mapState 的 Vuex 辅助函数来直接映射计算对象中的状态。您将要访问的状态属性名称( count )指定为列表中的字符串,并将其作为参数添加到 mapState 函数中。

同样,你也使用 Vuex mapMutations 对 increment 突变函数做了同样的操作。

潜在陷阱和解决方案

TypeScript 可确保更好的代码实践。您可能会遇到类似 TypeErrors 这样的问题,即您想使用的值与您需要的函数中的类型不匹配。快速的解决方案是将类型指定为 any ,这样就可以使用任何类型。注意不要过多使用,而是要确保接口定义清晰。

小结

在本文中,您探索了将 TypeScript 与 Vuex 集成的各种方法,了解了 TypeScript 的强类型系统的优势,以及它如何帮助您防患于未然。您还熟悉了什么是 Vuex 存储,以及 statesmutationsactions, 和 getters

最后,你还学会了如何在需要时使用 Vuex 模块拆分状态管理系统。

本文将作为一个平台,帮助您使用 Vuex 构建更简洁、更健壮的应用程序。使用 TypeScript 是一种强大的工具,可在错误成为大问题之前将其消除。

我们鼓励您在其官方文档中探索更多有关 Vuex 和 TypeScript 的内容,以便在构建更多项目的过程中充分利用其优势。

评论留言