Pinia 保姆級教程

作者:會飛的豬

https://zhuanlan.zhihu.com/p/533233367

Vue3 已經推出很長時間了,它周邊的生態也是越來越完善了。之前我們使用 Vue2 的時候,Vuex 可以說是必備的,它作爲一個狀態管理工具,給我們帶來了極大的方便。Vue3 推出後,雖然相對於 Vue2 很多東西都變了,但是核心的東西還是沒有變的,比如說狀態管理、路由等等。再 Vue3 種,尤大神推薦我們使用 pinia 來實現狀態管理,他也說 pinia 就是 Vuex 的新版本。

那麼 pinia 究竟是何方神聖,本篇文章帶大家一起學透它!

1.pinia 是什麼?

如果你學過 Vue2,那麼你一定使用過 Vuex。我們都知道 Vuex 在 Vue2 中主要充當狀態管理的角色,所謂狀態管理,簡單來說就是一個存儲數據的地方,存放在 Vuex 中的數據在各個組件中都能訪問到,它是 Vue 生態中重要的組成部分。

既然 Vuex 那麼重要,那麼在 Vue3 中豈能丟棄!

在 Vue3 中,可以使用傳統的 Vuex 來實現狀態管理,也可以使用最新的 pinia 來實現狀態管理,我們來看看官網如何解釋 pinia 的。

官網解釋:

Pinia 是 Vue 的存儲庫,它允許您跨組件 / 頁面共享狀態。

從上面官網的解釋不難看出,pinia 和 Vuex 的作用是一樣的,它也充當的是一個存儲數據的作用,存儲在 pinia 的數據允許我們在各個組件中使用。

實際上,pinia 就是 Vuex 的升級版,官網也說過,爲了尊重原作者,所以取名 pinia,而沒有取名 Vuex,所以大家可以直接將 pinia 比作爲 Vue3 的 Vuex。

2. 爲什麼要使用 pinia?

很多小夥伴內心是抗拒學習新東西的,比如我們這裏所說的 pinia,很多小夥伴可能就會拋出一系列的疑問:爲什麼要學習 pinia?pinia 有什麼優點嗎?既然 Vue3 還能使用 Vuex 爲什麼我還要學它?......

針對上面一系列的問題,我相信很多剛開始學習 pinia 的小夥伴都會有,包括我自己當初也有這個疑問。當然,這些問題其實都有答案,我們不可能平白無故的而去學習一樣東西吧!肯定它有自己的優點的,所以我們這裏先給出 pinia 的優點,大家心裏先有個大概,當你熟練使用它之後,在會過頭來看這些優點,相信你能理解。

優點:

pinia 的優點還有非常多,上面列出的主要是它的一些主要優點,更多細節的地方還需要大家在使用的時候慢慢體會。

3. 準備工作

想要學習 pinia,最好有 Vue3 的基礎,明白組合式 API 是什麼。如果你還不會 Vue3,建議先去學習 Vue3。

本篇文章講解 pinia 時,全部基於 Vue3 來講解,至於 Vue2 中如何使用 pinia,小夥伴們可以自行去 pinia 官網學習,畢竟 Vue2 中使用 pinia 的還是少數。

項目搭建:

我們這裏搭建一個最新的 Vue3 + TS + Vite 項目。

執行命令:

npm create vite@latest my-vite-app --template vue-ts

運行項目:

npm install
npm run dev

刪除 app.vue 中的其它無用代碼,最終頁面如下:

4.pinia 基礎使用

4.1 安裝 pinia

和 vue-router、vuex 等一樣,我們想要使用 pinia 都需要先安裝它,安裝它也比較簡單。

安裝命令:

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

安裝完成後我們需要將 pinia 掛載到 Vue 應用中,也就是我們需要創建一個根存儲傳遞給應用程序,簡單來說就是創建一個存儲數據的數據桶,放到應用程序中去。

修改 main.js,引入 pinia 提供的 createPinia 方法,創建根存儲。

代碼如下:

// main.ts


import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();


const app = createApp(App);
app.use(pinia);
app.mount("#app");

4.2 創建 store

