基于Vue3的小型图书管理项目

前言

Vue3 练手项目,为了加深对 composition-api 的理解,项目参考于 sl1673495/vue-bookshelf,不过这个项目还是基于 vue2+composition-api,里面对于组合函数的使用和理解还是很有帮助的,这里用 Vue3 做了修改。

项目地址:vue-bookshelf

你需要在项目开始之前对 Vue3 的api有所了解,包括但不限于下面👇的api:

  • Provide / Inject
  • ref、reactive、watch、computed
  • directive
  • 生命周期函数
  • v-model 多选项绑定

provide/inject代替vuex

Vue3 中新增的一对api,provideinject,可以很方便的管理应用的全局状态,有兴趣可以参考下这篇文章: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<any>) => {
  const loading = ref(false)
  onMounted(async () => {
    try {
      loading.value = true
      await func()
    } catch (error) {
      throw error
    } finally {
      loading.value = false
    }
  })

  return loading
}

组件中使用:

<Books :books="booksAvaluable" :loading="loading"></Books>

分页

对于分页这里使用组合函数 usePages 进行管理,目的是返回当前页的图书列表和分页组件所需的参数:

import { reactive, Ref, ref, watch } from 'vue'

export interface PageOption {
  pageSize?: number
}

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

  const rawData = ref([]) as Ref<T[]>
  const data = ref([]) as Ref<T[]>

  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 的分页组件。

<Pagination
  class="pagination"
  :total="books.length"
  :page-size="pageSize"
  :hide-on-single-page="true"
  v-model:current-page="bindings.current"
  @current-change="bindings.currentChange"
/>

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)
  },
}