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 的內容,以便在構建更多專案的過程中充分利用其優勢。
評論留言