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

简介: Vue3 Beta版发布了,离正式投入生产使用又更近了一步。此外,React Hook在社区的发展也是如火如荼。一时间大家都觉得Redux很low,都在研究各种各样配合hook实现的新形状态管理模式。

前言


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

一时间大家都觉得Redux很low,都在研究各种各样配合hook实现的新形状态管理模式。

在React社区中,Context + useReducer的新型状态管理模式广受好评,那么这种模式能不能套用到 Vue3 之中呢?

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

vue-composition-api-rfc:

https://vue-composition-api-rfc.netlify.com/api.html

vue官方提供的尝鲜库:

https://github.com/vuejs/composition-api


预览


可以在这里先预览一下这个图书管理的小型网页:

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

也可以直接看源码:

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


应用程序接口


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

在上层组件通过provide提供一些变量,在子组件中可以通过inject来拿到,但是必须在组件的对象里面声明,使用场景的也很少,所以之前我也并没有往状态管理的方向去想。

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

如果我们在context.ts中

  1. 1.自定义并export一个hook叫,并且在这个hook中使用provide并且注册一些全局状态,useProvide
  2. 2.再自定义并export一个hook叫,并且在这个hook中使用inject返回刚刚provide的全局状态,useInject
  3. 3.然后在根组件的setup函数中调用。useProvide
  4. 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. 1.查看图书
  2. 2.增加已阅图书
  3. 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里引入了一个函数,我编写它的目的是为了管理异步方法前后的loading状态,看一下它的实现。useAsync

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的作用就是把外部传入的异步方法在生命周期里调用

并且在调用的前后改变响应式变量的值,并且把loading返回出去,这样loading就可以在模板中自由使用,从而让loading这个变量和页面的渲染关联起来。funconMountedloading

Vue3的hooks让我们可以在组件外部调用Vue的所有能力,

包括onMounted,ref, reactive等等,

这使得自定义hook可以做非常多的事情,

并且在组件的setup函数把多个自定义hook组合起来完成逻辑,

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


增加分页Hook


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

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

<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>

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

其实这个方法在Hook内部会传给watch方法作为第一个参数,由于props是响应式的,所以对的读取自然也能收集到依赖,从而在外部传入的发生变化的时候,可以通知去重新执行回调函数。props.booksbookswatch

看一下的编写: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内部定义好了一些响应式的数据如分页后的数据,以及提供给组件的props对象,此后对于前端分页的需求来说,就可以通过在模板中使用Hook返回的值来轻松实现,而不用在每个组件都写一些、之类的重复逻辑了。datael-paginationelPagenationBindingsdatapageNo

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. 1.逻辑聚合 我们想要维护某一个功能的时候更加方便的能找到所有相关的逻辑,而不再是在选项突变,state,action的文件之间跳来跳去(一般跳到第三个的时候我可能就把第一个忘了)
  2. 2.和Vue3 api一致 不用像Vuex那样记忆很多琐碎的api(mutations, actions, getters, mapMutations, mapState ....这些甚至会作为面试题),Vue3的api学完了,这套状态管理机制自然就可以运用。
  3. 3.跳转清晰 在组件代码里看到,command + 点击后利用vscode的能力就可以跳转到代码定义的地方,一目了然的看到所有的逻辑。(想一下Vue2中vuex看到mapState,mapAction还得去对应的文件夹自己找,简直是...)useBookInject


总结


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

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

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

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

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

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

