Pinia學習(個人筆記)

image.png

Pinia官方文檔:https://pinia.web3doc.top/introduction.html

為什么叫Pinia


Pinia(發音為 /pi?nj?/,類似于英語中的“peenya”)是最接近有效包名 pi?a(西班牙語中的pineapple)的詞。 菠蘿實際上是一組單獨的花朵,它們結合在一起形成多個水果。 與 Store 類似,每一家都是獨立誕生的,但最終都是相互聯系的。 它也是一種美味的熱帶水果,原產于南美洲。

安裝


npm install pinia

什么是 Store ?


一個 Store (如 Pinia)是一個實體,它持有未綁定到您的組件樹的狀態和業務邏輯。換句話說,它托管全局狀態。它有點像一個始終存在并且每個人都可以讀取和寫入的組件。它有三個概念stategettersactions 并且可以安全地假設這些概念等同于組件中的“數據”“計算”“方法”

提前預覽


import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
    // pinia中沒有mutations
    state: () => { // state相當于vue中的data
        return {
            count: 0
        }
    },
    getters: { // getters相當于vue中的computed
        doubleCount(state) {
            // return this.count * 2 也可以直接通過this訪問state
            return state.count * 2
        }
    },
    actions: { // actions相當于vue中的methods
        increment() {
            this.count++
        }
    }
})

正式開始


定義一個 Store

在深入了解核心概念之前,我們需要知道 Store 是使用 defineStore() 定義的,并且它需要一個唯一名稱,作為第一個參數傳遞:

import { defineStore } from 'pinia'

// useStore 可以是 useUser、useCart 之類的任何東西
// 第一個參數是應用程序中 store 的唯一 id
export const useStore = defineStore('main', {
  state: () => {
      return {
        count: 0
      }
  }
  // other options...
})

這個 name,也稱為 id,是必要的,Pinia 使用它來將 store 連接到 devtools。 將返回的函數命名為 use... 是跨可組合項的約定,以使其符合你的使用習慣。

使用 store

我們正在 定義 一個 store,因為在 setup() 中調用 useStore() 之前不會創建 store:

一旦 store 被實例化,你就可以直接在 store 上訪問 state、getters 和 actions 中定義的任何屬性。

import { useStore } from '@/stores/counter'
<script setup>
const store = useStore()
console.log(store.count) // store中定義的state, getters和actions的屬性和方法,都可以直接通過store實例訪問
</script>

注意:store 是一個用reactive 包裹的對象,這意味著不需要在getter 之后寫.value,但是,就像setup 中的props 一樣,我們不能對其進行解構

import { useStore } from '@/stores/counter'
<script setup>
const store = useStore()
// ? 這不起作用,因為它會破壞響應式
// 這和從 props 解構是一樣的
const { name, doubleCount } = store
</script>

想要對其進行解構同時保持其響應式,解決辦法如下:使用storeToRefs()。它將為任何響應式屬性創建 refs。

import { useStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
<script setup>
const store = useStore()
// `name` 和 `doubleCount` 是響應式引用
// 這也會為插件添加的屬性創建引用
// 但跳過任何 action 或 非響應式(不是 ref/reactive)的屬性
const { name, doubleCount } = storeToRefs(store)
</script>

State


state相當于vue組件中的data

大多數時候,state 是 store 的核心部分。 我們通常從定義應用程序的狀態開始。 在 Pinia 中,狀態被定義為返回初始狀態的函數。 Pinia 在服務器端和客戶端都可以工作。

import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
  // 推薦使用 完整類型推斷的箭頭函數
  state: () => {
    return {
      // 所有這些屬性都將自動推斷其類型
      counter: 0,
      name: 'Eduardo',
      isAdmin: true,
    }
  },
})

訪問state

默認情況下,您可以通過 store 實例訪問狀態來直接讀取和寫入狀態:

<script setup>
import { useStore } from '@/stores/counter'
const store = useStore()
store.counter++
</script>

重置狀態

您可以通過調用 store 上的 $reset() 方法將狀態 重置 到其初始值:

<script setup>
import { useStore } from '@/stores/counter'
const store = useStore()
store.$reset()
</script>

$patch改變狀態

除了直接用 store.counter++ 修改 store,你還可以調用 $patch 方法。 它允許您使用部分“state”對象同時應用多個更改:

