在《基於 vite 創建 vue3 項目》一文中整合了 pinia,有不少伙伴不知道 pinia 是什麼,本文簡單介紹 pinia。主要包括三方面: pinia 的基本用法,在《基於 vite 創建 vue3 項目》中 demo 的基礎上簡單重構。 如何持久化 pinia 中的數據,保證瀏覽器刷新時 ...
在《基於 vite 創建 vue3 項目》一文中整合了 pinia,有不少伙伴不知道 pinia 是什麼,本文簡單介紹 pinia。主要包括三方面:
- pinia 的基本用法,在《基於 vite 創建 vue3 項目》中 demo 的基礎上簡單重構。
- 如何持久化 pinia 中的數據,保證瀏覽器刷新時,pinia 中的數據不丟失;
- 在 vue-router 路由守衛中如何使用 pinia。
文中的 demo 仍然基於 vite
1 pinia 的使用
1.1 pinia 是什麼
在 vue 2.x 中,vuex 是官方的狀態管理庫,並且 vue 3 中也有對應的 vuex 版本。但 vue 作者尤大神看了 pinia 後,強勢推薦使用 pinia 作為狀態管理庫。下圖是 vue 官網 “生態系統”,pinia 是 vue 生態之一。
1.2 pinia 的特點
- 支持 vue2 和 vue3,兩者都可以使用 pinia;
- 語法簡潔,支持 vue3 中 setup 的寫法,不必像 vuex 那樣定義 state、mutations、actions、getters 等,可以按照 setup Composition API 的方式返回狀態和改變狀態的方法,實現代碼的扁平化;
- 支持 vuex 中 state、actions、getters 形式的寫法,丟棄了 mutations,開發時候不用根據同步非同步來決定使用 mutations 或 actions,pinia 中只有 actions;
- 對 TypeScript 支持非常友好。
1.3 pinia 的使用
在《基於 vite 創建 vue3 項目》中已經整合了 pinia,現簡單回顧併進行一些調整。
- 安裝 pinia 依賴:
yarn add pinia
- 創建 pinia 實例(根存儲 root store):
之前咱是在 main.ts 中創建的,現將其抽取到獨立的文件中:
src/store/index.ts:
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
- 在 main.ts 中以插件的方式傳遞給 App 實例。
...
import pinia from '@/store'
...
app.use(pinia)
...
- 在 store/ 目錄下創建 modules 目錄,存儲每個模塊的狀態,將之前的 demo.ts 移動到 store/modules/ 中。這裡使用最新的 Composition API setup 的方式來定義狀態。
src/store/modules/demo.ts:
import { defineStore } from 'pinia'
import { ref } from 'vue'
const useDemoStore = defineStore('demo', () => {
const counter = ref(0)
const increment = () => {
counter.value++
}
return {
counter,
increment
}
})
export default useDemoStore
- 在組件 about.vue 中使用 demo 中的狀態 counter 和改變狀態的函數 increment。代碼和之前一樣。
先引入 demo.ts 中定義的 useDemoStore 函數,通過該函數創建 demoStore 實例。然後就可以調用 demoStore 的狀態 counter 和 increment 函數了。這裡需要註意,無論是 pinia 還是 vuex,通過解構的方式獲取狀態,會導致狀態失去響應性。如:
const { counter } = demoStore
此時的 counter 會丟失響應性,當其值改變時,其他組件不會監聽到。所以 pinia 提供了 storeToRefs 函數,使其解構出來的狀態仍然具備響應性。
const { counter } = storeToRefs(demoStore)
src/views/about.vue 完整代碼如下:
<template>
<div class="about">
<h1>This is an about page</h1>
<h3>counter: {{counter}}</h3>
<el-button @click="add">
<el-icon-plus></el-icon-plus>
</el-button>
<div>
<svg-icon icon="http://www.yygnb.com/demo/car.svg"></svg-icon>
<svg-icon icon="car"></svg-icon>
<svg-icon class-name="icon" icon="http://www.yygnb.com/demo/car.svg"></svg-icon>
<svg-icon class-name="icon" icon="car"></svg-icon>
</div>
</div>
</template>
<script lang="ts" setup>
import useDemoStore from '@/store/modules/demo'
import { storeToRefs } from 'pinia'
import SvgIcon from '@/components/svg-icon/index.vue'
const demoStore = useDemoStore()
const { counter } = storeToRefs(demoStore)
const add = () => {
demoStore.increment()
}
</script>
<style scoped>
.icon {
color: cornflowerblue;
font-size: 30px;
}
</style>
最後在瀏覽器中訪問 about 頁面,可以正常運行,點擊加號按鈕,計數器會加1。
2 持久化 pinia 狀態
2.1 為什麼需要持久化 pinia 狀態
在上面的 demo 中,假設計數器加到 5,如果刷新瀏覽器,counter 的值又會被初始化為 0。這是因為狀態是存儲在瀏覽器記憶體中的,刷新瀏覽器後,重新載入頁面時會重新初始化 vue、 pinia,而 pinia 中狀態的值僅在記憶體中存在,而刷新導致瀏覽器存儲中的數據沒了,所以 counter 的值就被初始化為 0。
在實際開發中,瀏覽器刷新時,有些數據希望是保存下來的。如用戶登錄後,用戶信息會存儲在全局狀態中,如果不持久化狀態,那麼每次刷新用戶都需要重新登錄了。
要解決這個問題非常簡單,在狀態改變時將其同步到瀏覽器的存儲中,如 cookie、localStorage、sessionStorage 。每次初始化狀態時從存儲中去獲取初始值即可。
說起來思路很簡單,可真正實現起來就各種問題了,所以咱們就使用 pinia 的插件 pinia-plugin-persistedstate 來實現。
2.2 pinia-plugin-persistedstate
接下來就使用 pinia-plugin-persistedstate 插件實現 pinia 狀態的持久化。
- 安裝依賴:
yarn add pinia-plugin-persistedstate
- 引入該插件,在創建 pinia 實例時傳入該插件
src/store/index.ts:
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia
- 在需要持久化狀態的模塊中設置 persist。咱假設 demo 模塊需要對狀態需要持久化,defineStore 第一個參數定義唯一的模塊名,第二個參數傳遞 setup,其實還有第三個參數 options,在 options 中便可開啟 persist:
src/store/modules/demo.ts:
...
const useDemoStore = defineStore('demo', () => {
...
}, {
persist: true
})
此時改變 counter 的值後,刷新瀏覽器,counter 不會被重置為 0,仍然停留在刷新前的狀態。
persist 支持多種類型的值,最簡單的就是傳遞 true,此時會將狀態緩存在 localStorage 中,該 localStorage 的 key 為模塊名(defineStore 的第一個參數),value 為該模塊的狀態對象,由於該模塊只有一個狀態 counter,故value為 {"counter":8}。如下圖:
如果需要將其存儲在 sessionStorage 中,就需要設置 persist 的值為一個對象:
...
const useDemoStore = defineStore('demo', () => {
...
}, {
persist: {
key: 'aaa',
storage: sessionStorage
}
})
此時狀態就會同步緩存到 sessionStorage 中,並且key 為咱們指定的 key:
persist 對象類型為 PersistedStateOptions,上面演示了 key 和 storage 屬性,該對象的其他屬性如下:
}
interface PersistedStateOptions {
/**
* Storage key to use.
* @default $store.id
*/
key?: string;
/**
* Where to store persisted state.
* @default localStorage
*/
storage?: StorageLike;
/**
* Dot-notation paths to partially save state. Saves everything if undefined.
* @default undefined
*/
paths?: Array<string>;
/**
* Customer serializer to serialize/deserialize state.
*/
serializer?: Serializer;
/**
* Hook called before state is hydrated from storage.
* @default null
*/
beforeRestore?: (context: PiniaPluginContext) => void;
/**
* Hook called after state is hydrated from storage.
* @default undefined
*/
afterRestore?: (context: PiniaPluginContext) => void;
}
3 在路由守衛中使用狀態
前面演示了在組件中使用 pinia,在組件外如何使用呢?這裡演示在全局路由守衛中獲取狀態值。咱們創建一個路由守衛,在路由守衛中使用 nprogress 顯示頁面載入進度條。
3.1 創建全局路由守衛
- 安裝 nprogress
yarn add nprogress
yarn add @types/nprogress -D
- 創建全局路由守衛
src/router/guard/index.ts:
import router from '@/router'
import nProgress from 'nprogress'
import 'nprogress/nprogress.css'
nProgress.configure({
showSpinner: false
})
// 全局前置守衛
router.beforeEach((to, from) => {
nProgress.start()
return true
})
// 全局後置鉤子
router.afterEach(() => {
nProgress.done(true)
})
- 在 main.ts 中引入全局路由守衛:
...
import '@/router/guard/index'
...
此時路由切換時,頁面頂部會出現載入進度條,路由切換完成時該進度條消失。如果效果不明顯,可在前置守衛中 setTimeout 查看效果(個人覺得沒這必要,畫蛇添足):
// 全局前置守衛
router.beforeEach((to, from) => {
nProgress.start()
return new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, 1000)
})
})
3.2 全局守衛中使用全局狀態
實際開發中,路由切換時,可能需要從全局狀態中獲取 token 等信息,判斷是否能進入下一個頁面。這裡演示路由切換時獲取 demo 中的 counter 的值。
首先試試在鉤子函數外面使用全局狀態:
...
import useDemoStore from '@/store/modules/demo'
import { storeToRefs } from 'pinia'
...
const demoStore = useDemoStore()
const { counter } = storeToRefs(demoStore)
// 全局前置守衛
router.beforeEach((to, from) => {
nProgress.start()
// 從 store 中獲取其他值,再決定返回值
// 這裡演示獲取 store 中 counter 的值
console.log(`counter:${counter}`)
return true
})
...
此時瀏覽器控制台會報如下錯誤,這是因為 pinia 還沒有掛載到 app 上。
網上有些解決方案是直接實例化一個 pinia 實例,傳遞給 useDemoStore() 函數,如下:
...
import useDemoStore from '@/store/modules/demo'
import { storeToRefs } from 'pinia'
import pinia from '@/store'
...
const demoStore = useDemoStore(pinia)
const { counter } = storeToRefs(demoStore)
...
這樣做,瀏覽器控制台不報錯了,頁面也可以正常載入,路由切換時,控制台會輸出當前 counter 的值。
但是如果刷新瀏覽器,counter 的值又被初始化為 0,貌似前面設置的持久化插件 pinia-plugin-persistedstate 失效了。那應該怎麼處理呢?
3.3 正確的處理方式
上面這種傳遞 pinia 對象給 useDemoStore() 函數只是一種野路子,pinia 官網已經清楚寫明組件外應該如何使用 pinia:
在鉤子函數外,pinia 還沒有被掛載,但是在前置守衛函數中,pinia 已經被掛載了,所以獲取全局狀態,需要在鉤子函數中進行,正確的實現如下:
import router from '@/router'
import nProgress from 'nprogress'
import 'nprogress/nprogress.css'
import useDemoStore from '@/store/modules/demo'
import { storeToRefs } from 'pinia'
nProgress.configure({
showSpinner: false
})
// 全局前置守衛
router.beforeEach((to, from) => {
nProgress.start()
const demoStore = useDemoStore()
const { counter } = storeToRefs(demoStore)
// 從 store 中獲取其他值,再決定返回值
// 這裡演示獲取 store 中 counter 的值
console.log(`counter:${counter.value}`)
return true
})
// 全局後置鉤子
router.afterEach(() => {
nProgress.done(true)
})
文中 demo 在 github 上搜索 vue3-vite-archetype,main 分支可以直接 yarn dev 啟動運行; template 分支是 yyg-cli 執行 yyg create 創建項目時拉取的模板。你也可以先執行 npm install -g yyg-cli 安裝 yyg-cli 腳手架工具,然後通過 yyg create xxx 創建項目,創建後的項目包含了 vue3 vite 的全部demo。