store 簡單來說就是數據倉庫的意思,我們數據都放在 store 裏面。當然你也可以把它理解爲一個公共組件,只不過該公共組件只存放數據,這些數據我們其它所有的組件都能夠訪問且可以修改。

我們需要使用 pinia 提供的 defineStore() 方法來創建一個 store,該 store 用來存放我們需要全局使用的數據。

首先在項目 src 目錄下新建 store 文件夾,用來存放我們創建的各種 store,然後在該目錄下新建 user.ts 文件,主要用來存放與 user 相關的 store。

代碼如下:

/src/store/user.ts


import { defineStore } from 'pinia'


// 第一個參數是應用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
  // 其它配置項
})

創建 store 很簡單,調用 pinia 中的 defineStore 函數即可,該函數接收兩個參數:

我們可以定義任意數量的 store,因爲我們其實一個 store 就是一個函數,這也是 pinia 的好處之一,讓我們的代碼扁平化了,這和 Vue3 的實現思想是一樣的。

4.3 使用 store

前面我們創建了一個 store,說白了就是創建了一個方法,那麼我們的目的肯定是使用它,假如我們要在 App.vue 裏面使用它,該如何使用呢?

代碼如下:

/src/App.vue
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>

使用 store 很簡單,直接引入我們聲明的 useUsersStore 方法即可,我們可以先看一下執行該方法輸出的是什麼:

4.4 添加 state

我們都知道 store 是用來存放公共數據的,那麼數據具體存在在哪裏呢?前面我們利用 defineStore 函數創建了一個 store,該函數第二個參數是一個 options 配置項,我們需要存放的數據就放在 options 對象中的 state 屬性內。

假設我們往 store 添加一些任務基本數據,修改 user.ts 代碼。

代碼如下:

export const useUsersStore = defineStore("users", {
  state: () => {
    return {
      name: "小豬課堂",
      age: 25,
      sex: "男",
    };
  },
});

上段代碼中我們給配置項添加了 state 屬性,該屬性就是用來存儲數據的,我們往 state 中添加了 3 條數據。需要注意的是,state 接收的是一個箭頭函數返回的值,它不能直接接收一個對象。

4.5 操作 state

我們往 store 存儲數據的目的就是爲了操作它,那麼我們接下來就嘗試操作 state 中的數據。

4.5.1 讀取 state 數據

讀取 state 數據很簡單,前面我們嘗試過在 App.vue 中打印 store,那麼我們添加數據後再來看看打印結果:

這個時候我們發現打印的結果裏面多了幾個屬性,恰好就是我們添加的數據,修改 App.vue,讓這幾個數據顯示出來。

代碼如下:

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <p>姓名{{ name }}</p>
  <p>年齡{{ age }}</p>
  <p>性別{{ sex }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>

輸出結果:

上段代碼中我們直接通過 store.age 等方式獲取到了 store 存儲的值,但是大家有沒有發現,這樣比較繁瑣,我們其實可以用解構的方式來獲取值,使得代碼更簡潔一點。

解構代碼如下:

import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = store;

上段代碼實現的效果與一個一個獲取的效果一樣,不過代碼簡潔了很多。

4.5.2 多個組件使用 state

我們使用 store 的最重要的目的就是爲了組件之間共享數據,那麼接下來我們新建一個 child.vue 組件,在該組件內部也使用 state 數據。

child.vue 代碼如下:

<template>
  <h1>我是child組件</h1>
  <p>姓名{{ name }}</p>
  <p>年齡{{ age }}</p>
  <p>性別{{ sex }}</p>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = store;
</script>

child 組件和 app.vue 組件幾乎一樣,就是很簡單的使用了 store 中的數據。

實現效果:

這樣我們就實現了多個組件同時使用 store 中的數據。

4.5.3 修改 state 數據

如果我們想要修改 store 中的數據,可以直接重新賦值即可,我們在 App.vue 裏面添加一個按鈕,點擊按鈕修改 store 中的某一個數據。

代碼如下:

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <p>姓名{{ name }}</p>
  <p>年齡{{ age }}</p>
  <p>性別{{ sex }}</p>
  <button @click="changeName">更改姓名</button>
</template>
<script setup lang="ts">
import child from './child.vue';
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = store;
const changeName = () => {
  store.name = "張三";
  console.log(store);
};
</script>

上段代碼新增了 changeName 方法,改變了 store 中 name 的值,我們點擊按鈕,看看最終效果:

我們可以看到 store 中的 name 確實被修改了,但是頁面上似乎沒有變化,這說明我們的使用的 name 不是響應式的。

很多小夥伴可能會說那可以用監聽函數啊,監聽 store 變化,刷新頁面...

其實,pinia 提供了方法給我們,讓我們獲得的 name 等屬性變爲響應式的,我們重新修改代碼。

app.vue 和 child.vue 代碼修改如下:

import { storeToRefs } from 'pinia';
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);