store.$patch({
  counter: store.counter + 1,
  name: 'Abalam',
})

但是,使用這種語法應用某些突變非常困難或代價高昂:任何集合修改(例如,從數組中推送、刪除、拼接元素)都需要您創建一個新集合。 正因為如此,$patch 方法也接受一個函數來批量修改集合內部分對象的情況:

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

替換state

您可以通過將其 $state 屬性設置為新對象來替換 Store 的整個狀態:

store.$state = { counter: 666, name: 'Paimon' }

Getters


getters相當于vue組件中的computed

Getter 完全等同于 Store 狀態的 計算值。 它們可以用 defineStore() 中的 getters 屬性定義。 他們接收“狀態”作為第一個參數以鼓勵箭頭函數的使用:

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    doubleCount: (state) => state.counter * 2,
  },
})

大多數時候,getter 只會依賴狀態,但是,他們可能需要使用其他 getter。 正因為如此,我們可以在定義常規函數時通過 this 訪問到 整個 store 的實例, 但是需要定義返回類型(在 TypeScript 中)。

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    // 自動將返回類型推斷為數字
    doubleCount(state) {
      return state.counter * 2
    },
    // 返回類型必須明確設置
    doublePlusOne(): number {
      return this.counter * 2 + 1
    },
  },
})

然后你可以直接在 store 實例上訪問 getter

<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>
<script setup>
import { useStore } from '@/stores/counter'
const store = useStore()
</script>

訪問其他 getter

與計算屬性一樣,您可以組合多個 getter。 通過 this 訪問任何其他 getter

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    // 類型是自動推斷的,因為我們沒有使用 `this`
    doubleCount: (state) => state.counter * 2,
    doubleCountPlusOne() {
      // 自動完成 ?
      return this.doubleCount + 1
    },
  },
})

將參數傳遞給 getter

Getters 只是幕后的 computed 屬性,因此無法向它們傳遞任何參數。 但是,您可以從 getter 返回一個函數以接受任何參數

export const useStore = defineStore('main', {
  getters: {
    getUserById: (state) => {
      // 接收userId參數
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

并在組件中使用:

<template>
  <p>User 2: {{ store.getUserById(2) }}</p>
</template>
<script setup>
    const store = useStore()
</script>

訪問其他 Store 的getter

要使用其他存儲 getter,您可以直接在 getter 內部使用它:

import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})

Actions


actions 相當于vue組件中的 methods

Actions 相當于組件中的 methods。 它們可以使用 defineStore() 中的 actions 屬性定義,并且它們非常適合定義業務邏輯

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  actions: {
    increment() {
      // 可以通過this訪問整個store實例
      this.counter++
    },
    randomizeCounter() {
      this.counter = Math.round(100 * Math.random())
    },
  },
})

Actions 像 methods 一樣被調用:

<script setup>
    import { useStore } from '@/stores/counter'
    const main = useStore()
    // Actions 像 methods 一樣被調用:
    main.randomizeCounter()
</script>

getters 一樣,操作可以通過 this 訪問 whole store instance 并提供完整類型(和自動完成?)支持。 與它們不同,actions 可以是異步的,您可以在其中await 任何 API 調用甚至其他操作! 這是使用 Mande 的示例。 請注意,只要您獲得“Promise”,您使用的庫并不重要,您甚至可以使用瀏覽器的“fetch”函數:

import { mande } from 'mande'

const api = mande('/api/users')

export const useUsers = defineStore('users', {
  state: () => ({
    userData: null,
    // ...
  }),

  actions: {
    async registerUser(login, password) {
      try {
        this.userData = await api.post({ login, password })
        // do something...
      } catch (error) {
        return error
      }
    },
  },
})

訪問其他 store 操作

要使用另一個 store ,您可以直接在action內部使用它:

