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
类型的变量赋值会导致错误,因为空对象不具有 name
和 age
属性。正确的代码如下所示:
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 接口的示例:
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
的面积。我们创建了两个独立的类 Rectangle
和 Square
,它们都实现了 Shape
接口(它们都是 Shape
的类型)。因此,只需使用一个 getArea
泛型函数,就能计算出 Rectangle
和 Square
实例的面积。
现在,您已经了解了 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.vue
和 App.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); }, }, });
即使没有真正实现, state
, actions
, 和 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, }, });
您会发现, user
, post
, 和 comments
的逻辑被分成了不同的 Modules
。每个模块都有自己的 state
, actions
, 和 mutations
。
建议将每个模块存储在各自独立的文件中,以便更好地分隔关注点,使每个模块的代码更小、更紧凑。
Vuex 模块还可以包含内部模块,在 Vuex 官方文档中可以探索到很多关于这一强大功能的内容。
Vuex 中使用的常见模式
探索一些最佳实践和实用策略,以增强您的 TypeScript 代码。这些技巧将指导您进行更易于维护的 TypeScript 开发。
辅助函数
主 store
中不必包含 actions
和 mutations
的函数。可以将 actions
, mutation
, 或 getters
的辅助函数分离到不同的模块中,然后从这些模块中导入。
Vuex 映射器
Vuex 提供的辅助函数可将 actions
, mutations
, 或 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
组件中使用名为 mapMutations
和 mapState
的 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 存储,以及 states
, mutations
, actions
, 和 getters
。
最后,你还学会了如何在需要时使用 Vuex 模块拆分状态管理系统。
本文将作为一个平台,帮助您使用 Vuex 构建更简洁、更健壮的应用程序。使用 TypeScript 是一种强大的工具,可在错误成为大问题之前将其消除。
我们鼓励您在其官方文档中探索更多有关 Vuex 和 TypeScript 的内容,以便在构建更多项目的过程中充分利用其优势。
评论留言