iT邦幫忙

2024 iThome 鐵人賽

DAY 11
1
Modern Web

Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器系列 第 11

Day 11: TypeScript 與 Pinia:如何定義強型別的 Store

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240920/20117461EoBkY0Y5v5.jpg

介紹

在 Vue 3 中,Pinia 作為狀態管理庫,提供了靈活而強大的工具來管理應用的狀態。為了進一步提高 Pinia 的使用體驗,了解其底層機制和相關概念非常重要。本文將深入探討 Pinia 中的 effectScopeMapWeakMapSetWeakSet 以及 private storereadonly store 的運作方式,並提供對應的實作範例。

effectScope 的工作原理及其與 Pinia 的關係

effectScope 是 Vue 3 提供的一個響應式作用域控制機制,它允許我們在一個作用域中集中管理響應式狀態、計算屬性和監聽器,並能夠在需要時一鍵清除。Pinia 使用 effectScope 來管理 store 的響應式狀態,確保每個 store 的狀態在作用域被清理時自動停止追蹤。

effectScope 的基本使用

import { shallowRef, effectScope } from 'vue';

// 創建一個新的 effectScope
const scope = effectScope();

scope.run(() => {
  const count = shallowRef<number>(0);
  
  // 使用 effectScope 管理的狀態
  const increment = () => {
    count.value++;
  };

  console.log('當前計數:', count.value);
  increment();
  console.log('增加後的計數:', count.value);
});

// 清理 scope 中的所有狀態
scope.stop();

在這個例子中,effectScope 管理了 countincrement 函數,當 scope.stop() 被調用時,所有在 scope 中的反應性依賴都會被清理。

Pinia 中的 effectScope

在 Pinia 中,每個 store 都是由 effectScope 管理的,這意味著當我們銷毀 store 或不再使用時,可以通過停止該作用域來清理所有反應性狀態。這確保了 store 狀態的隔離和內存管理。

2. Pinia 中的 Map、WeakMap、Set、WeakSet

在 Pinia 的內部,MapWeakMap 被用於管理 store 的依賴和作用域。這些數據結構的選擇使得 Pinia 能夠高效地處理狀態管理和依賴追蹤。

  • Map:用於存儲 key/value,key/value 都可以是任意類型。Pinia 使用 Map 來管理 store 實例和狀態。
  • WeakMap:鍵必須是 Object,且其引用是弱引用。Pinia 使用 WeakMap 來處理與 store 相關的依賴,避免內存泄漏。
  • SetWeakSet:用於存儲唯一值,WeakSet 中的對象引用也是弱引用。Pinia 使用這些結構來管理動作和狀態追蹤。

MapWeakMap 的基本使用

// 使用 Map 管理 store 的實例
interface CustomObject {
  count: number;
}

const storeMap = new Map<string, CustomObject>();

const myStore = { count: 0 };
storeMap.set('myStore', myStore);

console.log(storeMap.get('myStore')); //結果是 { count: 0 }

// 使用 WeakMap 管理 store 的依賴
interface MappingKey {
  id: string;
  state: string;
}
const storeWeakMap = new WeakMap<MappingKey, CustomObject>();
const storeInstanceKey: MappingKey = { 
  id: 'this is id',
  state: 'start...'
};
storeWeakMap.set(storeInstanceKey, { count: 1 });

console.log(storeWeakMap.get(storeInstance)); // { count: 1 }

實作 private storereadonly store 的概念及實作

關於我們在 Day8 提供單向數據流,揭開了 pinia 可維護性的正確寫法,但那只是冰山一角。接下來我們正是系統性地處理並解決可維護性的 pinia store 該如何展現。

Pinia 支持 private storereadonly store 的設計,使得我們可以創建只能在內部修改的狀態,並對外部提供只讀訪問。確保整體的設計是一致有系統性且嚴謹的

Private Store

private store 的概念是指某些狀態只能在 store 內部修改,外部只能通過 actions 來操作。

(檔案src/stores/useCounterStore.ts)

import { computed, shallowRef } from 'vue'
import { defineStore, acceptHMRUpdate } from 'pinia';

// private pinia
const usePrivateCounterStore = defineStore("usePrivateCounterStore", () => {
  // private state::
  const count = shallowRef<number>(0);

  return {
    count,
  }
});

export const useCounterStore = defineStore('useCounterStore', () => {
  const privateCounterStore = usePrivateCounterStore();
  // state::

  // getter::
  const doubleCount = computed<number>(() => privateCounterStore.count * 2);

  // methods::
  const increment = (): void => {
    privateCounterStore.count++;
  };

  return {
    // state::
    count: computed<number>(() => privateCounterStore.count),
    // getters::
    doubleCount,
    // methods::
    increment
  }
});

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(usePrivateCounterStore, import.meta.hot));
  import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot));
}

在這個例子中,privateCount 是私有的,外部無法直接修改它,只能通過內部的 increment 方法來改變其值。可以更近一步確保封裝的安全,且不會受到外部 composables, stores, components 直接更改。

Readonly Store

readonly store 允許我們定義一個僅提供讀取的 store 狀態,這對於需要保證狀態不可變的情況非常有用。

(檔案 src/stores/readonlyStore.ts)

import { defineStore } from 'pinia';
import { readonly, shallowRef } from 'vue';

export const useReadonlyStore = defineStore('readonly', () => {
  const count = shallowRef(0);

  const increment = () => {
    count.value++;
  };

  return { count: readonly(count), increment };
});

這裡的 count 是只讀的,雖然可以通過 increment 修改,但直接操作 count 的值將被禁止,這保護了狀態的完整性。

**優化/強化 private store **

