還有人沒嘗過 Pinia 嗎,請收下這份食用指南!

語言: CN / TW / HK

theme: qklhk-chocolate highlight: atom-one-dark


本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

前言

Pinia ,發音為 /piːnjʌ/,來源於西班牙語 piña 。意思為菠蘿,表示與菠蘿一樣,由很多小塊組成。在 Pinia 中,每個 Store 都是單獨存在,一同進行狀態管理。

Pinia 是由 Vue.js 團隊成員開發,最初是為了探索 Vuex 下一次迭代會是什麼樣子。過程中,Pinia 實現了 Vuex5 提案的大部分內容,於是就取而代之了。

與 Vuex 相比,Pinia 提供了更簡單的 API,更少的規範,以及 Composition-API 風格的 API 。更重要的是,與 TypeScript 一起使用具有可靠的型別推斷支援。

Pinia 與 Vuex 3.x/4.x 的不同

  • mutations 不復存在。只有 state 、getters 、actions。
  • actions 中支援同步和非同步方法修改 state 狀態。
  • 與 TypeScript 一起使用具有可靠的型別推斷支援。
  • 不再有模組巢狀,只有 Store 的概念,Store 之間可以相互呼叫。
  • 支援外掛擴充套件,可以非常方便實現本地儲存等功能。
  • 更加輕量,壓縮後體積只有 2kb 左右。

既然 Pinia 這麼香,那麼還等什麼,一起用起來吧!

基本用法

安裝

js npm install pinia 在 main.js 中 引入 Pinia

```js // src/main.js import { createPinia } from 'pinia'

const pinia = createPinia() app.use(pinia) ```

定義一個 Store

src/stores 目錄下建立 counter.js 檔案,使用 defineStore() 定義一個 Store 。defineStore() 第一個引數是 storeId ,第二個引數是一個選項物件:

```js // src/stores/counter.js import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { doubleCount: (state) => state.count * 2 }, actions: { increment() { this.count++ } } }) ``` 我們也可以使用更高階的方法,第二個引數傳入一個函式來定義 Store :

```js // src/stores/counter.js import { ref, computed } from 'vue' import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => { const count = ref(0) const doubleCount = computed(() => count.value * 2) function increment() { count.value++ }

return { count, doubleCount, increment } }) ```

在元件中使用

在元件中匯入剛才定義的函式,並執行一下這個函式,就可以獲取到 store 了: ```js

``` 這就是基本用法,下面我們來介紹一下每個選項的功能,及外掛的使用方法。

State

解構 store

store 是一個用 reactive 包裹的物件,如果直接解構會失去響應性。我們可以使用 storeToRefs() 對其進行解構: ```

```

修改 store

除了可以直接用 store.count++ 來修改 store,我們還可以呼叫 $patch 方法進行修改。$patch 效能更高,並且可以同時修改多個狀態。

```

`` 但是,這種方法修改集合(比如從陣列中新增、刪除、插入元素)都需要建立一個新的集合,代價太高。因此,$patch` 方法也接受一個函式來批量修改:

cartStore.$patch((state) => { state.items.push({ name: 'shoes', quantity: 1 }) state.hasChanged = true })

監聽 store

我們可以通過 $subscribe() 方法可以監聽 store 狀態的變化,類似於 Vuex 的 subscribe 方法。與 watch() 相比,使用 $subscribe() 的優點是,store 多個狀態發生變化之後,回撥函式只會執行一次。

```js

``` 也可以監聽 pinia 例項上所有 store 的變化

```js // src/main.js import { watch } from 'vue' import { createPinia } from 'pinia'

const pinia = createPinia() watch( pinia.state, (state) => { // 每當狀態發生變化時,將所有 state 持久化到本地儲存 localStorage.setItem('piniaState', JSON.stringify(state)) }, { deep: true } ) ```

Getters

訪問 store 例項

大多數情況下,getter 只會依賴 state 狀態。但有時候,它會使用到其他的 getter ,這時候我們可以通過 this 訪問到當前 store 例項。

```js // src/stores/counter.js import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { doubleCount(state) { return state.count * 2 }, doublePlusOne() { return this.doubleCount + 1 } } }) ```

訪問其他 Store 的 getter

要使用其他 Store 的 getter,可以直接在 getter 內部使用:

```js // src/stores/counter.js import { defineStore } from 'pinia' import { useOtherStore } from './otherStore'

export const useCounterStore = defineStore('counter', { state: () => ({ count: 1 }), getters: { composeGetter(state) { const otherStore = useOtherStore() return state.count + otherStore.count } } }) ```

將引數傳遞給 getter

getter 本質上是一個 computed ,無法向它傳遞任何引數。但是,我們可以讓它返回一個函式以接受引數:

```js // src/stores/user.js import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', { state: () => ({ users: [{ id: 1, name: 'Tom'}, {id: 2, name: 'Jack'}] }), getters: { getUserById: (state) => { return (userId) => state.users.find((user) => user.id === userId) } } }) ``` 在元件中使用:

```

``` 注意:如果這樣使用,getter 不會快取,它只會當作一個普通函式使用。一般不推薦這種用法,因為在元件中定義一個函式,可以實現同樣的功能。

Actions

訪問 store 例項

與 getters 一樣,actions 可以通過 this 訪問當 store 的例項。不同的是,actions 可以是非同步的。

```js // src/stores/user.js import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', { state: () => ({ userData: null }), actions: { async registerUser(login, password) { try { this.userData = await api.post({ login, password }) } catch (error) { return error } } } }) ```

訪問其他 Store 的 action

要使用其他 Store 的 action,也可以直接在 action 內部使用:

