Vue 狀態管理未來樣子

前端小智發表於2023-04-06
本文首發於微信公眾號:大遷世界, 我的微信:qq449245884,我會第一時間和你分享前端行業趨勢,學習途徑等等。
更多開源作品請看 GitHub https://github.com/qq449245884/xiaozhi ,包含一線大廠面試完整考點、資料以及我的系列文章。

隨著Vue 3越來越受重視併成為預設版本,許多事情正在發生變化,生態系統逐漸完善中。直到最近,Vue3 的狀態管理預設推薦的是使用 Pinia。這節課,我們根據專案的規模,探索不同的狀態管理方式,並嘗試預測 Vue 中狀態管理的未來會是什麼樣子。

響應式 API

在options API中,我們可以使用 data() 選項為一個元件宣告響應式資料。在內部,返回的物件被包在響應式幫助器中。這個幫助器也可以作為一個公共API使用。

如果是多個資料被多個例項共享的狀態,那麼 可以使用 reactive()來建立一個 reactive 物件,然後從多個元件中匯入它。

import { reactive } from "vue";

export const store = {
  state: reactive({
    heroes: ['Aragorn', 'Legolas', 'Gimli', 'Gandalf']
  }),
  addHero(hero) {
    this.state.heroes.push(hero);
  }
};

透過這種方法,資料被集中起來,並可以在各個元件之間重複使用。這可能是一個簡單的選擇,對一個小的應用程式來說佔用的空間最小。

組合

一個類似的概念,即composition API帶來的概念,是使用一個組合 。這種模式在React 那麼非常流行,結合Vue強大的響應性機制,可以編寫一些優雅的、可重複使用的可組合,比如下面這些:

import { ref, computed } from "vue";
import fakeApiCall from "../api";

export default function useFellowship() {
  const heroes = ref([]);
  const loading = ref(false);

  async function init() {
    loading.value = true;
    heroes.value = await fakeApiCall();
    loading.value = false;
  }
  return {
    heroes: computed(() => heroes.value),
    loading: computed(() => loading.value),
    init
  };
}

然後,可以這樣使用:

<template>
  <p v-if="loading">Loading...</p>
  <p v-else>Companions: {{ heroes.join(", ") }}</p>
</template>

<script>
import useFellowship from "../composables/useFellowship";
import { computed } from "vue";
export default {
  name: "MiddleEarth",
  setup() {
    const { heroes, loading, init } = useFellowship();
    init();
    return {
      heroes: computed(() => heroes.value),
      loading,
    };
  },
};
</script>

事例地址:https://codesandbox.io/s/composables-middle-earth-07yc6h?file...

這種模式最初是為了取代 mixins 而引入的,因為現在的組合比繼承更受歡迎。但它也可以用來在元件之間共享狀態。這也是許多為取代 Vuex 而出現的庫背後的主要想法。

Vuex 4

image.png

Vuex是不會消失的。它支援Vue 3,具有相同的API和最小的破壞性變化(這可能是其他庫應該注意的)。唯一的變化是,安裝必須發生在一個 Vue 例項上,而不是直接安裝在 Vue 原型上。

import { createApp } from 'vue'
import { store } from './store'
import App from './App.vue'

const app = createApp(App)

app.use(store)

app.mount('#app')

Vuex 4 仍在維護中。不過,不會再有很多新的功能被新增到它裡面。如果你已經有一個使用Vuex 3的專案,並想推遲遷移到其他東西上,這是一個不錯的選擇。

Pinia

image.png

Pinia 開始是一個實驗,但很快就成為 Vue 3 的主要選擇。它提供了比 Vuex 更多的 API ,有更好的架構和更直觀的語法,充分利用了組合API。

在開發工具的支援上(狀態檢查、帶動作的時間線和時間旅行的能力),以及 Vuex 所提供的使用外掛的擴充套件性,pinia 在設計上是型別安全和模組化的,這是使用Vuex時最大的兩個痛點。