我們兩個組件中獲取 state 數據的方式都改爲上段代碼的形式,利用 pinia 的 storeToRefs 函數,將 sstate 中的數據變爲了響應式的。

除此之外,我們也給 child.vue 也加上更改 state 數據的方法。

child.vue 代碼如下:

<template>
  <h1>我是child組件</h1>
  <p>姓名{{ name }}</p>
  <p>年齡{{ age }}</p>
  <p>性別{{ sex }}</p>
  <button @click="changeName">更改姓名</button>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
import { storeToRefs } from 'pinia';
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {
  store.name = "小豬課堂";
};
</script>

這個時候我們再來嘗試分別點擊兩個組件的按鈕,實現效果如下:

當我們 store 中數據發生變化時,頁面也更新了!

4.5.4 重置 state

有時候我們修改了 state 數據,想要將它還原,這個時候該怎麼做呢?就比如用戶填寫了一部分表單,突然想重置爲最初始的狀態。

此時,我們直接調用 store 的 $reset() 方法即可,繼續使用我們的例子,添加一個重置按鈕。

代碼如下:

<button @click="reset">重置store</button>
// 重置store
const reset = () => {
  store.$reset();
};

當我們點擊重置按鈕時,store 中的數據會變爲初始狀態,頁面也會更新。

4.5.5 批量更改 state 數據

前面我們修改 state 的數據是都是一條一條修改的,比如 store. 等等,如果我們一次性需要修改很多條數據的話,有更加簡便的方法,使用 store 的 $patch 方法,修改 app.vue 代碼,添加一個批量更改數據的方法。

代碼如下:

<button @click="patchStore">批量修改數據</button>
// 批量修改數據
const patchStore = () => {
  store.$patch({
    name: "張三",
    age: 100,
    sex: "女",
  });
};

有經驗的小夥伴可能發現了,我們採用這種批量更改的方式似乎代價有一點大,假如我們 state 中有些字段無需更改,但是按照上段代碼的寫法,我們必須要將 state 中的所有字段例舉出了。

爲了解決該問題,pinia 提供的 $patch 方法還可以接收一個回調函數,它的用法有點像我們的數組循環回調函數了。

示例代碼如下:

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

上段代碼中我們即批量更改了 state 的數據,又沒有將所有的 state 字段列舉出來。

4.5.6 直接替換整個 state

pinia 提供了方法讓我們直接替換整個 state 對象,使用 store 的 $state 方法。

示例代碼:

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

上段代碼會將我們提前聲明的 state 替換爲新的對象,可能這種場景用得比較少,這裏我就不展開說明了。

4.6 getters 屬性

getters 是 defineStore 參數配置項裏面的另一個屬性,前面我們講了 state 屬性。getter 屬性值是一個對象,該對象裏面是各種各樣的方法。大家可以把 getter 想象成 Vue 中的計算屬性,它的作用就是返回一個新的結果,既然它和 Vue 中的計算屬性類似,那麼它肯定也是會被緩存的,就和 computed 一樣。

當然我們這裏的 getter 就是處理 state 數據。

4.6.1 添加 getter

我們先來看一下如何定義 getter 吧,修改 user.ts。

代碼如下:

