Vuex是Vue的一个著名的状态管理库,而TypeScript为您的代码添加了数据类型,以便检测和避免错误,因此将两者一起使用是非常合理的,本文将向您展示如何做到这一点。
Vuex是为Vue.js开发的官方状态管理库。随着应用程序的扩展和组件数量的增加,处理共享状态变得越来越具有挑战性。为了应对这种复杂性,引入了Vuex。它提供了一种统一的方法来管理和更新状态,确保变化的一致性和可追溯性。
Vuex的创建受到了其他生态系统中的状态管理模式和实践的影响,比如React社区的Flux,但它专门为了与Vue无缝集成而构建。
TypeScript本质上是在JavaScript的基础上提供了一套有益的工具。它是由微软开发的一种强类型的JavaScript超集。TypeScript引入了静态类型到JavaScript中,这意味着你可以指定一个变量只能持有某种特定的原始类型,比如字符串、布尔值、数字等。如果你给变量赋予了未指定的类型,TypeScript编译器应该会抛出一个错误。它还允许定义更复杂的类型,比如接口和枚举。
编译时类型检查还有一个重要的优势,即在编译时捕获更多的错误,而不是在运行时,这也意味着在生产中有更少的错误。大多数JavaScript库也支持并与TypeScript兼容,包括增强集成开发环境(IDE)和代码编辑器的功能,从其静态类型系统中提供信息。
TypeScript还提供其他丰富的功能,例如在集成开发环境中的自动完成,以及在悬停在变量或函数上时提供的类型信息、预期参数、返回类型等。
与TypeScript集成的IDE具有重构的额外优势。例如,当变量名发生变化时,由于TypeScript类型检查,新名称会在整个代码库中得到更新。
TypeScript改善了开发者的体验,而Vuex特别受益于使用定义的类型来塑造和结构化状态,从而提高了整体状态管理的体验。
设置环境
要将Vuex与TypeScript集成,您需要先安装Vue(如果尚未安装),然后使用以下命令创建一个新的Vue项目:
代码语言:javascript复制npm install -g @vue/cli
# Create a new project
vue create my-vue-ts-project
您将被提示选择Vue项目所需的功能。选择“手动选择功能”选项,然后选择Vuex和TypeScript。这将自动为您的应用程序引导使用TypeScript,并即时为您初始化一个Vuex存储。
安装完成后,使用以下命令导航到您的项目:
代码语言:javascript复制# Install Vue CLI globally
cd my-vue-ts-project
您可以在您选择的任何集成开发环境中打开新创建的文件夹。
Typescript基础
在开始使用TypeScript与Vue之前,了解一些基本的TypeScript概念是必要的。TypeScript与基本的JavaScript语法相似,但添加了额外的功能,如静态类型。这意味着变量的类型在初始化时被定义。这有助于在编码过程中防止错误。下面给出了一些基本概念的解释:
自定义类型
TypeScript使您能够定义自定义类型,您可以在应用程序中使用这些类型。这确保了您的对象严格遵循您创建的任何自定义类型。例如:
代码语言:javascript复制type Person = {
name: string;
age: number;
};
const personA: Person = {};
// Type '{}' is missing the following properties from type 'Person': name, age
在这里,您创建了一个自定义类型 Person ,并发现将一个类型为 Person 的变量赋值会导致错误,因为一个空对象没有属性 name 和 age 。正确的代码如下所示:
代码语言:javascript复制type Person = {
name: string;
age: number;
};
const personA: Person = {
name: "John",
age: 20,
};
console.log(personA.name, personA.age); // John, 20
如果具有类型 Person 的变量同时具有属性 name 和 age ,TypeScript不会抛出任何错误。
接口
接口与类型相似,但一个关键区别是接口可以用来定义类,而类型不能。下面是使用TypeScript接口的示例:
代码语言:javascript复制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泛型
泛型允许您编写可重用的代码,可以应用于具有相同结构的不同类型。下面是一个示例:
代码语言:javascript复制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 的面积。创建了两个独立的类 Rectangle 和 Square ,它们实现了 Shape 接口(它们是 Shape 的类型)。因此,可以通过一个单一的 getArea 通用函数来计算 Rectangle 和 Square 实例的面积。
现在你已经学习了一些TypeScript的基本概念,你将开始将这些概念应用于使用Vuex状态管理构建Vue应用程序。
入门指南
Vue-CLI会自动为您创建一个 store (如果您在添加项目时选择了 Vuex 作为附加功能)。否则,请在 src 目录中创建一个store,并添加一个 index.ts 文件。同时使用 npm i vuex 安装Vuex。将 index.ts 的内容替换为以下代码:
代码语言:javascript复制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 并添加以下代码:
代码语言:javascript复制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.vue 和 App.vue 组件
HelloWorld.vue
代码语言:javascript复制<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
代码语言:javascript复制<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 Mutations
Mutations改变了存储在Vuex状态中的数据的值。突变是一组可以访问状态数据并对其进行更改的函数。请注意,在 store/index.ts 中,您有一个当前为空的 mutations 对象。
使用 mutations ,将 store/index.ts 代码调整为以下内容:
代码语言:javascript复制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 mutation,并将 State 接口作为参数。调用 mutation 会更新状态的 count 属性。要在 HelloWorld.vue 组件中使用它,请将代码替换为以下内容:
代码语言:javascript复制<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>
increment 方法是 HelloWorld.vue 组件的一种方法,每次调用时都会提交Vuex存储的 increment 变化。您将此方法附加到模板中按钮的 click 事件上。每次点击按钮时,存储中 count 属性的值都会更新。
Vuex Actions
Vuex的actions是一组方法,可以异步地更新Vuex存储的值。Vuex的mutations是同步的设计,不建议在Vuex的mutations中使用异步函数。要创建一个Vuex的action,请在您的 store/index.ts 中输入以下代码:
代码语言:javascript复制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: {},
});
这将一个 incrementAsync 函数添加到 actions 对象中。它使用 setTimeout 在一秒后调用 increment 动作。这个 { commit } 解构了提供给Vuex动作的 store 参数。这样可以更简洁地提交到状态。
使用该操作,将 HelloWorld.vue 组件替换为以下内容:
代码语言:javascript复制<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 action而不是直接提交到状态。你会注意到,点击按钮后,1秒钟后状态中的 count 会更新。
Vuex Getters
Vuex的getters允许我们从原始状态计算出派生状态。它们是只读的辅助函数,允许我们推导出关于原始状态的更多信息。要使用Vuex的getters,请将以下代码添加到 store/index.ts :
代码语言:javascript复制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 接口,所以会出现错误。请使用以下代码完成:
代码语言:javascript复制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 getters中的Vuex getters。继续在 HelloWorld.vue 组件中使用以下代码:
代码语言:javascript复制<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>
这添加了额外的计算方法,返回getter函数。 isEven 确定 count 状态是否为偶数, doubleCount 计算count的两倍值。
Vuex Modules
模块允许将状态的不同部分分离,并允许将不同的逻辑进行分段。它还可以防止状态对象变得庞大且难以维护。要使用Vuex模块,请按照以下示例进行操作:
考虑这样一个场景,你想要构建一个简单的社交媒体应用。为了管理用户、帖子和评论的状态,你可以有一个类似下面的Vuex配置:
代码语言:javascript复制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);
},
},
});
state , actions 和 mutations 即使没有实际实现,看起来已经很庞大了。Vuex模块有助于解决这个问题。下面是使用Vuex模块进行重构的代码示例:
代码语言:javascript复制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,
},
});
你会注意到 user 、 post 和 comments 的逻辑已经分别放在不同的 Modules 中。每个 Modules 都有自己的 state 、 actions 和 mutations 。
建议将每个模块存储在自己独立的文件中,以促进关注点分离和每个模块的更小、紧密相关的紧凑代码。
Vuex模块也可以包含内部模块,在官方Vuex文档中可以探索到很多有关这个强大功能的内容。
在Vuex中常用的模式
探索一些增强您的TypeScript代码的最佳实践和实用策略。这些技巧将指导您进行更易维护的TypeScript开发。
辅助函数
主要的 store 不必包含你的 actions 和 mutations 的功能。辅助函数可以分离到不同的模块中,并从那里导入。
Vuex Mappers
而不是在每个操作或 mutation的组件中添加 methods ,Vuex提供了帮助函数,直接将 actions 、 mutations 或 getters 映射到组件的 methods 或 computed 。在前面的示例中,我们在组件的 methods 或计算属性 object 中调用了存储的 dispatch 或 commit 方法。
代码语言:javascript复制import { createStore } from "vuex";
export interface State {
count: 0;
}
export default createStore<State>({
state: { count: 0 },
mutations: {
increment(state: State) {
state.count ;
},
},
});
这段代码是设置Vuex存储的先前示例,但您将在 mapMutations 和 mapState 组件中使用名为Vuex助手的工具,如下所示:
代码语言:javascript复制<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>
不要创建一个计算属性来访问状态,而是使用名为 mapState 的Vuex辅助函数直接将其映射到计算对象中。您在列表中将要访问的状态属性的名称( count )作为字符串指定,并将其作为参数添加到 mapState 函数中。
同样地,你对使用Vuex的 mapMutations 的 mutation 函数也做了同样的事情。
潜在的陷阱和解决方案
TypeScript确保更好的代码实践。你可能会遇到像 TypeErrors 这样的问题,即你想要使用的值与你需要的函数的类型不匹配。一个快速的解决方案是将你的类型指定为 any ,这将允许使用任何类型。但要小心不要过多地使用它,而是确保清晰的接口定义。
结束
在本文中,您探索了将TypeScript与Vuex集成的各种方法,并观察了TypeScript强类型系统的好处以及它如何在错误发生之前帮助预防错误。您还熟悉了Vuex存储库是什么,以及 states , mutations , actions 和 getters 。
最后,你学会了如何使用Vuex模块来拆分你的状态管理系统,以应对需要的情况。
本文旨在为您提供一个平台,以使用Vuex构建更干净、更健壮的应用程序。使用TypeScript作为一种强大的工具,可以在错误变成重大问题之前消除它们。
建议您在使用TypeScript构建更多项目的过程中,通过阅读Vuex的官方文档来深入了解并利用其优势。
由于文章内容篇幅有限,今天的内容就分享到这里,文章结尾,我想提醒您,文章的创作不易,如果您喜欢我的分享,请别忘了点赞和转发,让更多有需要的人看到。同时,如果您想获取更多前端技术的知识,欢迎关注我,您的支持将是我分享最大的动力。我会持续输出更多内容,敬请期待。