此外,定義 story 的語法與 Vuex 模組非常相似,這使得遷移的工作量非常小,而在使用該 store 時,用到的 API,接近於 Vue3 使用組合API的方式。

import { defineStore } from 'pinia'

export const useFellowship = defineStore('fellowship', {
  state: () => {
    return { heroes: ['Aragorn', 'Legolas', 'Gimli', 'Gandalf'] }
  },
  actions: {
    addHero(hero) {
      this.heroes.push(hero)
    },
  },
})
<script>
import { useFellowship } from '@/stores/fellowship'
export default {
  setup() {
    const fellowship = useFellowship()
    
    // 對狀態的訪問 
    //可以直接進行
    console.log(fellowship.heroes)
    
    // Using an action
    fellowship.addHero('Boromir')
  },
}
</script>

你可能已經注意到的,最大的區別是 mutations 完全消失了。它們通常被認為是極其冗長的,而使用它們沒有任何真正的好處。此外,也不再需要名稱空間了。有了新的匯入 store 的方式,所有的東西都被設計成了名稱空間。這意味著,在 Pinia 中,你沒有一個帶有多個模組的 store ,而是有多個按需匯入和使用的 store 。

Pinia Setup Store

Pinia支援另一種語法來定義 store。它使用一個定義響應式屬性和方法的函式,並返回它們,與Vue Composition API的 setup 函式非常相似。

import { defineStore } from 'pinia'

export const useFellowship = defineStore('fellowship', () => {
  const heroes = ref([]);
  
  function addHero(hero) {
    heroes.value.push(hero)
  }
  return {
    heroes,
    addHero
  };
})

在 Setup Stores 中:

  • ref() 成為 state 屬性
  • computed() 成為 getters
  • function() 成為 actions

Setup storesOptions Store 帶來了更多的靈活性,因為可以在一個 store 內建立 watchers ,並自由使用任何可組合的。

一個更實際的例子

建立一個 fellowship store,它可以容納一個 heroes 列表,並能新增和刪除對應的元素:

import { defineStore } from 'pinia'

export const useFellowship = defineStore('fellowship', {
  state: () => ({
    heroes: [],
    filter: 'all',
    // type will be automatically inferred to number
    id: 1
  }),
  getters: {
    aliveHeroes(state) {
      return state.heroes.filter((hero) => hero.isAlive)
    },
    deadHeroes(state) {
      return state.heroes.filter((hero) => !hero.isAlive)
    },
    filteredHeroes() {
      switch (this.filter) {
        case 'alive':
          return this.aliveHeroes
        case 'dead':
          return this.deadHeroes
        default:
          return this.heroes
      }
    }
  },
  actions: {
    addHero(name) {
      if (!name) return
      // Directly mutating the state!
      this.heroes.push({ name, id: this.id++, isAlive: true })
    },
    killHero(name) {
      this.heroes = this.heroes.map((hero) => {
        if (hero.name === name) {
          hero.isAlive = false
        }
        return hero
      })
    },
    setActiveFilter(filter) {
      this.filter = filter
    }
  }
})

如果你熟悉Vuex,那麼理解這段程式碼應該不是什麼難事。

首先,每個 state 都需要一個作為名稱空間的鍵。這裡,我們使用 fellowship

state 是一個函式,儲存這個 store 的所有響應性資料,getters 是訪問 store 裡面的資料。stategetters 都與Vuex相同。

但對於 actions 來說與 Vuex 差異比較大。上下文引數已經消失了,actions 可以直接透過 this 訪問 state 和 getters 。你可能已經注意到的,actions 直接操作 state,這在Vuex 中是被嚴格禁止的。

最後,由於狀態操作現在是在 actions 進行的,所以 mutations 被完全刪除。

使用 pinia store 很簡單:

