onmounted vue3_Vue3.0 尝鲜 Hook TypeScript 取代 Vuex【项目实践】

4bc2ba35ee5111340bd074274b42908c.png

作者:ssh 前端从进阶到入院

前言

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

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

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

推荐文章:

聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总

vue官方提供的尝鲜库:https://github.com/vuejs/composition-api

预览

10f0926af57fd225e529042300db2f39.png

直接看源码:

https://github.com/sl1673495/vue-bookshelf

api

Vue3中有一对新增的api,provide和inject,熟悉Vue2的朋友应该明白,

在上层组件通过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;  setBooks: (value: Books) => void;};const BookSymbol = Symbol();export const useBookListProvide = () => {  // 全部图书  const books = ref([]);  const setBooks = (value: Books) => (books.value = value);  provide(BookSymbol, {    books,    setBooks,  });};export const useBookListInject = () => {  const booksContext = inject(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中使用:

  

这个页面需要初始化books的数据,并且从inject中拿到setBooks的方法并调用,之后这份books数据就可以供所有组件使用了。

在setup里引入了一个useAsync函数,我编写它的目的是为了管理异步方法前后的loading状态,看一下它的实现。

import { ref, onMounted } from '@vue/composition-api';export const useAsync = (func: () => Promise) => {  const loading = ref(false);  onMounted(async () => {    try {      loading.value = true;      await func();    } catch (error) {      throw error;    } finally {      loading.value = false;    }  });  return loading;};

可以看出,这个hook的作用就是把外部传入的异步方法func在onMounted生命周期里调用并且在调用的前后改变响应式变量loading的值,并且把loading返回出去,这样loading就可以在模板中自由使用,从而让loading这个变量和页面的渲染关联起来。

Vue3的hooks让我们可以在组件外部调用Vue的所有能力,包括onMounted,ref, reactive等等,

这使得自定义hook可以做非常多的事情,并且在组件的setup函数把多个自定义hook组合起来完成逻辑,

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

增加分页Hook

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

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

d49c00d42203b107dc5a3fa8cc0d3c80.gif
      正在加载中...                          

这里主要的逻辑就是用了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(watchCallback: () => T[], pageOption?: PageOption) {  const { pageSize = 10 } = pageOption || {};  const data = ref([]);  // 提供给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返回的值来轻松实现,而不用在每个组件都写一些data、pageNo之类的重复逻辑了。

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

已阅图书

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

7ce2871c96c9845eabe9827e59206c3c.png
// 是否已阅const hasReadedBook = (book: Book) => finishedBooks.value.includes(book)provide(BookSymbol, {  books,  setBooks,  finishedBooks,  addFinishedBooks,  removeFinishedBooks,  hasReadedBook,  booksAvaluable,})

在StatusButton组件中:

  删  阅

最终的books模块context

import { provide, inject, computed, ref, Ref } from "@vue/composition-api";import { Book, Books } from "@/types";type BookContext = {  books: Ref;  setBooks: (value: Books) => void;  finishedBooks: Ref;  addFinishedBooks: (book: Book) => void;  removeFinishedBooks: (book: Book) => void;  hasReadedBook: (book: Book) => boolean;  booksAvaluable: Ref;};const BookSymbol = Symbol();export const useBookListProvide = () => {  // 全部图书  const books = ref([]);  const setBooks = (value: Books) => (books.value = value);  // 已完成图书  const finishedBooks = ref([]);  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(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还得去对应的文件夹自己找,简直是...)

总结

本文相关的所有代码都放在

https://github.com/sl1673495/vue-bookshelf

这个仓库里了,感兴趣的同学可以去看,

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

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

总体来说,Vue3虽然也有一些自己的缺点,但是带给我们React Hook几乎所有的好处,而且还规避了React Hook的一些让人难以理解坑,在某些方面还优于它,期待Vue3正式版的发布!

求点赞

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

作者:ssh 前端从进阶到入院

转发链接:https://mp.weixin.qq.com/s/qD_acbCw3Vv8uP7aOHuqhQ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值