前言
Vue3 练手项目,为了加深对 composition-api
的理解,项目参考于 sl1673495/vue-bookshelf,不过这个项目还是基于 vue2+composition-api
,里面对于组合函数的使用和理解还是很有帮助的,这里用 Vue3
做了修改。
项目地址:vue-bookshelf
项目中会用到的 Vue3 api,你需要在开始之前对它们有所了解:
- [x] Provide / Inject
- [x] ref、reactive、watch、computed
- [x] directive
- [x] 生命周期函数
- [x] v-model 多选项绑定
provide/inject代替vuex
Vue3 中新增的一对api,provide
和 inject
,可以很方便的管理应用的全局状态,有兴趣可以参考下这篇文章:Vue 3 store without Vuex
官方文档对 Provide / Inject
的使用说明:Provide / Inject
利用这两个api,在没有vuex的情况下也可以很好的管理项目中的全局状态:
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
- vue3
- vue-router
- sass
context
项目基于 Provide/Inject
实现全局的图书状态管理,context/books.ts
包含两个组合函数:
useBookListProvide
提供书籍的全局状态管理和方法useBookListInject
书籍状态和方法注入(在需要的组件中使用)
在main.ts中,根组件注入全局状态:
// main.ts
import { createApp, h } from 'vue'
import App from './App.vue'
import { useBookListProvide } from '@/context'
const app = createApp({
setup() {
useBookListProvide();
return () => h(App)
}
})
组件中使用:
import { defineComponent } from "vue";
import { useBookListInject } from "@/context";
import { useAsync } from "@/hooks";
import { getBooks } from "@/hacks/fetch";
export default defineComponent({
name: "books",
setup() {
// 注入全局状态
const { setBooks, booksAvaluable } = useBookListInject();
// 获取数据的异步组合函数
const loading = useAsync(async () => {
const requestBooks = await getBooks();
setBooks(requestBooks);
});
return {
booksAvaluable,
loading,
};
}
});
组合函数 useAsync
目的是管理异步方法前后loading状态:
import { onMounted, ref } from 'vue'
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
}
组件中使用:
分页
对于分页这里使用组合函数 usePages
进行管理,目的是返回当前页的图书列表和分页组件所需的参数:
import { reactive, Ref, ref, watch } from 'vue'
export interface PageOption {
pageSize?: number
}
export function usePages(watchCallback: () => T[], pageOption?: PageOption) {
const { pageSize = 10 } = pageOption || {}
const rawData = ref([]) as Ref
const data = ref([]) as Ref
const bindings = reactive({
current: 1,
currentChange: (currentPage: number) => {
data.value = sliceData(rawData.value, currentPage)
},
})
const sliceData = (rawData: T[], currentPage: number) => {
return rawData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
watch(
watchCallback,
(value) => {
rawData.value = value
bindings.currentChange(1)
},
{
immediate: true,
}
)
return {
data,
bindings,
}
}
基于 composition-api
可以很方便的将统一的逻辑进行拆分,例如分页块的逻辑,很可能在其它的业务模块中使用,所以统一拆分到了hooks
文件夹下。
这里简单实现了分页插件,参考 element-plus/pagination 的分页组件。
Vue3 可以实现在组件上使用多个 v-model
进行双向数据绑定,让 v-model
的使用更加灵活,详情可查看官方文档 v-model。
项目中的分页组件也使用了v-model:current-page
的方式进行传参。
图片加载指令
vue3 的指令也做了更新: 官方文档-directives
主要是生命周期函数的变化:
const MyDirective = {
beforeMount(el, binding, vnode, prevVnode) {},
mounted() {},
beforeUpdate() {}, // new
updated() {},
beforeUnmount() {}, // new
unmounted() {}
}
项目中的指令主要是针对图片src做处理,directives/load-img-src.ts
:
// 图片加载指令,使用 ![](默认路径)
// 图片加载失败路径
const errorURL =
'https://imgservices-1252317822.image.myqcloud.com/image/20201015/45prvdakqe.svg'
const loadImgSrc = {
beforeMount(el: HTMLImageElement, binding: { value: string }) {
const imgURL = binding.value || ''
const img = new Image()
img.src = imgURL
img.onload = () => {
if (img.complete) {
el.src = imgURL
}
}
img.onerror = () => (el.src = errorURL)
},
}