深入解析在Vue 3中如何管理共用狀態?【翻譯】

2022-01-27 13:00:35
在Vue 3中如何管理共用狀態?下面本篇文章就來帶大家探討一下,希望對大家有所幫助!

快速總結↬ 編寫大型應用程式可能是一個挑戰。在Vue3應用程式中使用共用狀態可以解決專案複雜性。有很多常見的狀態解決方案, 在本文中,我將深入探討使用工廠、共用物件和Vuex等方法的優缺點。我還將向你展示Vuex5中可能改變我們在Vue3中使用共用狀態的一些內容。【相關推薦:】

state可能很難,當我們開始一個簡單的Vue專案時,只需要我們保持特定元件的工作狀態就很簡單:

setup() {
  let books: Work[] = reactive([]);

  onMounted(async () => {
    // Call the API
    const response = await bookService.getScienceBooks();
    if (response.status === 200) {
      books.splice(0, books.length, ...response.data.works);
    }
  });

  return {
    books
  };
},

當你的專案是顯示資料的單一頁面(可能是為了排序或過濾資料)時,這可能很有吸引力。但是在本例中,該元件將獲得每個請求的資料。如果你想保留它呢?這時候狀態管理就可以發揮作用了。由於網路連線通常代價很高並且偶爾不太可靠。因此在瀏覽應用程式時最好保持一些狀態。

另一個問題是元件之間的通訊。雖然您可以使用eventsprops直接與child-parents通訊, 但是當你的每個views/pages都是獨立的時候,處理諸如錯誤攔截和忙碌標誌之類的簡單情況會變得很困難。舉個例子, 假設你使用了一個頂級控制元件來顯示error和載入動畫:

// App.vue
<template>
  <div class="container mx-auto bg-gray-100 p-1">
    <router-link to="/"><h1>Bookcase</h1></router-link>
    <div class="alert" v-if="error">{{ error }}</div>
    <div class="alert bg-gray-200 text-gray-900" v-if="isBusy">
      Loading...
    </div>
    <router-view :key="$route.fullPath"></router-view>
  </div>
</template>

如果沒有有效的方法來處理這種狀態,它可能會建議使用釋出/訂閱系統,但實際上在許多情況下共用資料更為直接。如果你想使用共用狀態,你該怎麼做呢?讓我們看看一些常見的方法。

注意:你將在 GitHub 上範例專案的「main」分支中找到此部分的程式碼。

Vue3中的共用狀態 #

自從遷移到 Vue 3 後,我已經完全遷移到使用 Composition API,這篇文章我還使用了TypeScript 儘管在我展示的範例中這不是必需的。雖然你可以以任何方式共用狀態,但我將向你展示我發現的幾種最常用的技術模式,每一種都有自己的優點和缺點,所以不要把我在這裡所說的任何東西當作唯一。

技術包括:

注意: 在撰寫本文時,Vuex 5還處於RFC(徵求意見)階段,所以我想讓你為Vuex5的到來做好準備,但目前還沒有該選項的工作版本。

讓我們深入瞭解……

工廠#

注意: 此部分的程式碼位於GitHub 上範例專案的「Factories」分支中。

工廠模式只關心你所建立的狀態的範例。在此模式中,你將返回一個與Composition API中的start函數非常相似的函數。你將建立一個作用域並構建你想尋找的元件。舉個例子:

export default function () {

  const books: Work[] = reactive([]);

  async function loadBooks(val: string) {
      const response = await bookService.getBooks(val, currentPage.value);
      if (response.status === 200) {
        books.splice(0, books.length, ...response.data.works);
      }
  }

  return {
    loadBooks,
    books
  };
}

你可以要求工廠建立你所需要的那部分物件:

// In Home.vue
  const { books, loadBooks } = BookFactory();

如果我們新增一個isBusy標記來顯示網路請求何時發生,上面的程式碼不會改變,但你可以決定在哪裡顯示isBusy

