除了 Vuex ,還有更好的選擇 Pinia

來自:掘金,作者:凱哥愛吃皮皮蝦
鏈接:https://juejin.cn/post/7068113574043844622
Pinia
pinia 目前已經(jīng)是 vue 官方正式的狀態(tài)庫。適用于 vue2 和 vue3,本文只描述vue3的寫法。

pinia 的優(yōu)勢
相對于以前的 vuex,pinia具有以下優(yōu)勢
更簡單的寫法,代碼更清晰簡潔,支持? composition api?和?options api?語法更完善的 typescript 支持,無需創(chuàng)建自定義復雜的包裝類型來支持 TypeScript,所有內(nèi)容都是類型化的,并且 API 的設計方式盡可能利用 TS 類型推斷 非常輕量,只有1kb的大小 不需要再注入魔法字符串等進行調用
安裝
yarn?add?pinia
//?or
npm?install?pinia
定義、使用store
創(chuàng)建一個 pinia 并傳遞給 vue 應用
import?{?createPinia?}?from? pinia
import?{?createApp?}?from? vue
import?App?from? ./app.vue
createApp(App).use(createPinia()).mount( #app )
定義store
store的定義是通過 defineStore 這個函數(shù),
它需要一個唯一的名稱,該名稱可以作為第一個參數(shù)傳遞,也可以用 id 熟悉傳遞。
import?{?defineStore?}?from? pinia
export?const?useMainStore?=?defineStore( main ,?{
??//?other?options...
})
import?{?defineStore?}?from? pinia
export?const?useMainStore?=?defineStore({
??id:? main
??//?other?options...
})
該 id 是必要的,主要是用于 vue devtools
使用store
import?{?useMainStore?}?from? @/stores/main
export?default?defineComponent({
??setup()?{
????const?store?=?useMainStore()
????return?{
??????store,
????}
??},
})
上述代碼中,useMainStore實例化后的,我們就可以在 store 上訪問 state、getters、actions 等(pinia中沒有mutations)。
該 store 是一個 reactive 對象,所以不需要 “.value”,也不能對其進行解構使用,否則失去響應性(類似 props)。
storeToRefs
如果一定要對其進行解構使用,可以使用 storeToRefs ,類似 vue3 中的?toRefs
import?{?storeToRefs?}?from? pinia
export?default?defineComponent({
??setup()?{
????const?store?=?useMainStore()
????const?{?user,?company?}?=?storeToRefs(store)
????return?{
??????user,?
??????company
????}
??},
})
state
定義state
在 pinia 中,定義 state 是在函數(shù)中返回 state 初始狀態(tài)
import?{?defineStore?}?from? pinia
const?useMainStore?=?defineStore( main ,?{
????state:?()?=>?({
????????teacherName:? 艾倫 ,
????????userList:?[
????????????{?name:? 小明 ,?age:?18?},
????????????{?name:? 小李 ,?age:?15?},
????????????{?name:? 小白 ,?age:?16?},
????????],
????}),
})
export?default?useMainStore
訪問state
可以通過store 實例直接訪問
import?useMainStore?from? @/store/main
export?default?defineComponent({
????setup()?{
????????const?mainStore?=?useMainStore()
????????const?teacherName?=?computed(()?=>?mainStore.teacherName)
????????const?userList?=?computed(()?=>?mainStore.userList)
????????return?{
????????????teacherName,
????????????userList,
????????}
????},
})
也可以直接修改狀態(tài)
import?useMainStore?from? @/store/main
export?default?defineComponent({
????setup()?{
????????const?mainStore?=?useMainStore()
????????function?change()?{
????????????mainStore.teacherName?=? 米利
????????????mainStore.userList.push({
????????????????name:? 小琪 ,
????????????????age:?19
????????????})
????????}
????????return?{
????????????change
????????}
????},
})
雖然可以直接修改,但是出于代碼結構來說,全局的狀態(tài)管理還是不要直接在各個組件處隨意修改狀態(tài),應放于 action 中統(tǒng)一方法修改(沒有mutation了)
重置狀態(tài)
可以通過調用store 上的方法將狀態(tài)重置為初始狀態(tài)
const?mainStore?=?useMainStore()
mainStore.$reset()
$patch
修改state還可以通過使用 $patch 方法
$patch 可以同時修改多個值,舉個例子
import?useMainStore?from? @/store/main
export?default?defineComponent({
????setup()?{
????????const?mainStore?=?useMainStore()
????????
??mainStore.$patch({
??????teacherName:? 德普 ,
????????????userList:?[
????????????????{?name:? 小明 ,?age:?18?},
????????????????{?name:? 小李 ,?age:?15?},
????????????]
??})
????????return?{}
????},
})
但是,這種寫法的在修改數(shù)組時,例如我只想要把 userList 的中第一項"小明"的age 改為 20,也需要傳入整個包括所有成員的數(shù)組,這無疑增加了書寫成本和風險,于是一般都推薦使用以下的傳入一個函數(shù)的寫法
mainStore.$patch((state)=>{
??state.teacherName?=? 德普
??state.userList[0].age?=?20
})
監(jiān)聽訂閱state
通過 store.$subscribe() 的方法,
該方法的第一個參數(shù)接受一個回調函數(shù),該函數(shù)可以在 state 變化時觸發(fā)
const?subscribe?=?mainStore.$subscribe((mutation,?state)?=>?{
????console.log(mutation)
????console.log(state)
})
如上所示,該回調函數(shù)的兩個參數(shù)
其中 state 是 mainStore 實例,而 mutation 打印如下

可以發(fā)現(xiàn),打印結果的mutation對象主要包含三個屬性
events : 是這次state改變的具體數(shù)據(jù),包括改變前的值和改變后的值等等數(shù)據(jù) storeId :是當前store的id type:type表示這次變化是通過什么產(chǎn)生的,主要有三個分別是 “direct” :通過 action 變化的 ”patch object“ :通過 $patch 傳遞對象的方式改變的 “patch function” :通過 $patch 傳遞函數(shù)的方式改變的
停止監(jiān)聽
上面代碼中,調用mainStore.$subscribe返回的值(即上方示例的 subscribe 變量)可以停止訂閱
subscribe()
store.$subscribe() 的方法的第二個參數(shù)options對象,是各種配置參數(shù),包括
detached屬性,其值是一個布爾值,默認是 false, 正常情況下,當 訂閱所在的組件被卸載時,訂閱將被停止刪除,如果設置detached值為 true 時,即使所在組件被卸載,訂閱依然可以生效。
其他屬性主要還有 immediate、deep、flush 等等,和 vue3 watch的對應參數(shù)效果一樣。
getter
定義getter
getter 是 store 中的 state 計算值,以defineStore中的getters屬性定義
getters屬性的值是一個函數(shù),該函數(shù)的第一個參數(shù)是 state
const?useMainStore?=?defineStore( main ,?{
????state:?()?=>?({
????????user:?{
????????????name:? 小明 ,
????????????age:?7,
????????},
????}),
????getters:?{
????????userInfo:?(state)?=>?`${state.user.name}今年${state.user.age}歲了`,
????????//?這里想要正確推斷參數(shù)?state?的類型,則定義?state?時需要使用箭頭函數(shù)定義
????},
})
上面代碼中,getters的值是箭頭函數(shù),當getters的值是普通函數(shù)時,可以通過 this 訪問整個store實例(如下)
但是如果是普通函數(shù),想要通過 this 獲取state的值并希望this的類型能正確推斷,同時希望函數(shù)的返回值類型正確推斷,我們需要聲明函數(shù)的返回類型。
getters:?{
????????userDesc:?(state)?=>?`${state.user.name}今年${state.user.age}歲了`,
????????????
????????userBesidesDesc():?string{?//?需注明類型
????????????return?`${this.user.age}歲的${this.user.name}`?//?可以使用?this?獲取值
????????},
????????????
????????returnUserInfo()?{
????????????return?this.userDesc?//?也可以使用?this?獲取其他getters
????????},????
},
訪問getter
import?useMainStore?from? @/store/main
export?default?defineComponent({
????setup()?{
????????const?mainStore?=?useMainStore()
????????const?userDesc?=?computed(()?=>?mainStore.userDesc)
????????const?userBesidesDesc?=?computed(()?=>?mainStore.userBesidesDesc)
????????const?returnUserInfo?=?computed(()?=>?mainStore.returnUserInfo)
????????return?{
????????????userDesc,
????????????userBesidesDesc,
????????????returnUserInfo,
????????}
????},
})
action
定義action
action 是 store 中的 方法,支持同步或異步。
action 定義的函數(shù)可以是普通函數(shù)從而可以通過 this 訪問整個store實例,同時該函數(shù)可以傳入任意參數(shù)并返回任何數(shù)據(jù)
const?useMainStore?=?defineStore( main ,?{
????state:?()?=>?({
????????count:?0,
????}),
????actions:?{
????????add()?{
????????????this.count++
????????},
????????
????????addCountNum(num:?number)?{
????????????this.count?+=?num
????????},
????},
})
調用action
setup()?{
????????const?mainStore?=?useMainStore()
????????function?mainAction()?{
????????????mainStore.addCount()
????????}
????
?????function?addCountTwo()?{
????????????mainStore.addCountNum(2)
????????}
????????return?{
????????????mainAction,
????????????addCountTwo
????????}
},
監(jiān)聽訂閱action
通過?store.$onAction(),可以監(jiān)聽action的動作及結果等
該函數(shù)可以接收一個回調函數(shù)作為參數(shù),回調函數(shù)的參數(shù)中有五個屬性,具體如下
const?unsubscribe?=?mainStore.$onAction(({
????name,?//?action?函數(shù)的名稱
????store,?//?store?實例,這里是?mainStore
????args,?//?action?函數(shù)參數(shù)數(shù)組
????after,?//?鉤子函數(shù),在action函數(shù)執(zhí)行完成返回或者resolves后執(zhí)行
????onError,?//?鉤子函數(shù),在action函數(shù)報錯或者rejects后執(zhí)行
})?=>?{})
舉個例子,
首先,定義一個store
import?{?defineStore?}?from? pinia
const?useMainStore?=?defineStore( main ,?{
????state:?()?=>?({
????????user:?{
????????????name:? 小明 ,
????????????age:?7,
????????},
????}),
????actions:?{
????????subscribeAction(name:?string,?age:?number,?manualError?:?boolean)?{
????????????return?new?Promise((resolve,?reject)?=>?{
????????????????console.log( subscribeAction函數(shù)執(zhí)行 )
????????????????if?(manualError)?{
????????????????????reject( 手動報錯 )
????????????????}?else?{
????????????????????this.user.name?=?name
????????????????????this.user.age?=?age
????????????????????resolve(`${this.user.name}今年${this.user.age}歲了`)
????????????????}
????????????})
????????},
????},
})
export?default?useMainStore
然后在 setup 中使用
import?useMainStore?from? @/store/main
import?{?ref,?defineComponent,?computed?}?from? vue
export?default?defineComponent({
????setup()?{
????????const?mainStore?=?useMainStore()
????????function?subscribeNormal()?{
????????????mainStore.subscribeAction( 小李 ,?18,?false)
????????}
????????
????????function?subscribeError()?{
????????????mainStore.subscribeAction( 小白 ,?17,?true)
????????}
????????const?unsubscribe?=?mainStore.$onAction(({
????????????name,?//?action?函數(shù)的名稱
????????????store,?//?store?實例,這里是?mainStore
????????????args,?//?action?函數(shù)參數(shù)數(shù)組
????????????after,?//?鉤子函數(shù),在action函數(shù)執(zhí)行完成返回或者resolves后執(zhí)行
????????????onError,?//?鉤子函數(shù),在action函數(shù)報錯或者rejects后執(zhí)行
????????})?=>?{
????????????console.log( action的函數(shù)名 ,?name)
????????????console.log( 參數(shù)數(shù)組 ,?args)
????????????console.log( store實例 ,?store)
????????????after((result)?=>?{
????????????????console.log( $onAction?after函數(shù) ,?result)
????????????})
????????????onError(error?=>?{
????????????????console.log( 錯誤捕獲 ,?error)
????????????})
????????})
????????return?{
????????????subscribeNormal,
????????????subscribeError,
????????}
????},
})
如上,在 setup 中,調用了 subscribeNormal 函數(shù)后,頁面打印如下

調用了 subscribeError 函數(shù)后,頁面打印如下

同樣,可以通過調用 mainStore.$onAction 返回的值來手動停止訂閱,在上面代碼的例子中,即是
unsubscribe()?//?手動停止訂閱
store.$onAction 默認在所在組件卸載時會被自動刪除,可以通過傳遞第二個參數(shù) true,來將action訂閱和所在組件分開(即組件卸載時,訂閱依然有效)
mainStore.$onAction(callback,?true)
store使用位置
在組件中使用時,useStore() 在大多數(shù)情況下都可以在調用后開箱即用。
在其他地方使用時,需確保在 pinia 激活使用后( app.use(createPinia()) )才能使用 useStore()
例如在路由守衛(wèi)中
import?{?createRouter?}?from? vue-router
import?useMainStore?from? @/store/main
const?router?=?createRouter({
??//?...
})
//?報錯
const?mainStore?=?useMainStore()
router.beforeEach((to)?=>?{
??//?正常使用
??const?mainStore?=?useMainStore()
})
在store中也可以訪問其他store
import?{?defineStore?}?from? pinia
import?{?useUserStore?}?from? ./user
export?const?useMainStore?=?defineStore( main ,?{
??getters:?{
????otherGetter(state)?{
??????const?userStore?=?useUserStore()
??????return?userStore.data?+?state.data
????},
??},
??actions:?{
????async?fetchUserInfo()?{
??????const?userStore?=?useUserStore()
??????if?(userStore.userInfo)?{
????????...
??????}
????},
??},
})
pinia插件
pinia store 支持擴展,通過 pinia 插件我們可以實現(xiàn)以下
給 store 添加新屬性
給 store 添加新選項
給 store 添加新方法
包裝已存在的方法
修改甚至刪除actions
...
例如可以寫一個簡單的插件來給所有store添加一個靜態(tài)屬性
import?{?createPinia?}?from? pinia
const?pinia?=?createPinia()
//?傳遞一個返回函數(shù)
pinia.use(()?=>?({?env:? dev ?}))
app.use(pinia)
然后,在所有其他的store都可以訪問到上面添加的 env 屬性
setup()?{
????????const?mainStore?=?useMainStore()
????????console.log(mainStore.env)?//?dev
}????????
插件函數(shù)
從上方代碼可以發(fā)現(xiàn),pinia 插件是一個函數(shù),這個函數(shù)有一個可選參數(shù)
import?{?PiniaPluginContext?}?from? pinia
function?myPiniaPlugin(context:?PiniaPluginContext)?{
????console.log(context)
}

context 打印出來主要有
app : 當前應用 Vue.createApp() 創(chuàng)建的 app options : defineStore 配置的數(shù)據(jù) pinia : 當前通過 createPinia() 創(chuàng)建的 pinia 實例 store :當前 store 實例
通過 context 我們可以在 store 上設置屬性
pinia.use(({?store?})?=>?{
????store.env?=? dev
})
這樣,在所有其他的store都可以訪問到上面添加的 env 屬性
pinia 的 store 是通過 reactive 包裝的,可以自動解包它包含的任何 ref 對象
pinia.use(({?store?})?=>?{
????store.env?=?ref( dev )
})
通過上面插件,訪問store 的 env 時不需要 .value,就可以直接訪問
setup()?{
????????const?mainStore?=?useMainStore()
????????console.log(mainStore.env)?//?不需要加?.value
}
添加外部屬性
當需要添加來自其他庫或不需要響應式的數(shù)據(jù)時,應該用 markRaw() 包裝傳遞的對象,例如
markRaw 來自 vue3,可以標記一個對象,使其永遠不會轉換為 proxy。返回對象本身。
import?{?markRaw?}?from? vue
import?{?router?}?from? ./router
import?{?axios?}?from? axios
pinia.use(({?store?})?=>?{
??store.router?=?markRaw(router)
??store.axios?=?markRaw(axios)
})
在插件內(nèi)部使用onAction
pinia.use(({?store?})?=>?{
??store.$subscribe(()?=>?{
????//?react?to?store?changes
??})
??store.$onAction(()?=>?{
????//?react?to?store?actions
??})
})
新屬性的typescript支持
當通過插件添加新屬性時,可以擴展?PiniaCustomProperties接口
可以用設置get,set或者簡單聲明值的類型,以此來安全地寫入和讀取新加的屬性
import? pinia
declare?module? pinia ?{
????export?interface?PiniaCustomProperties?{
????????set?env(value:?string?|?Ref<string>)
????????get?env():?string
????????//?或者
????????env:?string
????}
}
