

抛弃Vuex!Vue3 Composition-Api + TypeScript + 新型状态管理模式探索。
source link: https://juejin.im/post/5e0da5606fb9a048483ecf64
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Vue3 Composition-Api + TypeScript + 新型状态管理模式探索。
Vue3 Beta 版发布了,离正式投入生产使用又更近了一步。此外,React Hook 在社区的发 展也是如火如荼。
在 React 社区中,Context + useReducer 的新型状态管理模式广受好评,那么这种模式能 不能套用到 Vue3 之中呢?
这篇文章就从 Vue3 的角度出发,探索一下未来的 Vue 状态管理模式。
vue-composition-api-rfc:
vue-composition-api-rfc.netlify.com/api.html
vue 官方提供的尝鲜库:
github.com/vuejs/compo…
可以在这里先预览一下这个图书管理的小型网页:
也可以直接看源码:
Vue3 中有一对新增的 api,provide
和inject
,熟悉 Vue2 的朋友应该明白,
在上层组件通过 provide 提供一些变量,在子组件中可以通过 inject 来拿到,但是必须 在组件的对象里面声明,使用场景的也很少,所以之前我也并没有往状态管理的方向去想。
但是 Vue3 中新增了 Hook,而 Hook 的特征之一就是可以在组件外去写一些自定义 Hook, 所以我们不光可以在.vue 组件内部使用 Vue 的能力, 在任意的文件下(如 context.ts) 下也可以,
如果我们在 context.ts 中
-
自定义并 export 一个 hook 叫
useProvide
,并且在这个 hook 中使用 provide 并且 注册一些全局状态, -
再自定义并 export 一个 hook 叫
useInject
,并且在这个 hook 中使用 inject 返回 刚刚 provide 的全局状态, -
然后在根组件的 setup 函数中调用
useProvide
。 -
就可以在任意的子组件去共享这些全局状态了。
顺着这个思路,先看一下这两个 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,
}
},
}
复制代码
这个项目是一个简单的图书管理应用,功能很简单:
- 增加已阅图书
- 删除已阅图书
首先使用 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 的作用就是把外部传入的异步方法func
在onMounted
生命周期里
调用
并且在调用的前后改变响应式变量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="bindings.current"
@current-change="bindings.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 { bindings, data: pagedBooks } = usePages(
() => props.books as Books,
{ pageSize }
);
return {
bindings,
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 rawData = ref<T[]>([])
const data = ref<T[]>([])
// 提供给el-pagination组件的参数
const bindings = reactive({
current: 1,
currentChange: (currnetPage: number) => {
data.value = sliceData(rawData.value, currnetPage)
},
})
// 根据页数切分数据
const sliceData = (rawData: T[], currentPage: number) => {
return rawData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
watch(watchCallback, values => {
// 更新原始数据
rawData.value = values
bindings.currentChange(1)
})
return {
data,
bindings,
}
}
复制代码
Hook 内部定义好了一些响应式的数据如原始数据rawData
,分页后的数据data
,以及提
供给el-pagination
组件的 props 对象bindings
。并且在 watch 到原始数据变化后,
也会及时同步 Hook 中的数据。
此后对于前端分页的需求来说,就可以通过在模板中使用 Hook 返回的值来轻松实现,而不
用在每个组件都写一些data
、pageNo
之类的重复逻辑了。
const {bindings, 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 区分,而是按照逻辑关注点分隔,
这样的好处显而易见,我们想要维护某一个功能的时候更加方便的能找到所有相关的逻辑, 而不再是在选项和文件之间跳来跳去。
-
逻辑聚合 我们想要维护某一个功能的时候更加方便的能找到所有相关的逻辑,而不 再是在选项 mutation,state,action 的文件之间跳来跳去(一般跳到第三个的时候我 可能就把第一个忘了)
-
和 Vue3 api 一致 不用像 Vuex 那样记忆很多琐碎的 api(mutations, actions, getters, mapMutations, mapState ....这些甚至会作为面试题),Vue3 的 api 学完了 ,这套状态管理机制自然就可以运用。
-
跳转清晰 在组件代码里看到
useBookInject
,command + 点击后利用 vscode 的 能力就可以跳转到代码定义的地方,一目了然的看到所有的逻辑。(想一下 Vue2 中 vuex 看到 mapState,mapAction 还得去对应的文件夹自己找,简直是...)
本文相关的所有代码都放在
这个仓库里了,感兴趣的同学可以去看,
在之前刚看到 composition-api,还有尤大对于 Vue3 的 Hook 和 React 的 Hook 的区别 对比的时候,我对于 Vue3 的 Hook 甚至有了一些盲目的崇拜,但是真正使用下来发现,虽 然不需要我们再去手动管理依赖项,但是由于 Vue 的响应式机制始终需要非原始的数据类 型来保持响应式,所带来的一些心智负担也是需要注意和适应的。
另外,vuex-next 也已经编写了一部分,我去看了一下,也是选择使
用provide
和inject
作为跨模块读取store
的方法。vue-router-next 同理,未来这两
个 api 真的会大有作为。
总体来说,Vue3 虽然也有一些自己的缺点,但是带给我们 React Hook 几乎所有的好处, 而且还规避了 React Hook 的一些让人难以理解坑,在某些方面还优于它,期待 Vue3 正式 版的发布!
如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我持续进行创作的动力,让我知道 你喜欢看我的文章吧~
❤️ 感谢大家
抽奖时间,关注公众号有机会抽取「掘金小册 5 折优惠码」。
关注公众号「前端从进阶到入院」即可加我好友,我拉你进「前端进阶交流群」,大家一起 共同交流和进步。
Recommend
-
81
随着医疗信息化近年来的迅速发展,医疗技术与就医服务水平都上升到了一个新的台阶。越来越多的大型现代化医院开始利用信息化大幅度提升了医院的服务能力,优化就医流程,增强管理效率,从而使医务人员在单位时间能够提供更多的服务,让病人轻...
-
31
程序员 - @lihongjie0209 - 这样做的好处很明显1. 包只需要全局存在, 不同版本的包在不同的文件夹, 全局只需要下载一次就好了2. 使用包只需要在启动时指定相关的 PATH 就可以了这样可以完美的避免每个项目
-
46
一、缓存设计 1、缓存的作用 在业务系统中,查询时最容易出现性能问题的模块,查询面对的数据量大,筛选条件复杂,所以在系统架构中引入缓存层,则是非常必要的,用来缓存热点数据,达到快速响应的...
-
5
深度剖析四类去中心化资产管理模式及发展前景HashKey Me2021-04-19热度: 7409去中心化的资产管理已经初步具备了的较完备模式,随着资产类型...
-
6
腾讯与清华大学牵手大数据科研 构建卫生健康智慧管理模式_大数据资讯_中国IDC圈 腾讯与清华大学牵手大数据科研 构建卫生健康智慧管理模式 健康大数据是清华大学万科公共卫生与健康学院四个重点学科方向之一。...
-
4
什么是合伙人管理模式?电商团队合伙人如何分配利润? 2021年6月4日未来的商业社会一定是合伙人模式。例如,阿里巴巴的
-
6
英科医疗启动股东福利活动,不断丰富投资者关系管理模式2021-06-24 00:00:00 来源:投资家网 作者: 6月23日,英科医疗(300677.SZ)在其微信公众号“INTCO英科医疗”启动公司创新型投资者关系管理模式。...
-
10
For various reasons, many people are still using Vue 2 and have some doubts about migrating to Vue 3. Others are only thinking of trying Vue for the first time, and they don’t know which version to start with. In this article, we’ll try to ex...
-
3
为什么很多新型编程语言都抛弃了 C 语言风格的 for 语句?
-
9
Revisiting SQL composition in JavaScript and TypeScript ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK