vue pinia

[Vue] 新一代狀態管理工具 Pinia

許馨方 Mia Hsu 2022/11/08 14:56:59
17139

在閱讀這篇文章之前,希望你已經具備 Vue 的相關知識以及基本的使用,以及了解狀態管理(可參考:[Vuex] 認識 Vue 的好朋友

不是有 Vuex 嗎?怎麼來了個 Pinia

Pinia 是 Vue3 推出後,官方推薦使用的新狀態管理工具, 也是默認配置。可以將 Pinia 視為 Vuex 5,而 Vuex 也進入維護狀態。

Pinia 和 Vuex 適用 Vue 版本

Vuex

  • v4 適用於 Vue3
  • V3 適用於 Vue2

Pinia

  • Vue3 、Vue2 都支援

Pinia 和 Vuex 之間的差異

取自於 Pinia 官網對比 Vuex 的簡要敘述如下

  • Pinia 移除 Mutations
  • Pinia 支持 Server Side Rendering
  • Pinia 無需設置 namespaced,所有的 store module 都已自動 namespaced
  • Pinia 可以直接從 store 取得任何的 State
  • Pinia 對 TS 有著更好的支援,不再需要多餘的 types 來包裝
  • Pinia 使用 action 可以直接引入函數,不再需要
  • Pinia 取得 state 不再需要傳遞參數,可直接使用「this」取得
  • Pinia 可以在 actions 中使用 async/await 取得非同步資料後更改 State
  • Pinia 不在是單一 Store,使得我們與 Store 互動 API 比 Vuex 簡單許多

Pinia 基本架構(Store、State、Actions)

圖片來源
  • Store & State

與 Vuex 完全相同,不再贅述。

  • Actions

Pinia 的 Actions 與 Vuex Mutations 操作相同,並支援 async function,這是改變 State 的唯一方法。

 

 

 

一個簡單的 Pania Store 可能如下:

import { defineStore } from "pinia";

export const useCounterStore = defineStore({
  id: "counter",
  state: () => ({
    counter: 0,
  }),

  getters: {
    doubleCount: (state) => state.counter * 2,
  },

  actions: {
    increment() {
      this.counter++;
    },
    reduce() {
      this.counter--;
    },
  },
});

 

 

了解 Pinia 的基本用法後,我們接著會來比較寫法上的差異,會比較的寫法有以下三種

  • Vue3(Composition API)+Pinia
  • Vue2(Option API)+ Pinia
  • Vue3(Option API)+ Vuex4

前置安裝說明

vue 起專案可以使用 vue cli 或 npm init 兩種方式:

  • vue cli(v5.0.8):沒有安裝 Pinia 的選項,如果要使用 Pinia 需 npm 另外安裝。
  • npm(v6.14.5):npm init vue 會詢問是否要安裝 pinia,沒有安裝 Vuex 的選項,如果要使用 Vuex 需 npm 另外安裝。。

比較1. 註冊套件、創建 Store 並在 component 中引入 State

在註冊的部分 Pinia 與 Vuex 的差異在於,Pinia 默認就是動態創建模組,而 Vuex 需要先把 Store module 建立出來後在進行註冊。

Vue3(Composition API)+Pinia

 

 

 

  • createPinia:創建 Pinia 實例
  • defineStore:定義 Store
  • storeToRefs:由於 Store 是 reactive 物件,需要解構才能取出 State

 

// ----- main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')


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

export const useCounterStore = defineStore({
  id: 'counter',
  state: () => ({
    counter: 0
  }),
})


// ----- app.js
<script setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from "./stores/counter";

const counterStore = useCounterStore()
const { counter } = storeToRefs(counterStore);
console.log(counter)
</script>

<template>
  {{counter}}
</template>

Vue2(Option API)+ Pinia

 

 

 

  • PiniaVuePlugin:需要在 main.js 注入才能使用 Pinia
  • mapState :在 component 中使用 State

 

// ----- main.js
import Vue from 'vue'
import App from './App.vue'
import { createPinia, PiniaVuePlugin } from 'pinia'

Vue.config.productionTip = false
Vue.use(PiniaVuePlugin);
const pinia = createPinia();

new Vue({
  pinia,
  render: h => h(App),
}).$mount('#app')


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

export const useCounterStore = defineStore({
  id: 'counter',
  state: () => ({
    counter: 0
  }),
})


// ----- app.js
<template>
  <div id="app">
    {{counter}}
  </div>
</template>

<script>
import { mapState } from 'pinia';
import { useCounterStore } from "./stores/counter";

export default {
  name: 'App',
  computed: {
    ...mapState(useCounterStore, ['counter']),
  }
}
</script>

 

Vue3(Option API)+ Vuex4

  • createStore:建立初始 State 並註冊
  • mapState:在 component 中使用 State

 

// ----- stores/index.js
import { createStore } from 'vuex'

const state = {
    counter: 0,
}
const store = createStore({
    state,
})
export default store


// ----- main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

createApp(App)
    .use(store)
    .mount('#app')


// ----- app.js
<template>
  Counter: {{ counter }}
</template>

<script>
import { mapState } from 'vuex';

export default {
  name: 'App',
  computed: {
    ...mapState(['counter'])
  }
}
</script>

 

使用 vue devtools 就會看到 counter 的 state

比較2. 建立 Actions,並在 Component 中事件觸發 Action

在事件觸發 Action 的部分 Pinia 與 Vuex 的差異在於,Pinia 不用定義 mutation,可直接在 Action 的部分改變 State 的狀態。

Vue3(Composition API)+Pinia

  • 可直接在解構完的 Store 中使用點運算子取得 Ation

 

// ------ /stores/counter.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore({
  id: 'counter',
  state: () => ({
    counter: 0
  }),
  actions: {
    increment() {
      this.counter++
    },
    decrease() {
      this.counter--
    }
  }
})





