Vue3 Hook + TypeScript 取代 Vuex 实现图书管理小型应用

前言

Vue3 Beta版发布了,离正式投入生产使用又更近了一步。此外,React Hook在社区的发展也是如火如荼。javascript

一时间你们都以为Redux很low,都在研究各类各样配合hook实现的新形状态管理模式。
在React社区中,Context + useReducer的新型状态管理模式广受好评,那么这种模式能不能套用到 Vue3 之中呢?html

这篇文章就从Vue3的角度出发,探索一下将来的Vue状态管理模式。前端

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

vue官方提供的尝鲜库:
github.com/vuejs/compo…java

预览

能够在这里先预览一下这个图书管理的小型网页:react

sl1673495.github.io/vue-bookshe…git

也能够直接看源码:github

github.com/sl1673495/v…面试

api

Vue3中有一对新增的api,provideinject,熟悉Vue2的朋友应该明白,vue-router

在上层组件经过provide提供一些变量,在子组件中能够经过inject来拿到,可是必须在组件的对象里面声明,使用场景的也不多,因此以前我也并无往状态管理的方向去想。

可是Vue3中新增了Hook,而Hook的特征之一就是能够在组件外去写一些自定义Hook,因此咱们不光能够在.vue组件内部使用Vue的能力, 在任意的文件下(如context.ts)下也能够,

若是咱们在context.ts中

  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的初衷。

增长分页Hook

在某些场景中,前端也须要对数据作分页,配合Vue3的Hook,它会是怎样编写的呢?

进入Books这个UI组件,直接在这里把数据切分,而且引入Pagination组件。

<template>
  <section class="wrap">
    <span v-if="loading">正在加载中...</span>
    <section v-else class="content">
      <Book v-for="book in pagedBooks" :key="book.id" :book="book" />
      <el-pagination class="pagination" v-if="pagedBooks.length" :page-size="pageSize" :total="books.length" :current="elPagenationBindings.current" @current-change="elPagenationBindings.currentChange" />
    </section>
    <slot name="tips"></slot>
  </section>
</template>

<script lang="ts"> import { createComponent } from "@vue/composition-api"; import { usePages } from "@/hooks"; import { Books } from "@/types"; import Book from "./Book.vue"; export default createComponent({ name: "books", setup(props) { const pageSize = 10; const { elPagenationBindings, data: pagedBooks } = usePages( () => props.books as Books, { pageSize } ); return { elPagenationBindings, pagedBooks, pageSize }; }, props: { books: { type: Array, default: () => [] }, loading: { type: Boolean, default: false } }, components: { Book } }); </script>
复制代码

这里主要的逻辑就是用了usePages这个自定义Hook,有点奇怪的是第一项参数返回的是一个读取props.books的方法。

其实这个方法在Hook内部会传给watch方法做为第一个参数,因为props是响应式的,因此对props.books的读取天然也能收集到依赖,从而在外部传入的books发生变化的时候,能够通知watch去从新执行回调函数。

看一下usePages的编写:

import { watch, ref, reactive } from "@vue/composition-api";

export interface PageOption {
  pageSize?: number;
}

export function usePages<T>(watchCallback: () => T[], pageOption?: PageOption) {
  const { pageSize = 10 } = pageOption || {};

  const data = ref<T[]>([]);

  // 提供给el-pagination组件的参数
  const elPagenationBindings = reactive({
    current: 1,
    currentChange: (currnetPage: number) => {}
  });

  // 根据页数切分数据
  const sliceData = (currentData: T[], currentPage: number) => {
    return currentData.slice(
      (currentPage - 1) * pageSize,
      currentPage * pageSize
    );
  };

  watch(watchCallback, values => {
    const currentChange = (currnetPage: number) => {
      elPagenationBindings.current = currnetPage;
      data.value = sliceData(values, currnetPage);
    };
    currentChange(1);
    elPagenationBindings.currentChange = currentChange;
  });

  return {
    data,
    elPagenationBindings
  };
}
复制代码

Hook内部定义好了一些响应式的数据如分页后的数据data,以及提供给el-pagination组件的props对象elPagenationBindings,此后对于前端分页的需求来讲,就能够经过在模板中使用Hook返回的值来轻松实现,而不用在每一个组件都写一些datapageNo之类的重复逻辑了。