export default function () {

  const books: Work[] = reactive([]);
  const isBusy = ref(false);

  async function loadBooks(val: string) {
    isBusy.value = true;
    const response = await bookService.getBooks(val, currentPage.value);
    if (response.status === 200) {
      books.splice(0, books.length, ...response.data.works);
    }
  }

  return {
    loadBooks,
    books,
    isBusy
  };
}

在另一個檢視(vue?)中,你可以只要isBusy標記,而不需要知道工廠其他部分是如何工作的:

// App.vue
export default defineComponent({
  setup() {
    const { isBusy } = BookFactory();
    return {
      isBusy
    }
  },
})

但是你可能注意到了一個問題;每次我們呼叫工廠時,我們都會獲得所有物件的新範例。有時,你希望工廠返回新範例,但在我們的例子中,我們討論的是共用狀態,因此我們需要將建立移到工廠之外:

const books: Work[] = reactive([]);
const isBusy = ref(false);

async function loadBooks(val: string) {
  isBusy.value = true;
  const response = await bookService.getBooks(val, currentPage.value);
  if (response.status === 200) {
    books.splice(0, books.length, ...response.data.works);
  }
}

export default function () {
 return {
    loadBooks,
    books,
    isBusy
  };
}

現在,工廠給了我們一個共用範例,或者一個單例。雖然這種模式可以工作,但返回一個每次都不建立新範例的函數可能會令人困惑。

因為底層物件被標記為const,所以不能替換它們(也不能破壞單例的性質)。所以這段程式碼應該會報錯:

// In Home.vue
  const { books, loadBooks } = BookFactory();

  books = []; // Error, books is defined as const

因此,確保可變狀態能夠被更新是很重要的(例如,使用books.splice()而不是設定books)。另一種處理方法是使用共用範例

共用範例 #

本節的程式碼在GitHub上範例專案的「Sharedstate」分支中。

如果你使用共用狀態,最好清楚狀態是一個單例的事實。在本例中,它可以作為一個靜態物件匯入。例如,我喜歡建立一個可以作為響應式物件匯入的物件:

export default reactive({

  books: new Array<Work>(),
  isBusy: false,

  async loadBooks() {
    this.isBusy = true;
    const response = await bookService.getBooks(this.currentTopic, this.currentPage);
    if (response.status === 200) {
      this.books.splice(0, this.books.length, ...response.data.works);
    }
    this.isBusy = false;
  }
});

在這種情況下,你只需匯入物件(在本例中我將其稱為商店):

// Home.vue
import state from "@/state";

export default defineComponent({
  setup() {

    // ...

    onMounted(async () => {
      if (state.books.length === 0) state.loadBooks();
    });

    return {
      state,
      bookTopics,
    };
  },
});

然後很容易繫結到狀態:

<!-- Home.vue -->
<div class="grid grid-cols-4">
  <div
    v-for="book in state.books"
    :key="book.key"
    class="border bg-white border-grey-500 m-1 p-1"
  >
  <router-link :to="{ name: 'book', params: { id: book.key } }">
    <BookInfo :book="book" />
  </router-link>
</div>

與其他模式一樣,你可以在檢視之間共用此範例:

// App.vue
import state from "@/state";

export default defineComponent({
  setup() {
    return {
      state
    };
  },
})

然後它可以繫結到同一個物件(無論它是Home.Vue的父頁面)或路由到其他頁面):

<!-- App.vue -->
  <div class="container mx-auto bg-gray-100 p-1">
    <router-link to="/"><h1>Bookcase</h1></router-link>
    <div class="alert bg-gray-200 text-gray-900"   
         v-if="state.isBusy">Loading...</div>
    <router-view :key="$route.fullPath"></router-view>
  </div>

無論使用工廠模式還是共用範例,它們都有一個共同的問題:可變狀態。當你不希望繫結或程式碼更改狀態時,可能會產生意外的副作用。在本文中我使用簡單範例中,它還沒有那麼複雜,因此不用擔心。但是當你構建的應用程式越大越大時,將需要更仔細地考慮狀態的變化。這時候Vuex就可以伸出援手了。