import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    // ...
  }),
  actions: {
    async fetchUserPreferences(preferences) {
      // 在一個action內部訪問其他store
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

訂閱 Actions

可以使用 store.$onAction() 訂閱 action 及其結果。 傳遞給它的回調在 action 之前執行。 after 處理 Promise 并允許您在 action 完成后執行函數(類似then)。 以類似的方式,onError 允許您在處理中拋出錯誤(類似catch)。

這是一個在運行 action 之前和它們 resolve/reject 之后記錄的示例。

const unsubscribe = someStore.$onAction(
  ({
    name, // action 的名字
    store, // store 實例
    args, // 調用這個 action 的參數
    after, // 在這個 action 執行完畢之后,執行這個函數
    onError, // 在這個 action 拋出異常的時候,執行這個函數
  }) => {
    // 記錄開始的時間變量
    const startTime = Date.now()
    // 這將在 `store` 上的操作執行之前觸發
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 如果 action 成功并且完全運行后,after 將觸發。
    // 它將等待任何返回的 promise
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // 如果 action 拋出或返回 Promise.reject ,onError 將觸發
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// 手動移除訂閱
unsubscribe()

默認情況下,action subscriptions 綁定到添加它們的組件(如果 store 位于組件的 setup() 內)。 意思是,當組件被卸載時,它們將被自動刪除。 如果要在卸載組件后保留它們,請將 true 作為第二個參數傳遞給當前組件的 detach action subscription:

<script setup>
    const someStore = useSomeStore()
    // 此訂閱將在組件卸載后保留
    someStore.$onAction(callback, true)
</script>

Plugins插件


由于是底層 API,Pania Store可以完全擴展。 以下是您可以執行的操作列表:

  • 向 Store 添加新屬性
  • 定義 Store 時添加新選項
  • 為 Store 添加新方法
  • 包裝現有方法
  • 更改甚至取消操作
  • 實現本地存儲等副作用
  • 僅適用于特定 Store

使用 pinia.use() 將插件添加到 pinia 實例中。 最簡單的例子是通過返回一個對象為所有Store添加一個靜態屬性:

import { createPinia } from 'pinia'

// 為安裝此插件后創建的每個store添加一個名為 `secret` 的屬性
// 這可能在不同的文件中
function SecretPiniaPlugin() {
  return { secret: 'the cake is a lie' }
}

const pinia = createPinia()
// 將插件提供給 pinia
pinia.use(SecretPiniaPlugin)

// 在另一個文件中
const store = useStore()
store.secret // 'the cake is a lie'
image.png

secret屬性就被添加到了所有的store上面。這對于添加全局對象(如路由器、模式或 toast 管理器)很有用。

插件介紹

Pinia 插件是一個函數,可以選擇返回要添加到 store 的屬性。 它需要一個可選參數,一個 context:

export function myPiniaPlugin(context) {
  context.pinia // 使用 `createPinia()` 創建的 pinia
  context.app // 使用 `createApp()` 創建的當前應用程序(僅限 Vue 3)
  context.store // 插件正在擴充的 store
  context.options // 定義存儲的選項對象傳遞給`defineStore()`
  // ...
}

// 解構寫法
export function myPiniaPlugin({ pinia, app, store, options }) {
  // ...
}

然后使用 pinia.use() 將此函數傳遞給 pinia:

pinia.use(myPiniaPlugin)

插件僅適用于在將pinia傳遞給應用程序后創建的 store,否則將不會被應用。

擴充 store

您可以通過簡單地在插件中返回它們的對象來為每個 store 添加屬性:

pinia.use(() => ({ hello: 'world' }))

您也可以直接在 store 上設置屬性,但如果可能,請使用return版本,以便 devtools 可以自動跟蹤它們

pinia.use(({ store }) => {
  store.hello = 'world'
})

插件的任何屬性 returned 都會被devtools自動跟蹤,所以為了讓hello在devtools中可見,如果你想調試它,請確保將它添加到store._customProperties僅在開發模式 開發工具:

// 從上面的例子
pinia.use(({ store }) => {
  store.hello = 'world'
  // 確保您的打包器可以處理這個問題。 webpack 和 vite 應該默認這樣做
  if (process.env.NODE_ENV === 'development') {
    // 添加您在 store 中設置的任何 keys
    store._customProperties.add('hello')
  }
})

請注意,每個 store 都使用 reactive包裝,自動展開任何 Ref (ref(), computed() , ...), 它包含了:

const sharedRef = ref('shared')
pinia.use(({ store }) => {
  // 每個 store 都有自己的 `hello` 屬性
  store.hello = ref('secret')
  // 它會自動展開
  store.hello // 'secret'

  // 所有 store 都共享 value `shared` 屬性
  store.shared = sharedRef
  store.shared // 'shared'
})

這就是為什么您可以在沒有 .value 的情況下訪問所有計算屬性以及它們是響應式的原因

添加新狀態state

如果您想將新的狀態屬性添加到 store 或打算在 hydration 中使用的屬性,您必須在兩個地方添加它

  • store 上,因此您可以使用 store.myState 訪問它
  • store.$state 上,因此它可以在 devtools 中使用,并且在 SSR 期間被序列化

請注意,這允許您共享 refcomputed 屬性:

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

  const hasError = ref(false)
  store.$state.hasError = hasError
  // 這個必須始終設置
  store.hasError = toRef(store.$state, 'hasError')

  // 在這種情況下,最好不要返回 `hasError`,因為它
  // 將顯示在 devtools 的 `state` 部分
  // 無論如何,如果我們返回它,devtools 將顯示它兩次。
})

請注意,插件中發生的狀態更改或添加(包括調用store.$patch())發生在存儲處于活動狀態之前,因此不會觸發任何訂閱

添加新的外部屬性

當添加外部屬性、來自其他庫的類實例或僅僅是非響應式的東西時,您應該在將對象傳遞給 pinia 之前使用 markRaw() 包裝對象。 這是一個將路由添加到每個 store 的示例:

import { markRaw } from 'vue'
// 根據您的路由所在的位置進行調整
import { router } from './router'

pinia.use(({ store }) => {
  store.router = markRaw(router)
})

在插件中調用 $subscribe

您也可以在插件中使用 store.$subscribestore.$onAction

pinia.use(({ store }) => {
  store.$subscribe(() => {
    // 在存儲變化的時候執行
  })
  store.$onAction(() => {
    // 在 action 的時候執行
  })
})

添加新選項

可以在定義 store 時創建新選項,以便以后從插件中使用它們。 例如,您可以創建一個 debounce 選項,允許您對任何操作進行去抖動:

defineStore('search', {
  actions: {
    searchContacts() {
      // ...
    },
  },

  // 稍后將由插件讀取
  debounce: {
    // 將動作 searchContacts 防抖 300ms
    searchContacts: 300,
  },
})

然后插件可以讀取該選項以包裝操作并替換原始操作:

// 使用任何防抖庫
import debounce from 'lodash/debunce'

pinia.use(({ options, store }) => {
  // 可以通過options訪問新選項
  if (options.debounce) {
    // 我們正在用新的action覆蓋這些action
    return Object.keys(options.debounce).reduce((debouncedActions, action) => {
      debouncedActions[action] = debounce(
        store[action],
        options.debounce[action]
      )
      return debouncedActions
    }, {})
  }
})

請注意,使用設置語法時,自定義選項作為第三個參數傳遞:

defineStore(
  'search',
  () => {
    // ...
  },
  {
    // 稍后將由插件讀取
    debounce: {
      // 將動作 searchContacts 防抖 300ms
      searchContacts: 300,
    },
  }
)

遇到的問題匯總


  1. 在封裝的axios請求文件中訪問store,提示Uncaught ReferenceError: Cannot access 'store' before initialization
import { useStore } from '@/store'
const store = useStore()
class Http {
    constructor(apiList) {
        this.instance = axios.create({
            baseURL: config.baseUrl,
            headers: {
                'Authorization': store.token // --> 此處報錯
            }
        })
        // ...
    }
}

錯誤原因:store在pinia安裝之前使用了。
解決辦法:

The easiest way to ensure this is always applied is to defer calls of useStore() by placing them inside functions that will always run after pinia is installed.


確保始終應用此功能的最簡單方法是通過將useStore()的調用放在pinia安裝后始終運行的函數中來延遲調用。

于是把store放在請求攔截函數中訪問:

import { useStore } from '@/store'

class Http {
    constructor(apiList) {
        this.instance = axios.create({
            baseURL: config.baseUrl
        })
        this.instance.interceptors.request.use(function (config) {
            // 在發送請求之前做些什么, 比如修改headers-->config.headers.Authorization = 'xxx'
            const store = useStore()
            config.headers = {
                'Authorization': store.token
            }
            return config; // 必須要return config
        }, function (error) {
            // 對請求錯誤做些什么
            return Promise.reject(error);
        });
    }
}

等待繼續補充......

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容