Vue3 Composition-Api + TypeScript + 新型狀態管理模式探索

前言

Vue3的熱度還沒過去,React Hook在社區的發展也是如火如荼。html

一時間你們都以爲Redux很low,都在研究各類各樣配合hook實現的新形狀態管理模式。vue

在React社區中,Context + useReducer的新型狀態管理模式廣受好評。react

這篇文章就從Vue3的角度出發,探索一下將來的Vue狀態管理模式。git

vue-composition-api-rfc:
vue-composition-api-rfc.netlify.com/api.htmlgithub

vue官方提供的嚐鮮庫:
github.com/vuejs/compo…vue-cli

api

Vue3中有一對新增的api,provideinject,熟悉Vue2的朋友應該明白,api

在上層組件經過provide提供一些變量,在子組件中能夠經過inject來拿到,可是必須在組件的對象裏面聲明,使用場景的也不多,因此以前我也並無往狀態管理的方向去想。app

可是Vue3中新增了Hook,而Hook的特徵之一就是能夠在組件外去寫一些自定義Hook,因此咱們不光能夠在.vue組件內部使用Vue的能力, 在任意的文件下(如context.ts)下也能夠,異步

若是咱們在context.ts中async

  1. 自定義並export一個hook叫useProvide,而且在這個hook中使用provide而且註冊一些全局狀態,

  2. 再自定義並export一個hook叫useInject,而且在這個hook中使用inject返回剛剛provide的全局狀態,

  3. 而後在根組件的setup函數中調用useProvide

  4. 就能夠在任意的子組件去共享這些全局狀態了。

順着這個思路,先看一下這兩個api的介紹,而後一塊兒慢慢探索這對api。

import { provide, inject } from 'vue'

const ThemeSymbol = Symbol()

const Ancestor = {
  setup() {
    provide(ThemeSymbol, 'dark')
  }
}

const Descendent = {
  setup() {
    const theme = inject(ThemeSymbol, 'light' /* optional default value */)
    return {
      theme
    }
  }
}
複製代碼

開始

項目介紹

這個項目是一個簡單的圖書管理應用,功能很簡單:

  1. 查看圖書
  2. 增長已閱圖書
  3. 刪除已閱圖書

項目搭建

首先使用vue-cli搭建一個項目,在選擇依賴的時候手動選擇,這個項目中我使用了TypeScript,各位小夥伴能夠按需選擇。

而後引入官方提供的vue-composition-api庫,而且在main.ts裏註冊。

import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
複製代碼

context編寫

按照剛剛的思路,我創建了src/context/books.ts

import { provide, inject, computed, ref, Ref } from '@vue/composition-api';
import { Book, Books } from '@/types';

type BookContext = {
  books: Ref<Books>;
  setBooks: (value: Books) => void;
};

const BookSymbol = Symbol();

export const useBookListProvide = () => {
  // 所有圖書
  const books = ref<Books>([]);
  const setBooks = (value: Books) => (books.value = value);

  provide(BookSymbol, {
    books,
    setBooks,
  });
};

export const useBookListInject = () => {
  const booksContext = inject<BookContext>(BookSymbol);

  if (!booksContext) {
    throw new Error(`useBookListInject must be used after useBookListProvide`);
  }

  return booksContext;
};

複製代碼

全局狀態確定不止一個模塊,因此在context/index.ts下作統一的導出

import { useBookListProvide, useBookListInject } from './books';

export { useBookListInject };

export const useProvider = () => {
  useBookListProvide();
};
複製代碼

後續若是增長模塊的話,就按照這個套路就好。

而後在main.ts的根組件裏使用provide,在最上層的組件中注入全局狀態。

new Vue({
  router,
  setup() {
    useProvider();
    return {};
  },
  render: h => h(App),
}).$mount('#app');
複製代碼

在組件view/books.vue中使用:

<template>
  <Books :books="books" :loading="loading" /> </template>

<script lang="ts">
import { createComponent } from '@vue/composition-api';
import Books from '@/components/Books.vue';
import { useAsync } from '@/hooks';
import { getBooks } from '@/hacks/fetch';
import { useBookListInject } from '@/context';

export default createComponent({
  name: 'books',
  setup() {
    const { books, setBooks } = useBookListInject();

    const loading = useAsync(async () => {
      const requestBooks = await getBooks();
      setBooks(requestBooks);
    });

    return { books, loading };
  },
  components: {
    Books,
  },
});
</script>
複製代碼

