基於vue3的小型圖書管理項目

前言

Vue3 練手項目,爲了加深對 composition-api 的理解,項目參考於 sl1673495/vue-bookshelf,不過這個項目仍是基於 vue2+composition-api,裏面對於組合函數的使用和理解仍是頗有幫助的,這裏用 Vue3 作了修改。html

項目地址:vue-bookshelfvue

項目中會用到的 Vue3 api,你須要在開始以前對它們有所瞭解:node

  • [x] Provide / Inject
  • [x] ref、reactive、watch、computed
  • [x] directive
  • [x] 生命週期函數
  • [x] v-model 多選項綁定

provide/inject代替vuex

Vue3 中新增的一對api,provideinject,能夠很方便的管理應用的全局狀態,有興趣能夠參考下這篇文章:Vue 3 store without Vuexreact

官方文檔對 Provide / Inject 的使用說明:Provide / Injectgit

利用這兩個api,在沒有vuex的狀況下也能夠很好的管理項目中的全局狀態:github

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

  • 加載圖書列表數據
  • 路由頁:未閱圖書列表/已閱圖書列表
  • 功能:設置圖書已閱、刪除圖書已閱

項目搭建

項目基於 vue-cli 搭建:vuex

  • typescript
  • vue3
  • vue-router
  • sass

context

項目基於 Provide/Inject 實現全局的圖書狀態管理,context/books.ts包含兩個組合函數:vue-cli

  • useBookListProvide 提供書籍的全局狀態管理和方法
  • useBookListInject 書籍狀態和方法注入(在須要的組件中使用)

在main.ts中,根組件注入全局狀態:typescript

// 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)
  },
}
相關文章
相關標籤/搜索