export const useUsersStore = defineStore("users", {
  state: () => {
    return {
      name: "小豬課堂",
      age: 25,
      sex: "男",
    };
  },
  getters: {
    getAddAge: (state) => {
      return state.age + 100;
    },
  },
});

上段代碼中我們在配置項參數中添加了 getter 屬性,該屬性對象中定義了一個 getAddAge 方法,該方法會默認接收一個 state 參數,也就是 state 對象,然後該方法返回的是一個新的數據。

4.6.2 使用 getter

我們在 store 中定義了 getter,那麼在組件中如何使用呢?使用起來非常簡單,我們修改 App.vue。

代碼如下:

<template>
  <p>新年齡{{ store.getAddAge }}</p>
  <button @click="patchStore">批量修改數據</button>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
// 批量修改數據
const patchStore = () => {
  store.$patch({
    name: "張三",
    age: 100,
    sex: "女",
  });
};
</script>

上段代碼中我們直接在標籤上使用了 store.gettAddAge 方法,這樣可以保證響應式,其實我們 state 中的 name 等屬性也可以以此種方式直接在標籤上使用,也可以保持響應式。

當我們點擊批量修改數據按鈕時,頁面上的新年齡字段也會跟着變化。

4.6.3 getter 中調用其它 getter

前面我們的 getAddAge 方法只是簡單的使用了 state 方法,但是有時候我們需要在這一個 getter 方法中調用其它 getter 方法,這個時候如何調用呢?

其實很簡單,我們可以直接在 getter 方法中調用 this,this 指向的便是 store 實例,所以理所當然的能夠調用到其它 getter。

示例代碼如下:

export const useUsersStore = defineStore("users", {
  state: () => {
    return {
      name: "小豬課堂",
      age: 25,
      sex: "男",
    };
  },
  getters: {
    getAddAge: (state) => {
      return state.age + 100;
    },
    getNameAndAge(): string {
      return this.name + this.getAddAge; // 調用其它getter
    },
  },
});

上段代碼中我們又定義了一個名爲 getNameAndAge 的 getter 函數,在函數內部直接使用了 this 來獲取 state 數據以及調用其它 getter 函數。

細心的小夥伴可能會發現我們這裏沒有使用箭頭函數的形式,這是因爲我們在函數內部使用了 this,箭頭函數的 this 指向問題相信大家都知道吧!所以這裏我們沒有采用箭頭函數的形式。

那麼在組件中調用的形式沒什麼變化,代碼如下:

<p>調用其它getter{{ store.getNameAndAge }}</p>

4.6.4 getter 傳參

既然 getter 函數做了一些計算或者處理,那麼我們很可能會需要傳遞參數給 getter 函數,但是我們前面說 getter 函數就相當於 store 的計算屬性,和 vue 的計算屬性差不多,那麼我們都知道 Vue 中計算屬性是不能直接傳遞參數的,所以我們這裏的 getter 函數如果要接受參數的話,也是需要做處理的。

示例代碼:

export const useUsersStore = defineStore("users", {
  state: () => {
    return {
      name: "小豬課堂",
      age: 25,
      sex: "男",
    };
  },
  getters: {
    getAddAge: (state) => {
      return (num: number) => state.age + num;
    },
    getNameAndAge(): string {
      return this.name + this.getAddAge; // 調用其它getter
    },
  },
});

上段代碼中我們 getter 函數 getAddAge 接收了一個參數 num,這種寫法其實有點閉包的概念在裏面了,相當於我們整體返回了一個新的函數,並且將 state 傳入了新的函數。

接下來我們在組件中使用,方式很簡單,代碼如下:

 <p>新年齡{{ store.getAddAge(1100) }}</p>

4.7 actions 屬性

前面我們提到的 state 和 getters 屬性都主要是數據層面的,並沒有具體的業務邏輯代碼,它們兩個就和我們組件代碼中的 data 數據和 computed 計算屬性一樣。

那麼,如果我們有業務代碼的話,最好就是卸載 actions 屬性裏面,該屬性就和我們組件代碼中的 methods 相似,用來放置一些處理業務邏輯的方法。

actions 屬性值同樣是一個對象,該對象裏面也是存儲的各種各樣的方法,包括同步方法和異步方法。