在使用 privateStore 的狀態下每次都要去產生兩個 store 一個去處理 private state 一個去定義 public method 或是 getters 的部分,這樣每次撰寫時都要重複做一件事,那我們把整個方法封裝起來,建立 privateState 的方法

(檔案 src/stores/privateState.ts)

import { UnwrapRef } from 'vue';
import { Router } from 'vue-router'
import { defineStore, StateTree, PiniaCustomStateProperties } from 'pinia';

export function definePrivateState<
  Id extends string,
  PrivateState extends StateTree,
  SetupReturn
>(
  id: Id, 
  privateStateFn: () => PrivateState, 
  setup: (privateState: UnwrapRef<PrivateState> & PiniaCustomStateProperties<PrivateState>, router: Router) => SetupReturn
 ) {
  const usePrivateState = defineStore(`${id}_private`, {
    state: privateStateFn,
  });

  return defineStore(id, () => {
    const privateState = usePrivateState();
    return setup(privateState.$state);
  });
}

這樣就可以這樣使用 private state pinia store

(檔案 src/stores/useNewCounterStore.ts)

import { computed } from "vue"
import { acceptHMRUpdate } from "pinia";
import { definePrivateState } from "./privateState";
import { RoutesStatus } from "../router";

export const useNewCounterStore = definePrivateState("useNewCounterStore", () => {
  return {
    count: 0,
  }
}, privateState => {
  const doubleCount = computed<number>(() => privateState.count * 2);

  const increment = (): void => {
    privateState.count++;
  };

  return {
    count: computed(() => privateState.count),
    doubleCount,
    increment,
  }
});

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useNewCounterStore, import.meta.hot));
}

Pinia 至高領域 - 自定義 pinia 的行為

基本上有以上的操作會認為對 pinia 的理解已經非常通透了。
https://ithelp.ithome.com.tw/upload/images/20240925/201174614LKlHHxGoe.jpg
(圖片取自鬼滅之刃)

但要到達 pinia 至高領域,還需要可以自定義 pinia 的行為,但這些自定義行為需要道行高一點才能駕馭,就像炭治郎一開始要掌握日之呼吸不大可能的,建議以下深入的操作,在對 vue 和 pinia 有更深的理解再進行以下操作。(總共有 10 個型),由於篇幅有限,我只展示第一型最簡單的部分

https://ithelp.ithome.com.tw/upload/images/20240925/20117461JmH0TBu9Lf.jpg

把 vue-router 自動注入 pinia store 不用另外引入

這裡就魔改一下 privateState

(檔案 src/stores/privateState.ts)

import { UnwrapRef } from 'vue';
import { Router } from 'vue-router'
import { defineStore, StateTree, PiniaCustomStateProperties } from 'pinia';

export function definePrivateState<
  Id extends string,
  PrivateState extends StateTree,
  SetupReturn
>(
  id: Id, 
  privateStateFn: () => PrivateState, 
  setup: (privateState: UnwrapRef<PrivateState> & PiniaCustomStateProperties<PrivateState>, router: Router) => SetupReturn
 ) {
  const usePrivateState = defineStore(`${id}_private`, {
    state: privateStateFn,
    actions: {
      useRouter() {
        // 這裡注入 vue-router,讓往後使用的 store 可以直接呼叫 vue-router 的方法
        return this.router;
      }
    }
  });

  return defineStore(id, () => {
    const privateState = usePrivateState();
    return setup(privateState.$state, privateState.useRouter());
  });
}

為了做到上述的事情,要增加 vue-router 的 custom plugin 在 createPinia 的時候

(檔案 src/pinia/index.ts)

import { markRaw } from 'vue'
import { Router } from 'vue-router'
import { createPinia } from 'pinia';
import router from '../router';

export const pinia = createPinia();

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

declare module 'pinia' {
  interface PiniaCustomProperties {
    router: Router,
  }
}

這時候在 main.ts 可以稍微修改

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { pinia } from './pinia' // 這裡改成這樣不用直接 createPinia

createApp(App)
  .use(router)
  .use(pinia) // 這裡修改
  .mount('#app')

上述完成後,就可以可以做一個 privateState 使用 router 做一些操作了,這裡我們複製原來的 useNewCounterStore,複製貼上製作一個新的 useNewBaseStore 作為展示

(檔案 src/stores/useNewBaseStore.ts)

import { computed } from "vue"
import { acceptHMRUpdate } from "pinia";
import { definePrivateState } from "./privateState";
import { RoutesStatus } from "../router";

export const useNewBaseStore = definePrivateState("useNewBaseStore", () => {
  return {
    count: 0,
  }
}, (privateState, router) => {
  const doubleCount = computed<number>(() => privateState.count * 2);

  const increment = (): void => {
    privateState.count++;
  };

  // 這裡即可直接呼叫 vue-router 進行陸游的操作,不用額外的 `const router = useRouter`
  const goToFoo = (): void => {
    router.push({ name: RoutesStatus.Foo });
  };

  return {
    count: computed(() => privateState.count),
    doubleCount,
    increment,
    goToFoo,
  }
});

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useNewBaseStore, import.meta.hot));
}

結論

通過結合 TypeScript 與 Pinia,並深入了解 effectScope、依賴注入、MapWeakMapSetWeakSet 以及 private storereadonly store 的概念,我們能夠更加靈活和高效地管理應用中的狀態。這些概念不僅提升了 Pinia 的可用性,也保證了應用的健壯性和可維護性。

希望這篇文章能幫助你更好地理解 Pinia 的底層原理和實際應用,讓你在開發過程中能夠更加得心應手!


上一篇
Day 10: 使用 Vue Router 實現基於角色的路由權限控制
下一篇
Day 12: 在 UnoCSS 中設計響應式布局:從手機到桌面應用
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言