const { elPagenationBindings, data: pagedBooks } = usePages(
  () => props.books as Books,
  { pageSize: 10 }
);
复制代码

已阅图书

如何判断已阅后的图书,也能够经过在BookContext中返回一个函数,在组件中加以判断:

// 是否已阅
const hasReadedBook = (book: Book) => finishedBooks.value.includes(book)

provide(BookSymbol, {
  books,
  setBooks,
  finishedBooks,
  addFinishedBooks,
  removeFinishedBooks,
  hasReadedBook,
  booksAvaluable,
})

复制代码

StatusButton组件中:

<template>
  <button v-if="hasReaded" @click="removeFinish"></button>
  <button v-else @click="handleFinish"></button>
</template>

<script lang="ts"> import { createComponent } from "@vue/composition-api"; import { useBookListInject } from "@/context"; import { Book } from "../types"; interface Props { book: Book; } export default createComponent({ props: { book: Object }, setup(props: Props) { const { book } = props; const { addFinishedBooks, removeFinishedBooks, hasReadedBook } = useBookListInject(); const handleFinish = () => { addFinishedBooks(book); }; const removeFinish = () => { removeFinishedBooks(book); }; return { handleFinish, removeFinish, // 这里调用一下函数,轻松的判断出状态。 hasReaded: hasReadedBook(book) }; } }); </script>
复制代码

最终的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;
  removeFinishedBooks: (book: Book) => void;
  hasReadedBook: (book: Book) => boolean;
  booksAvaluable: 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 booksAvaluable = computed(() => {
    return books.value.filter(
      book => !finishedBooks.value.find(({ id }) => id === book.id)
    );
  });

  // 是否已阅
  const hasReadedBook = (book: Book) => finishedBooks.value.includes(book);

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

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区分,而是按照逻辑关注点分隔,

这样的好处显而易见,咱们想要维护某一个功能的时候更加方便的能找到全部相关的逻辑,而再也不是在选项和文件之间跳来跳去。

优势

  1. 逻辑聚合 咱们想要维护某一个功能的时候更加方便的能找到全部相关的逻辑,而再也不是在选项mutation,state,action的文件之间跳来跳去(通常跳到第三个的时候我可能就把第一个忘了)

  2. 和Vue3 api一致 不用像Vuex那样记忆不少琐碎的api(mutations, actions, getters, mapMutations, mapState ....这些甚至会做为面试题),Vue3的api学完了,这套状态管理机制天然就能够运用。

  3. 跳转清晰 在组件代码里看到useBookInject,command + 点击后利用vscode的能力就能够跳转到代码定义的地方,一目了然的看到全部的逻辑。(想一下Vue2中vuex看到mapState,mapAction还得去对应的文件夹本身找,简直是...)

总结

本文相关的全部代码都放在

github.com/sl1673495/v…

这个仓库里了,感兴趣的同窗能够去看,

在以前刚看到composition-api,还有尤大对于Vue3的Hook和React的Hook的区别对比的时候,我对于Vue3的Hook甚至有了一些盲目的崇拜,可是真正使用下来发现,虽然不须要咱们再去手动管理依赖项,可是因为Vue的响应式机制始终须要非原始的数据类型来保持响应式,所带来的一些心智负担也是须要注意和适应的。

另外,vuex-next也已经编写了一部分,我去看了一下,也是选择使用provideinject做为跨模块读取store的方法。vue-router-next同理,将来这两个api真的会大有做为。

整体来讲,Vue3虽然也有一些本身的缺点,可是带给咱们React Hook几乎全部的好处,并且还规避了React Hook的一些让人难以理解坑,在某些方面还优于它,期待Vue3正式版的发布!

求点赞

若是本文对你有帮助,就点个赞支持下吧,你的「赞」是我持续进行创做的动力,让我知道你喜欢看个人文章吧~

❤️感谢你们

抽奖时间,关注公众号有机会抽取「掘金小册5折优惠码」。

关注公众号「前端从进阶到入院」便可加我好友,我拉你进「前端进阶交流群」,你们一块儿共同交流和进步。

相关文章
相关标签/搜索