4.7.1 添加 actions

我們可以嘗試着添加一個 actions 方法,修改 user.ts。

代碼如下:

export const useUsersStore = defineStore("users", {
  state: () => {
    return {
      name: "小豬課堂",
      age: 25,
      sex: "男",
    };
  },
  getters: {
    getAddAge: (state) => {
      return (num: number) => state.age + num;
    },
    getNameAndAge(): string {
      return this.name + this.getAddAge; // 調用其它getter
    },
  },
  actions: {
    saveName(name: string) {
      this.name = name;
    },
  },
});

上段代碼中我們定義了一個非常簡單的 actions 方法,在實際場景中,該方法可以是任何邏輯,比如發送請求、存儲 token 等等。大家把 actions 方法當作一個普通的方法即可,特殊之處在於該方法內部的 this 指向的是當前 store。

4.7.2 使用 actions

使用 actions 中的方法也非常簡單,比如我們在 App.vue 中想要調用該方法。

代碼如下:

const saveName = () => {
  store.saveName("我是小豬");
};

我們點擊按鈕,直接調用 store 中的 actions 方法即可。

5. 總結示例代碼

前面的章節中的代碼都不完整,主要貼的是主要代碼部分,我們這節將我們本篇文章用到的所有代碼都貼出來,供大家練習。

main.ts 代碼:

import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();


const app = createApp(App);
app.use(pinia);
app.mount("#app");

user.ts 代碼:

import { defineStore } from "pinia";


// 第一個參數是應用程序中 store 的唯一 id
export const useUsersStore = defineStore("users", {
  state: () => {
    return {
      name: "小豬課堂",
      age: 25,
      sex: "男",
    };
  },
  getters: {
    getAddAge: (state) => {
      return (num: number) => state.age + num;
    },
    getNameAndAge(): string {
      return this.name + this.getAddAge; // 調用其它getter
    },
  },
  actions: {
    saveName(name: string) {
      this.name = name;
    },
  },
});

App.vue 代碼:

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <p>姓名{{ name }}</p>
  <p>年齡{{ age }}</p>
  <p>性別{{ sex }}</p>
  <p>新年齡{{ store.getAddAge(1100) }}</p>
  <p>調用其它getter{{ store.getNameAndAge }}</p>
  <button @click="changeName">更改姓名</button>
  <button @click="reset">重置store</button>
  <button @click="patchStore">批量修改數據</button>
  <button @click="saveName">調用aciton</button>


  <!-- 子組件 -->
  <child></child>
</template>
<script setup lang="ts">
import child from "./child.vue";
import { useUsersStore } from "../src/store/user";
import { storeToRefs } from "pinia";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {
  store.name = "張三";
  console.log(store);
};
// 重置store
const reset = () => {
  store.$reset();
};
// 批量修改數據
const patchStore = () => {
  store.$patch({
    name: "張三",
    age: 100,
    sex: "女",
  });
};
// 調用actions方法
const saveName = () => {
  store.saveName("我是小豬");
};
</script>

child.vue 代碼:

<template>
  <h1>我是child組件</h1>
  <p>姓名{{ name }}</p>
  <p>年齡{{ age }}</p>
  <p>性別{{ sex }}</p>
  <button @click="changeName">更改姓名</button>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
import { storeToRefs } from 'pinia';
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {
  store.name = "小豬課堂";
};
</script>

總結

pinia 的知識點很少,如果你有 Vuex 基礎,那麼學起來更是易如反掌。其實我們更應該關注的是它的函數思想,大家有沒有發現我們在 Vue3 中的所有東西似乎都可以用一個函數來表示,pinia 也是延續了這種思想。

所以,大家理解這種組合式編程的思想更重要,pinia 無非就是以下 3 個大點:

當然,本篇文章只是講解了基礎使用部分,但是在實際工作中也能滿足大部分需求了,如果還有興趣學習 pinia 的其它特點,比如插件、訂閱等等,可以移步官網:pinia 官網 (https://pinia.web3doc.top/)。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1C996KCXF6lRV1LVOzfvRg