VUEX 4 #

本節的程式碼位於GitHub上範例專案的「Vuex4」分支中。

Vuex是vue的狀態管理器。它是由核心團隊構建的,但它作為一個單獨的專案進行管理。Vuex的目的是將狀態與狀態執行操作分離開來。所有狀態更改都必須通過 Vuex,這意味著它更復雜,但你可以防止意外狀態更改。

Vuex 的想法是提供可預測的狀態管理流程。檢視流向 Actions,Actions 反過來使用 Mutations 來改變狀態,從而更新檢視。通過限制狀態更改的流程,可以減少改變應用程式狀態的副作用;因此更容易構建更大的應用程式。Vuex 有一個學習曲線,但有了這種複雜性,你就可以獲得可預測性。

此外,Vuex 還支援程式偵錯(通過 Vue Tools)來處理狀態管理,包括一個稱為time-travel的功能。這允許你檢視狀態的歷史記錄並前後移動以檢視它如何影響應用程式的。

有時,Vuex 也很重要。

要將其新增到你的 Vue 3 專案中,你可以將包新增到專案中:

> npm i vuex

或者,你也可以使用 Vue CLI 新增它:

> vue add vuex

通過使用CLI,它將為你的 Vuex Store 建立初始的起點, 否則你需要手動將其接入到專案。讓我們來看看這是如何工作的。

首先,你需要使用 Vuex 的 createStore 函數建立一個狀態物件:

import { createStore } from 'vuex'

export default createStore({
  state: {},
  mutations: {},
  actions: {},
  getters: {}
});

如你所見,Store需要定義多個屬性。State只是一個想要授予應用程式存取許可權的資料列表:

import { createStore } from 'vuex'

export default createStore({
  state: {
    books: [],
    isBusy: false
  },
  mutations: {},
  actions: {}
});

注意state不應使用ref or reactive 包裝。這裡的資料與我們在共用範例或工廠中使用的共用資料型別相同。這個store在你的應用程式中將作為一個單例,因此狀態中的資料也會被共用