相关文章
|
1月前
|
JavaScript 前端开发 安全
Vue 3 + TypeScript 现代前端开发最佳实践(2025版指南)
每日激励:“如果没有天赋,那就一直重复”。我是蒋星熠Jaxonic,一名执着于代码宇宙的星际旅人。用Vue 3与TypeScript构建高效、可维护的前端系统,分享Composition API、状态管理、性能优化等实战经验,助力技术进阶。
Vue 3 + TypeScript 现代前端开发最佳实践(2025版指南)
|
3月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
395 1
|
5月前
|
自然语言处理 JavaScript 前端开发
一夜获千星!已获 1.7k+,Art Design Pro:Vue3 + Vite + TypeScript 打造的高颜值管理系统模板,这个让后端小哥直呼救命的后台系统
Art Design Pro 是一款基于 Vue 3、Vite 和 TypeScript 的高颜值后台管理系统模板,已获 1.7k+ 星标。项目专注于用户体验与视觉设计,支持主题切换、多语言、权限管理及图表展示等功能,内置常用业务组件,便于快速搭建现代化管理界面。其技术栈先进,开发体验流畅,适配多设备,满足企业级应用需求。项目地址:[GitHub](https://github.com/Daymychen/art-design-pro)。
861 11
|
6月前
|
JavaScript 前端开发 编译器
Vue与TypeScript:如何实现更强大的前端开发
Vue.js 以其简洁的语法和灵活的架构在前端开发中广受欢迎,而 TypeScript 作为一种静态类型语言,为 JavaScript 提供了强大的类型系统和编译时检查。将 Vue.js 与 TypeScript 结合使用,不仅可以提升代码的可维护性和可扩展性,还能减少运行时错误,提高开发效率。本文将介绍如何在 Vue.js 项目中使用 TypeScript,并通过一些代码示例展示其强大功能。
276 22
|
5月前
|
JavaScript API 开发者
Vue框架中常见指令的应用概述。
通过以上的详细解析,你应该已经初窥Vue.js的指令的威力了。它们是Vue声明式编程模型的核心之一,无论是构建简单的静态网站还是复杂的单页面应用,你都会经常用到。记住,尽管Vue提供了大量预定义的指令,你还可以创建自定义指令以满足特定的需求。为你的Vue应用程序加上这些功能增强器,让编码变得更轻松、更愉快吧!
102 1
|
9月前
|
JavaScript 安全 前端开发
Gzm Design:开源神器!用 Vue3、Vite4、TypeScript 革新海报设计,免费开源的海报设计器,主流技术打造,轻松高效
海报设计在各个领域都有着广泛的应用,无论是商业广告、活动宣传还是个人创意表达。今天要给大家介绍一款免费开源的海报设计器——Gzm Design,它基于最新的主流技术开发,为用户提供了丰富的功能,让海报设计变得轻松又高效。
518 64
|
9月前
|
JavaScript 数据安全/隐私保护
Vue Amazing UI 组件库(Vue3+TypeScript+Vite 等最新技术栈开发)
Vue Amazing UI 是一个基于 Vue 3、TypeScript、Vite 等最新技术栈开发构建的现代化组件库,包含丰富的 UI 组件和常用工具函数,并且持续不断维护更新中。另外,组件库全量使用 TypeScript,支持自动按需引入和 Tree Shaking 等,能够显著提升开发效率,降低开发成本。
546 5
Vue Amazing UI 组件库(Vue3+TypeScript+Vite 等最新技术栈开发)
|
10月前
|
敏捷开发 人工智能 JavaScript
Figma-Low-Code:快速将Figma设计转换为Vue.js应用,支持低代码渲染、数据绑定
Figma-Low-Code 是一个开源项目,能够直接将 Figma 设计转换为 Vue.js 应用程序,减少设计师与开发者之间的交接时间,支持低代码渲染和数据绑定。
647 3
Figma-Low-Code:快速将Figma设计转换为Vue.js应用,支持低代码渲染、数据绑定
|
10月前
|
缓存 NoSQL JavaScript
Vue.js应用结合Redis数据库:实践与优化
将Vue.js应用与Redis结合,可以实现高效的数据管理和快速响应的用户体验。通过合理的实践步骤和优化策略,可以充分发挥两者的优势,提高应用的性能和可靠性。希望本文能为您在实际开发中提供有价值的参考。
228 11
|
10月前
|
存储 设计模式 JavaScript
Vue 组件化开发:构建高质量应用的核心
本文深入探讨了 Vue.js 组件化开发的核心概念与最佳实践。
671 1