<script>
import { useFellowship } from '../store/fellowship'
import HeroFilters from './HeroFilters'
export default {
  name: 'MiddleEarth',
  components: {
    HeroFilters
  },
  setup() {
    const fellowship = useFellowship()
    return {
      fellowship
    }
  }
}
</script>

<template>
  <div>
    <template v-if="fellowship.heroes.length">
      <HeroFilters />
      <ol>
        <li v-for="hero in fellowship.filteredHeroes" :key="hero.id">
          {{ hero.name }} - {{ hero.isAlive ? 'Alive' : 'Dead' }}
          <button v-if="hero.isAlive" @click="fellowship.killHero(hero.name)">Kill</button>
        </li>
      </ol>
    </template>
    <p v-else>Your fellowship is empty</p>
    <div>
      <input type="text" ref="heroName" />
      <input type="button" value="Add new hero" @click="fellowship.addHero($refs.heroName.value)" />
      <p>
        Sugestions:
        <button
          v-for="suggestion in ['Aragorn', 'Legolas', 'Gimli']"
          :key="suggestion"
          @click="fellowship.addHero(suggestion)"
        >
          {{ suggestion }}
        </button>
      </p>
    </div>
  </div>
</template>

所有的邏輯都發生在 setup 函式中。匯入的 useFellowship 鉤子被執行並返回。這樣在 template 就可以直接。

當然,這個元件應該被分解成更小的可重複使用的元件,但為了演示的目的,就先這樣吧。

如果一個不同的元件需要訪問相同的 state,可以用類似的方式來完成。

<script>
import { useFellowship } from '../store/fellowship'
export default {
  name: 'HeroFilters',
  setup() {
    const fellowship = useFellowship()
    return {
      fellowship
    }
  }
}
</script>

<template>
  <div>
    Filter:
    <div v-for="filter in ['all', 'dead', 'alive']" :key="filter">
      <input
        type="radio"
        :value="filter"
        :id="filter"
        @click="fellowship.setActiveFilter(filter)"
        v-model="fellowship.filter"
      />
      <label :for="filter">{{ filter }}</label>
    </div>
  </div>
</template>

事例地址:https://codesandbox.io/s/pinia-playground-brgy58?file=/src/co...

從 Vuex 遷移到 Pinia

Pinia的文件很樂觀,認為程式碼可以在庫之間重複使用,但事實是,架構非常不同,肯定需要重構。首先,在Vuex中,我們有一個帶有多個模組的 store ,而 Pinia 是圍繞著多個 store 的概念建立的。將這一概念過渡到Pinia 中的最簡單方法是,以前使用的每個模組現在都是一個 store 。

此外,mutations 不再存在。相反,這些應該轉換為直接訪問和改變狀態的操作。

Actions 不再接受上下文作為其第一個引數。它們應該被更新以直接訪問狀態或任何其他上下文屬性。這同樣適用於 rootStaterootGetters等,因為單一全域性儲存的概念並不存在。如果你想使用另一個 store,需要明確地匯入它。

很明顯,對於大型專案來說,遷移將是複雜和耗時的,但希望大量的模板程式碼將被消除,store 將遵循一個更乾淨和模組化的架構。改造可以逐個模組進行,而不是一次性改造所有內容。實際上,在遷移過程中,可以將 Pinea 和Vuex混合在一起,這種方法對於複雜的專案來說也是不錯的選擇。

總結

image.png

預測未來並不容易,但就目前而言,Pinea 是最安全的賭注。它提供了一個模組化的架構,透過設計實現型別安全,並消除了模板程式碼。如果你要用Vue 3開始一個新的專案,Pinia 是值得推薦的選擇。

如果你已經在使用Vuex,你可以在遷移到Pinia之前升級到第4版,因為這個過程看起來很簡單,但需要大量的時間。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

作者:Fotis Adamakis 譯者:前端小智 來源:mediun

原文:
https://fadamakis.medium.com/the-future-of-state-management-i...

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章