然後,讓我們看看actions。 Actions是可以授予你改變state中資料的一個操作檯,舉個例子:

  actions: {
    async loadBooks(store) {
      const response = await bookService.getBooks(store.state.currentTopic,
      if (response.status === 200) {
        // ...
      }
    }
  },

Actions 會傳遞一個 store 的範例,以便你可以獲取狀態和其他操作。通常,我們只會解構我們需要的部分:

  actions: {
    async loadBooks({ state }) {
      const response = await bookService.getBooks(state.currentTopic,
      if (response.status === 200) {
        // ...
      }
    }
  },

最後一個是Mutations。Mutations是能夠改變狀態的函數。只有Mutations才能影響狀態。在這個例子中,我們需要Mutations來改變狀態:

  mutations: {
    setBusy: (state) => state.isBusy = true,
    clearBusy: (state) => state.isBusy = false,
    setBooks(state, books) {
      state.books.splice(0, state.books.length, ...books);
    }
 },

Mutation函數總是傳入狀態物件,以便你可以改變狀態。在前兩個範例中,你可以看到我們顯式地設定了狀態。但在第三個例子中,我們傳入了set的狀態。呼叫mutation時,mutation總是接收兩個引數:state和實參。

要呼叫mutation, 你需要在store中使用commit函數。在我們的例子中,我只是將它新增到解構中:

  actions: {
    async loadBooks({ state, commit }) {
      commit("setBusy");
      const response = await bookService.getBooks(state.currentTopic, 
      if (response.status === 200) {
        commit("setBooks", response.data);
      }
      commit("clearBusy");
    }
  },

你將在此處看到commit如何要求action的命名。有一些技巧使它不僅僅是有魔力的字串。但是我現在跳過它們。這種有魔力字串是限制Vuex的使用方式之一。

雖然使用 commit 可能看起來像是一個不必要的包裝器,但請記住,Vuex 不會讓你改變狀態,除非在mutation內部,因此只有通過commit呼叫才會。

你還可以看到對setBooks的呼叫採用了第二個引數。這是呼叫mutation的第二個引數。如果你需要更多資訊,則需要將其打包成一個引數(目前 Vuex 的另一個限制)。假設你需要將一本書插入到圖書列表中,你可以這樣稱呼它:

commit("insertBook", { book, place: 4 }); // object, tuple, etc.

然後你可以解構成你需要的部分:

mutations: {
  insertBook(state, { book, place }) => // ...    
}

這優雅嗎?並不是,但它有效。

現在我們的 action 已經處理了mutations,我們需要能夠在我們的程式碼中使用 Vuex 儲存。有兩種方法可以得到store。首先,通過使用應用程式(例如 main.ts/js)註冊store,你將可以存取一個集中式store, 你可以在應用程式的任何地方存取它:

// main.ts
import store from './store'

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

注意,這並不是在新增Vuex,而是你實際上正在建立的商店。一旦新增後, 你只需要呼叫useStore即可獲取store物件:

import { useStore } from "vuex";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const store = useStore();
    const books = computed(() => store.state.books);
    // ...

這很好用,但我更喜歡直接匯入store:

import store from "@/store";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const books = computed(() => store.state.books);
    // ...

既然你可以存取 store 物件,那麼如何使用它呢?對於state,你需要使用計算函數包裝它們,以便將它們更改並傳遞到你的繫結上:

export default defineComponent({
  setup() {

    const books = computed(() => store.state.books);

    return {
      books
    };
  },
});

要呼叫actions,你需要呼叫dispatch 方法:

export default defineComponent({
  setup() {

    const books = computed(() => store.state.books);

    onMounted(async () => await store.dispatch("loadBooks"));

    return {
      books
    };
  },
});

Actions可以在方法名稱後新增的引數。最後,要更改狀態,你需要像我們在 Actions 中所做的那樣去呼叫 commit。例如,我在 store 中有一個 分頁屬性,然後我可以使用commit更改狀態:

const incrementPage = () =>
  store.commit("setPage", store.state.currentPage + 1);
const decrementPage = () =>
  store.commit("setPage", store.state.currentPage - 1);

注意, 這樣呼叫可能會丟擲一個異常(因為你不能手動改變state):

const incrementPage = () => store.state.currentPage++;
  const decrementPage = () => store.state.currentPage--;

這是Vuex真正強大的地方,我們想要控制狀態改變的地方並且不會造成在開發過程中產生進一步錯誤。

你可能對 Vuex 中大量改變狀態的方式感到不知所措,但它確實可以幫助管理更大、更復雜的專案中的狀態。我不會說你在每種情況下都需要它,但是會有一些大型專案在總體上對你有幫助。

這就是 Vuex 5 旨在簡化 Vuex 在 TypeScript(以及一般的 JavaScript 專案)中的工作方式的地方。讓我們看看它在下一次釋出後將如何運作。

VUEX 5 #

注意: 此部分的程式碼位於GitHub 上範例專案的「Vuex5」分支中。

在撰寫本文時,Vuex 5 還沒有出現。這是一個 RFC(徵求意見)。這是計劃。是討論的起點。所以我在這裡說明的很多東西可能會發生變化。但是為了讓你們對 Vuex 的變化有所準備,我想讓你們瞭解一下它的發展方向。因此,與此範例關聯的程式碼不會生成。

Vuex工作原理的基本概念從一開始就沒有改變過。隨著Vue 3的引入,Vuex 4的建立主要允許Vuex在新專案中工作。但該團隊正試圖找到Vuex的真正痛點並解決它們。為此,他們正在計劃一些重要的改變:

  • 沒有更多的mutations: actions能改變mutation (可能任意的也可以)。
  • 更好的TypeScript支援。
  • 更好的多store功能。

那麼它是如何工作的呢?讓我們從建立store開始:

export default createStore({
  key: 'bookStore',
  state: () => ({
    isBusy: false,
    books: new Array<Work>()
  }),
  actions: {
    async loadBooks() {
      try {
        this.isBusy = true;
        const response = await bookService.getBooks();
        if (response.status === 200) {
          this.books = response.data.works;
        }
      } finally {
        this.isBusy = false;
      }
    }
  },
  getters: {
    findBook(key: string): Work | undefined {
      return this.books.find(b => b.key === key);
    }
  }
});

首先要看到的是,每個store現在都需要有自己的key。這允許你檢索多個store。接下來,你會注意到state物件現在是一個工廠(例如,從一個函數返回,不是在解析時建立的)。沒有mutations部分了。最後,在actions內部,你可以看到我們存取的狀態只是this指向的屬性。不需要再通過傳遞state來commit Actions。這不僅有助於簡化開發,還使TypeScript更容易推斷型別。

要將 Vuex 註冊到你的應用程式中,你將註冊 Vuex 而不是你的全域性store:

import { createVuex } from 'vuex'

createApp(App)
  .use(createVuex())
  .use(router)
  .mount('#app')

最後,要使用store,你將匯入store然後建立它的一個範例:

import bookStore from "@/store";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const store = bookStore(); // Generate the wrapper
    // ...

請注意,從商店返回的是一個工廠物件,無論你呼叫工廠多少次,它都會返回該商店的範例。返回只是一個具有單一類行為的actions、state和getters物件(帶有型別資訊):

onMounted(async () => await store.loadBooks());

const incrementPage = () => store.currentPage++;
const decrementPage = () => store.currentPage--;

你將在此處看到 state(例如currentPage)只是簡單的屬性。而actions(例如loadBooks)只是函數。實際上你在這裡使用的store是一個副作用。你可以將 Vuex 物件視為一個物件並繼續工作。這是 API 的重大改進。

另一個需要指出的重要變化是,你也可以使用類似於Composition API的語法來生成你的store:

export default defineStore("another", () => {

  // State
  const isBusy = ref(false);
  const books = reactive(new Array≷Work>());

  // Actions
  async function loadBooks() {
    try {
      this.isBusy = true;
      const response = await bookService.getBooks(this.currentTopic, this.currentPage);
      if (response.status === 200) {
        this.books = response.data.works;
      }
    } finally {
      this.isBusy = false;
    }
  }

  findBook(key: string): Work | undefined {
    return this.books.find(b => b.key === key);
  }

  // Getters
  const bookCount = computed(() => this.books.length);

  return {
    isBusy,
    books,
    loadBooks,
    findBook,
    bookCount
  }
});

這允許你像使用 Composition API 構建檢視那樣去構建 Vuex 物件,並且可以說它更簡單。

這種新設計的一個主要缺點是失去了狀態的不可變性。圍繞能夠啟用此功能(僅用於開發,就像 Vuex 4 一樣)進行了討論,但對此的重要性尚未達成共識。我個人認為這是 Vuex 的主要有點,但我們必須看看它是如何發揮作用的。

我們在哪裡?#

在單頁面應用程式中管理共用狀態是大多數應用程式開發的關鍵部分。在設計解決方案時,制定一個關於如何使用Vue的策略是非常重要的一步。在本文中,我向你展示了幾種管理共用狀態的模式,包括即將在Vuex 5中推出的模式。希望你現在擁有為自己的專案做出正確決策的知識。

原文地址:https://www.smashingmagazine.com/2021/06/managing-shared-state-vue3/

原文作者:Shawn Wildermuth

更多程式設計相關知識,請存取:!!

以上就是深入解析在Vue 3中如何管理共用狀態?【翻譯】的詳細內容,更多請關注TW511.COM其它相關文章!