```js // src/stores/setting.js import { defineStore } from 'pinia' import { useAuthStore } from './authStore'

export const useSettingStore = defineStore('setting', { state: () => ({ preferences: null }), actions: { async fetchUserPreferences(preferences) { const authStore = useAuthStore() if (authStore.isAuthenticated()) { this.preferences = await fetchPreferences() } else { throw new Error('User must be authenticated!') } } } }) ``` 以上就是 Pinia 的詳細用法,是不是比 Vuex 簡單多了。除此之外,外掛也是 Pinia 的一個亮點,個人覺得非常實用,下面我們就來重點介紹一下。

Plugins

由於是底層 API,Pania Store 完全支援擴充套件。以下是可以擴充套件的功能列表: - 向 Store 新增新狀態 - 定義 Store 時新增新選項 - 為 Store 新增新方法 - 包裝現有方法 - 更改甚至取消操作 - 實現本地儲存等副作用 - 僅適用於特定 Store

使用方法

Pinia 外掛是一個函式,接受一個可選引數 contextcontext 包含四個屬性:app 例項、pinia 例項、當前 store 和選項物件。函式也可以返回一個物件,物件的屬性和方法會分別新增到 state 和 actions 中。

js export function myPiniaPlugin(context) { context.app // 使用 createApp() 建立的 app 例項(僅限 Vue 3) context.pinia // 使用 createPinia() 建立的 pinia context.store // 外掛正在擴充套件的 store context.options // 傳入 defineStore() 的選項物件(第二個引數) // ... return { hello: 'world', // 為 state 新增一個 hello 狀態 changeHello() { // 為 actions 新增一個 changeHello 方法 this.hello = 'pinia' } } } 然後使用 pinia.use() 將此函式傳遞給 pinia 就可以了:

```js // src/main.js import { createPinia } from 'pinia'

const pinia = createPinia() pinia.use(myPiniaPlugin) ```

向 Store 新增新狀態

可以簡單地通過返回一個物件來為每個 store 新增狀態: js pinia.use(() => ({ hello: 'world' })) 也可以直接在 store 上設定屬性來新增狀態,為了使它可以在 devtools 中使用,還需要對 store.$state 進行設定:

```js import { ref, toRef } from 'vue'

pinia.use(({ store }) => { const hello = ref('word') store.$state.hello = hello store.hello = toRef(store.$state, 'hello') }) ``` 也可以在 use 方法外面定義一個狀態,共享全域性的 ref 或 computed

```js import { ref } from 'vue'

const globalSecret = ref('secret') pinia.use(({ store }) => { // secret 在所有 store 之間共享 store.$state.secret = globalSecret store.secret = globalSecret }) ```

定義 Store 時新增新選項

可以在定義 store 時新增新的選項,以便在外掛中使用它們。例如,可以新增一個 debounce 選項,允許對所有操作進行去抖動:

```js // src/stores/search.js import { defineStore } from 'pinia'

export const useSearchStore = defineStore('search', { actions: { searchContacts() { // ... }, searchContent() { // ... } },

debounce: { // 操作 searchContacts 防抖 300ms searchContacts: 300, // 操作 searchContent 防抖 500ms searchContent: 500 } }) 然後使用外掛讀取該選項,包裝並替換原始操作: // src/main.js import { createPinia } from 'pinia' import { debounce } from 'lodash'

const pinia = createPinia() pinia.use(({ options, store }) => { if (options.debounce) { // 我們正在用新的 action 覆蓋原有的 action return Object.keys(options.debounce).reduce((debouncedActions, action) => { debouncedActions[action] = debounce( store[action], options.debounce[action] ) return debouncedActions }, {}) } }) ``` 這樣在元件中使用 actions 的方法就可以去抖動了,是不是很方便!

實現本地儲存

相信大家使用 Vuex 都有這樣的困惑,F5 重新整理一下資料全沒了。在我們眼裡這很正常,但在測試同學眼裡這就是一個 bug 。Vuex 中實現本地儲存比較麻煩,需要把狀態一個一個儲存到本地,取資料時也要進行處理。而使用 Pinia ,一個外掛就可以搞定。

這次我們就不自己寫了,直接安裝開源外掛。

js npm i pinia-plugin-persist 然後引入外掛,並將此外掛傳遞給 pinia : ```js // src/main.js import { createPinia } from 'pinia' import piniaPluginPersist from 'pinia-plugin-persist'

const pinia = createPinia() pinia.use(piniaPluginPersist) ``` 接著在定義 store 時開啟 persist 即可:

```js // src/stores/counter.js import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', { state: () => ({ count: 1 }), // 開啟資料快取 persist: { enabled: true } }) ``` 這樣,無論你用什麼姿勢重新整理,資料都不會丟失啦!

預設情況下,會以 storeId 作為 key 值,把 state 中的所有狀態儲存在 sessionStorage 中。我們也可以通過 strategies 進行修改:

js // 開啟資料快取 persist: { enabled: true, strategies: [ { key: 'myCounter', // 儲存的 key 值,預設為 storeId storage: localStorage, // 儲存的位置,預設為 sessionStorage paths: ['name', 'age'], // 需要儲存的 state 狀態,預設儲存所有的狀態 } ] } ok,今天的分享就是這些。不知道你對 Pinia 是不是有了更進一步的瞭解,歡迎評論區留言討論。

小結

Pinia 整體來說比 Vuex 更加簡單、輕量,但功能卻更加強大,也許這就是它取代 Vuex 的原因吧。此外,Pinia 還可以在 Vue2 中結合 map 函式使用,有興趣的同學可以研究一下。

歡迎關注專欄 Vue3 特訓營 ,後面我會持續分享更多 Vue3 相關的內容。如果對你有所幫助,記得點個贊呦!💕

參考文件:Pinia 中文文件