這個頁面須要初始化books的數據,而且從inject中拿到setBooks的方法並調用,以後這份books數據就能夠供全部組件使用了。

在setup裏引入了一個useAsync函數,我編寫它的目的是爲了管理異步方法先後的loading狀態,看一下它的實現。

import { ref, onMounted } from '@vue/composition-api';

export const useAsync = (func: () => Promise<any>) => {
  const loading = ref(false);

  onMounted(async () => {
    try {
      loading.value = true;
      await func();
    } catch (error) {
      throw error;
    } finally {
      loading.value = false;
    }
  });

  return loading;
};
複製代碼

能夠看出,這個hook的做用就是把外部傳入的異步方法funconMounted生命週期裏調用
而且在調用的先後改變響應式變量loading的值,而且把loading返回出去,這樣loading就能夠在模板中自由使用,從而讓loading這個變量和頁面的渲染關聯起來。

Vue3的hooks讓咱們能夠在組件外部調用Vue的全部能力,
包括onMounted,ref, reactive等等,

這使得自定義hook能夠作很是多的事情,
而且在組件的setup函數把多個自定義hook組合起來完成邏輯,

這恐怕也是起名叫composition-api的初衷。

最終的books模塊context

import { provide, inject, computed, ref, Ref } from '@vue/composition-api';
import { Book, Books } from '@/types';

type BookContext = {
  books: Ref<Books>;
  setBooks: (value: Books) => void;
  finishedBooks: Ref<Books>;
  addFinishedBooks: (book: Book) => void;
  booksAvailable: Ref<Books>;
};

const BookSymbol = Symbol();

export const useBookListProvide = () => {
  // 待完成圖書
  const books = ref<Books>([]);
  const setBooks = (value: Books) => (books.value = value);

  // 已完成圖書
  const finishedBooks = ref<Books>([]);
  const addFinishedBooks = (book: Book) => {
    if (!finishedBooks.value.find(({ id }) => id === book.id)) {
      finishedBooks.value.push(book);
    }
  };
  const removeFinishedBooks = (book: Book) => {
    const removeIndex = finishedBooks.value.findIndex(({ id }) => id === book.id);
    if (removeIndex !== -1) {
      finishedBooks.value.splice(removeIndex, 1);
    }
  };

  // 可選圖書
  const booksAvailable = computed(() => {
    return books.value.filter(book => !finishedBooks.value.find(({ id }) => id === book.id));
  });

  provide(BookSymbol, {
    books,
    setBooks,
    finishedBooks,
    addFinishedBooks,
    removeFinishedBooks,
    booksAvailable,
  });
};

export const useBookListInject = () => {
  const booksContext = inject<BookContext>(BookSymbol);

  if (!booksContext) {
    throw new Error(`useBookListInject must be used after useBookListProvide`);
  }

  return booksContext;
};
複製代碼

最終的books模塊就是這個樣子了,能夠看到在hooks的模式下,

代碼再也不按照state, mutation和actions區分,而是按照邏輯關注點分隔,

這樣的好處顯而易見,咱們想要維護某一個功能的時候更加方便的能找到全部相關的邏輯,而再也不是在選項和文件之間跳來跳去。

總結

本文相關的全部代碼都放在

github.com/sl1673495/v…

這個倉庫裏了,感興趣的同窗能夠去看,

在以前剛看到composition-api,還有尤大對於Vue3的Hook和React的Hook的區別對比的時候,我對於Vue3的Hook甚至有了一些盲目的崇拜,可是真正使用下來發現,雖然不須要咱們再去手動管理依賴項,可是因爲Vue的響應式機制始終須要非原始的數據類型來保持響應式,所帶來的一些心智負擔也是須要注意和適應的。

舉個簡單的例子

setup() {
    const loading = useAsync(async () => {
      await getBooks();
    });

    return {
      isLoading: !!loading.value
    }
  },
複製代碼

這一段看似符合直覺的代碼,卻會讓isLoading這個變量失去響應式,可是這也是性能和內部實現設計的一些取捨,咱們選擇了Vue,也須要去學習和習慣它。

整體來講,Vue3雖然也有一些本身的缺點,可是帶給咱們React Hook幾乎全部的好處,並且還規避了React Hook的一些讓人難以理解坑,在某些方面還優於它,期待Vue3正式版的發佈!

相關文章
相關標籤/搜索