從 Vuex 到 Pinia
本文作者爲奇舞團前端研發工程師
從 Vuex 到 Pinia
一. 概述
在開發 Vue 項目時,我們一般使用Vuex
來進行狀態管理,但是在使用Vuex
時始終伴隨着一些痛點。比如:需要使用Provide/Inject
來定義類型化的InjectionKey
以便支持TypeScript
,模塊的結構嵌套、命名空間以及對新手比較難理解的流程規範等。Pinia
的出現很好的解決了這些痛點。本質上Pinia
也是Vuex
團隊核心成員開發的,在Vuex
的基礎上提出了一些改進。與Vuex
相比,Pinia
去除了Vuex
中對於同步函數Mutations
和異步函數Actions
的區分。並且實現了在Vuex5
中想要的大部分內容。
二. 使用
在介紹Pinia
之前我們先來回顧一下Vuex
的使用流程
1.Vuex
Vuex
是一個專爲Vue.js
應用程序開發的狀態管理庫。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。它主要用來解決多個組件狀態共享的問題。
主流程: 在 Store 中創建要共享的狀態 state,修改 state 流程:
Vue Compontents
dispatch Actions(在 Actions 中定義異步函數),Action commit Mutations,在 Mutations 中我們定義直接修改 state 的純函數,state 修改促使 Vue compontents 做響應式變化。
(1) 核心概念
-
State
: 就是組件所依賴的狀態對象。我們可以在裏面定義我們組件所依賴的數據。可以在 Vue 組件中通過this.$store.state.xxx
獲取 state 裏面的數據. -
Getter
: 從store
中的state
派生出的一些狀態,可以把他理解爲是store
的計算屬性. -
Mutation
: 更改store
中狀態的唯一方法是提交 mutation,我們通過在mutation
中定義方法來改變 state 裏面的數據.
在 Vue 組件中,我們通過
store.commit('方法名')
, 來提交mutation
。需要注意的是,Mutation 必須是同步函數。
Action:
action 類似於 mutation,不同在於:
Action 提交的是 mutation,而不是直接變更狀態.
Action 可以包含任意異步操作.
Module
: 當我們的應用較大時,爲了避免所有狀態會集中到一個比較大的對象中,Vuex
允許我們將 store 分割成模塊(module),你可以把它理解爲Redux
中的combineReducer
的作用.
(2) 在組合式 API 中對TypeScript
的支持
在使用組合式 API 編寫Vue
組件時候,我們希望使用useStore
返回類型化的store
,流程大概如下:
-
定義類型化的
InjectionKey
。 -
將
store
安裝到Vue
應用時提供類型化的InjectionKey
。 -
將類型化的
InjectionKey
傳給useStore
方法並簡化useStore
用法
// store.ts
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import { InjectionKey } from 'vue'
export interface IState {
count: number
}
// 1.定義 injection key
export const key: InjectionKey<Store<IState>> = Symbol()
export default createStore<IState>({
state: {
count: 0
},
mutations: {
addCount (state:IState) {
state.count++
}
},
actions: {
asyncAddCount ({ commit, state }) {
console.log('state.count=====>', state.count++)
setTimeout(() => {
commit('addCount')
}, 2000)
}
},
})
// 定義自己的 `useStore` 組合式函數
export function useStore () {
return baseUseStore(key)
}
main.ts
import { createApp } from 'vue'
import { store, key } from './store'
const app = createApp({ ... })
// 傳入 injection key
app.use(store, key)
app.mount('#app')
組件中使用
<script lang="ts">
import { defineComponent, toRefs } from 'vue'
import { useStore } from '../store'
export default defineComponent({
setup () {
const store = useStore()
const clickHandel = () => {
console.log('====>')
store.commit('addCount')
}
const clickAsyncHandel = () => {
console.log('====>')
store.dispatch('asyncAddCount')
}
return {
...toRefs(store.state),
clickHandel,
clickAsyncHandel
}
}
})
</script>
Pinia 的使用
基本特點
Pinia
同樣是一個 Vue 的狀態管理工具,在Vuex
的基礎上提出了一些改進。與 vuex 相比,Pinia
最大的特點是:簡便。
-
它沒有
mutation
, 他只有state
,getters
,action
, 在action
中支持同步與異步方法來修改state
數據 -
類型安全,與
TypeScript
一起使用時具有可靠的類型推斷支持 -
模塊化設計,通過構建多個存儲模塊,可以讓程序自動拆分它們。
-
非常輕巧,只有大約 1kb 的大小。
-
不再有
modules
的嵌套結構, 沒有命名空間模塊 -
Pinia
支持擴展,可以非常方便地通過本地存儲,事物等進行擴展。 -
支持服務器端渲染
安裝與使用
安裝
yarn add pinia
# 或者使用 npm
npm install pinia
核心概念:
store
: 使用defineStore()
函數定義一個 store,第一個參數是應用程序中 store 的唯一 id. 裏面包含state
、getters
和 actions
, 與 Vuex 相比沒有了Mutations
.
export const useStore = defineStore('main', {
state: () => {
return {
name: 'ming',
doubleCount: 2
}
},
getters: {
},
actions: {
}
})
注意:store 是一個用 reactive 包裹的對象,這意味着不需要在 getter 之後寫. value,但是,就像 setup 中的 props 一樣,我們不能對其進行解構.
export default defineComponent({
setup() {
const store = useStore()
// ❌ 這不起作用,因爲它會破壞響應式
// 這和從 props 解構是一樣的
const { name, doubleCount } = store
return {
// 一直會是 "ming"
name,
// 一直會是 2
doubleCount,
// 這將是響應式的
doubleValue: computed(() => store.doubleCount),
}
},
})
當然你可以使用computed
來響應式的獲取 state 的值(這與 Vuex 中需要創建computed
引用以保留其響應性類似), 但是我們通常的做法是使用storeToRefs
響應式解構 Store.
const store = useStore()
// 正確的響應式解構
const { name, doubleCount } = storeToRefs(store)
State
: 在 Pinia 中,狀態被定義爲返回初始狀態的函數.
import { defineStore } from 'pinia'
const useStore = defineStore('main', {
// 推薦使用 完整類型推斷的箭頭函數
state: () => {
return {
// 所有這些屬性都將自動推斷其類型
counter: 0,
name: 'Eduardo'
}
},
})
組件中 state 的獲取與修改:
在Vuex
中我們修改state
的值必須在mutation
中定義方法進行修改,而在pinia
中我們有多中修改 state 的方式.
- 基本方法:
const store = useStore()
store.counter++
- 重置狀態:
const store = useStore()
store.$reset()
- 使用
$patch
修改 state [1] 使用部分state
對象進行修改
const mainStore = useMainStore()
mainStore.$patch({
name: '',
counter: mainStore.counter++
})
[2] $patch
方法也可以接受一個函數來批量修改集合內部分對象的值
cartStore.$patch((state) => {
state.counter++
state.name = 'test'
})
- 替換 state 可以通過將其 $state 屬性設置爲新對象, 來替換
Store
的整個狀態:
mainStore.$state = { name: '', counter: 0 }
-
訪問其他模塊的
state
-
Vuex
中我們要訪問其他帶命名空間的模塊的 state 我們需要使用rootState
addAsyncTabs ({ state, commit, rootState, rootGetters }:ActionContext<TabsState, RootState>, tab:ITab): void {
/// 通過rootState 訪問main的數據
console.log('rootState.main.count=======', rootState.main.count)
if (state.tabLists.some(item => item.id === tab.id)) { return }
setTimeout(() => {
state.tabLists.push(tab)
}, 1000)
},
- Pinia 中訪問其他
store
的state
import { useInputStore } from './inputStore'
export const useListStore = defineStore('listStore', {
state: () => {
return {
itemList: [] as IItemDate[],
counter: 0
}
},
getters: {
},
actions: {
addList (item: IItemDate) {
this.itemList.push(item)
///獲取store,直接調用
const inputStore = useInputStore()
inputStore.inputValue = ''
}
})
Getter: Getter 完全等同於 Store 狀態的計算值.
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
// 自動將返回類型推斷爲數字
doubleCount(state) {
return state.counter * 2
},
// 返回類型必須明確設置
doublePlusOne(): number {
return this.counter * 2 + 1
},
},
})
如果需要使用
this
訪問到 整個store
的實例, 在TypeScript
需要定義返回類型. 在setup()
中使用:
export default {
setup() {
const store = useStore()
store.counter = 3
store.doubleCount // 6
},
}
-
訪問其他模塊的 getter
-
對於
Vuex
而言如果要訪問其他命名空間模塊的getter
,需要使用rootGetters
屬性
/// action 方法
addAsyncTabs ({ state, commit, rootState, rootGetters }:ActionContext<TabsState, RootState>, tab:ITab): void {
/// 通過rootGetters 訪問main的數據
console.log('rootGetters[]=======', rootGetters['main/getCount'])
}
Pinia
中訪問其他 store 中的 getter
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
Action:actions 相當於組件中的 methods, 使用defineStore()
中的 actions 屬性定義.
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
actions: {
increment() {
this.counter++
},
randomizeCounter() {
this.counter = Math.round(100 * Math.random())
},
},
})
pinia
中沒有mutation
屬性,我們可以在action
中定義業務邏輯,action
可以是異步的,可以在其中 await 任何 API 調用甚至其他操作.
...
//定義一個action
asyncAddCounter () {
setTimeout(() => {
this.counter++
}, 1000)
}
...
///setup()中調用
export default defineComponent({
setup() {
const main = useMainStore()
// Actions 像 methods 一樣被調用:
main.asyncAddCounter()
return {}
}
})
-
訪問其他 store 中的 Action
要使用另一個 store 中的 action ,可以直接在操作內部使用它:
import { useAuthStore } from './auth-store'
export const useSettingsStore = defineStore('settings', {
state: () => ({
// ...
}),
actions: {
async fetchUserPreferences(preferences) {
const auth = useAuthStore()
///調用其他store的action
if (auth.isAuthenticated()) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
在Vuex
中如果要調用另一個模塊的Action
,我們需要在當前模塊中註冊該方法爲全局的Action
,
/// 註冊全局Action
globalSetCount: {
root: true,/// 設置root 爲true
handler ({ commit }:ActionContext<MainState, RootState>, count:number):void {
commit('setCount', count)
}
}
在另一個模塊中對其進行dispatch
調用
/// 調用全局命名空間的函數
handelGlobalAction ({ dispatch }:ActionContext<TabsState, RootState>):void {
dispatch('globalSetCount', 100, { root: true })
}
三. 總結
與 Vuex[1] 相比,Pinia[2] 提供了一個更簡單的 API,具有更少的操作,提供Composition API
,最重要的是,在與TypeScript
一起使用時具有可靠的類型推斷支持,如果你正在開發一個新項目並且使用了TypeScript
,可以嘗試一下pinia
,相信不會讓你失望。
參考資料
[1]
Vuex: https://vuex.vuejs.org/zh/# 什麼是 "狀態管理模式"?
[2]
Pinia: https://pinia.web3doc.top/core-concepts/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1tUkWA1Df6k1lX8q5PW17g