// ------ App.js

<script setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from "./stores/counter";

const counterStore = useCounterStore()
const { counter } = storeToRefs(counterStore);
const doDecrease = () => {
  counterStore.decrease()
}
const doIncrement = () => {
  counterStore.increment()
}
</script>

<template>
  <button @click="doDecrease">-</button>
  Counter: {{ counter }}
  <button @click="doIncrement">+</button>
</template>

 

Vue2(Option API)+ Pinia

  • mapActions:取得 Action

 

// ------ stores/counter.js

import { defineStore } from 'pinia';

export const useCounterStore = defineStore({
  id: 'counter',
  state: () => ({
    counter: 0
  }),
  actions: {
    increment() {
      this.counter++
    },
    decrease() {
      this.counter--
    }
  }
})




// ------ App.js

<template>
  <div id="app">
    <button @click="decrease">-</button>
    Counter: {{ counter }}
    <button @click="increment">+</button>
  </div>
</template>

<script>
import { mapState, mapActions } from 'pinia';
import { useCounterStore } from "./stores/counter";

export default {
  name: 'App',
  methods: {
    ...mapActions(useCounterStore, ['increment', 'decrease']),
  },
  computed: {
    ...mapState(useCounterStore, ['counter']),
  }
}
</script>

 

Vue3(Option API)+ Vuex4

 

// ------ stores/index.js
import { createStore } from 'vuex'
import modules from './modules'

const state = {
    counter: 0,
}

const mutations = {
    increment: (state) => state.counter++,
    decrease: (state) => state.counter--,
}

const actions = {
    increment: (context) => {
        context.commit('increment')
    },
    decrease: (context) => {
        context.commit('decrease')
    },
}

const store = createStore({
    state,
    mutations,
    actions,
})

export default store




// ------ App.js
<template>
  <button @click="decrease">-</button>
  Counter: {{ counter }}
  <button @click="increment">+</button>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  name: 'App',
  methods: {
    ...mapActions(['increment', 'decrease']),
  },
  computed: {
    ...mapState(['counter'])
  }
}
</script>

 

結語

以上為 Pinia 的基本用法,並同時附上 Vuex 的用法進行比較,我們可以發現,Pinia 的寫法簡潔非常多!並更直觀,把 Vuex 長久以來的詬病(Muation、引入各種 magic string)進行了優化,希望大家也可以試試看這新的 Store 管理套件。

參考資料

以上有任何錯誤的地方歡迎指正,感謝。

 

許馨方 Mia Hsu