從 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) 核心概念

在 Vue 組件中,我們通過store.commit('方法名'), 來提交mutation需要注意的是,Mutation 必須是同步函數

Action 提交的是 mutation,而不是直接變更狀態.

Action 可以包含任意異步操作.

(2) 在組合式 API 中對TypeScript的支持

在使用組合式 API 編寫Vue組件時候,我們希望使用useStore返回類型化的store,流程大概如下:

  1. 定義類型化的 InjectionKey

  2. store 安裝到 Vue 應用時提供類型化的 InjectionKey

  3. 將類型化的 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 最大的特點是:簡便。

安裝與使用

安裝

yarn add pinia
# 或者使用 npm
npm install pinia

核心概念:

store: 使用defineStore()函數定義一個 store,第一個參數是應用程序中 store 的唯一 id. 裏面包含stategettersactions, 與 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()
const mainStore = useMainStore()
mainStore.$patch({
   name: '',
   counter: mainStore.counter++
 })

[2] $patch方法也可以接受一個函數來批量修改集合內部分對象的值

cartStore.$patch((state) => {
  state.counter++
  state.name = 'test'
})
mainStore.$state = { name: '', counter: 0 }
  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)
    },
    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
  },
}
    /// action 方法
    addAsyncTabs ({ state, commit, rootState, rootGetters }:ActionContext<TabsState, RootState>, tab:ITab): void {
      /// 通過rootGetters 訪問main的數據
        console.log('rootGetters[]=======', rootGetters['main/getCount'])
      }
    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 {}
  }
})
  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