关于 vue3 composable function(hook)的一些遐想

308 阅读3分钟

前提

最近初步完成了公司项目的vue2到vue3的迁移,目前正在用composable function也就是常说的hooks的开发范式来重构业务代码,与此同时我也开发了vue3-easy-data-table这款高可定制性的data table组件,在开始这篇文章之前,有必要简单的了解如何使用vue3-easy-data-table,关于这个组件可以参考我的另一篇介绍文章或者在线文档

定制table footer

template refs + 计算属性:

为了让用户可以定制的table footer,vue3-easy-data-table暴露了一些分页相关的变量和方法:

截屏2022-07-31 下午11.44.57.png

我们可以通过template refs来获取和使用这些变量和方法,然后像下面的例子一样定制自己的footer:

先上效果:

chrome-capture-2022-6-31.gif

代码:

<script lang="ts" setup>
import type { Header, Item } from "vue3-easy-data-table";
import { computed, ref } from 'vue';
import { mockClientItems } from "../mock";

// $ref dataTable
const dataTable = ref();

// index related
const currentPageFirstIndex = computed(() => dataTable.value?.currentPageFirstIndex);
const currentPageLastIndex = computed(() => dataTable.value?.currentPageLastIndex);
const clientItemsLength = computed(() => dataTable.value?.clientItemsLength);

// pagination related
const maxPaginationNumber = computed(() => dataTable.value?.maxPaginationNumber);
const currentPaginationNumber = computed(() => dataTable.value?.currentPaginationNumber);

const isFirstPage = computed(() => dataTable.value?.isFirstPage);
const isLastPage = computed(() => dataTable.value?.isLastPage);

const nextPage = () => {
  dataTable.value.nextPage();
};
const prevPage = () => {
  dataTable.value.prevPage();
};
const updatePage = (paginationNumber: number) => {
  dataTable.value.updatePage(paginationNumber);
};

const headers: Header[] = [
  { text: "Name", value: "name" },
  { text: "Address", value: "address" },
  { text: "Height", value: "height", sortable: true },
  { text: "Weight", value: "weight", sortable: true },
  { text: "Age", value: "age", sortable: true },
  { text: "Favourite sport", value: "favouriteSport" },
  { text: "Favourite fruits", value: "favouriteFruits" },
];

const items: Item[] = mockClientItems(200);
</script>

从上面的代码可以看出来,其实本质上就是通过template refs来获取vue3-easy-data-table内部暴露出来的变量,然后通过计算属性获得这些变量的computed ref,最后在template中使用这些reactive的变量并配合css(或scss)样式代码来定制footer:

<template>
  <EasyDataTable
    ref="dataTable"
    :headers="headers"
    :items="items"
    :rows-per-page="10"
    :show-footer="false"
  />
  
  <div class="customize-footer">
    <div class="customize-index">
      Now displaying: {{currentPageFirstIndex}} ~ {{currentPageLastIndex}} of {{clientItemsLength}}
    </div>
  
    <div class="customize-buttons">
      <span
        v-for="paginationNumber in maxPaginationNumber"
        class="customize-button"
        :class="{'active': paginationNumber === currentPaginationNumber}"
        @click="updatePage(paginationNumber)"
      >
        {{paginationNumber}}
      </span>
    </div>
  
    <div class="customize-pagination">
      <button class="prev-page" @click="prevPage" :disabled="isFirstPage">prev page</button>
      <button class="next-page" @click="nextPage" :disabled="isLastPage">next page</button>
    </div>
  </div>
</template>

<style scoped>
.customize-footer {
  margin: 5px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.customize-footer div {
  margin: 5px;
}
.customize-button {
  display: inline-block;
  width: 20px;
  height: 20px;
  text-align: center;
  border-radius: 100%;
  cursor: pointer;
  padding: 3px;
  line-height: 20px;
}
.customize-button.active {
  color: #fff;
  background-color: #3db07f;
}
.customize-pagination button {
  margin: 0 5px;
  cursor: pointer;
}
</style>

Composable function(hook)

如果我写一个函数,参数是table的template ref,返回值是分页相关的state,以及更新state用的action,那么这个函数不就是一个composable function(hook)了吗,于是我写了usePagination这个hook:

usePagination.ts:

import { computed, Ref } from 'vue';

export type DataTableRef = Ref<null | {
  currentPageFirstIndex: number
  currentPageLastIndex: number
  clientItemsLength: number
  maxPaginationNumber: number
  currentPaginationNumber: number
  isFirstPage: boolean
  isLastPage: boolean
  nextPage: () => void
  prevPage: () => void
  updatePage: (page: number) => void
}>

export function usePagination(
  dataTableRef: DataTableRef,
) {
  // index related
  const currentPageFirstIndex = computed(() => dataTableRef.value?.currentPageFirstIndex);
  const currentPageLastIndex = computed(() => dataTableRef.value?.currentPageLastIndex);
  const clientItemsLength = computed(() => dataTableRef.value?.clientItemsLength);
  
  // pagination related
  const maxPaginationNumber = computed(() => dataTableRef.value?.maxPaginationNumber);
  const currentPaginationNumber = computed(() => dataTableRef.value?.currentPaginationNumber);
  
  const isFirstPage = computed(() => dataTableRef.value?.isFirstPage);
  const isLastPage = computed(() => dataTableRef.value?.isLastPage);
  
  const nextPage = () => {
    dataTableRef.value?.nextPage();
  };
  const prevPage = () => {
    dataTableRef.value?.prevPage();
  };
  const updatePage = (paginationNumber: number) => {
    dataTableRef.value?.updatePage(paginationNumber);
  };

  return {
    currentPageFirstIndex,
    currentPageLastIndex,
    clientItemsLength,
    maxPaginationNumber,
    currentPaginationNumber,
    isFirstPage,
    isLastPage,
    nextPage,
    prevPage,
    updatePage,
  }
}

export type UsePaginationReturn = ReturnType<typeof usePagination>

使用:

<script lang="ts" setup>
import type { Header, Item } from "vue3-easy-data-table";
import { computed, ref } from "vue";
import { mockClientItems } from "../mock";
import { usePagination } from "use-vue3-easy-data-table";
import type { UsePaginationReturn } from "use-vue3-easy-data-table";

const dataTable = ref();

const {
  currentPageFirstIndex,
  currentPageLastIndex,
  clientItemsLength,
  maxPaginationNumber,
  currentPaginationNumber,
  isFirstPage,
  isLastPage,
  nextPage,
  prevPage,
  updatePage,
}: UsePaginationReturn = usePagination(dataTable);

const headers: Header[] = [
  { text: "Name", value: "name" },
  { text: "Address", value: "address" },
  { text: "Height", value: "height", sortable: true },
  { text: "Weight", value: "weight", sortable: true },
  { text: "Age", value: "age", sortable: true },
  { text: "Favourite sport", value: "favouriteSport" },
  { text: "Favourite fruits", value: "favouriteFruits" },
];

const items: Item[] = mockClientItems(200);
</script>

对比一下上面两种方法(template refs + 计算属性和composable function),是不是composable function的script部分更加简洁了,我把computed的定义都移到了usePagination这个composable function的内部,因为对于想要定制footer的用户来说,他们需要的结果就只是footer相关的state(reactive的变量)和action(方法),所以我就提供给他们相应的composable function(hook)用来返回他们想要的结果。

遐想

看到这里你有没有发现,其实composable function(hook)更好的将view层(html+css)和业务逻辑层(js)分开了,以后前端开发会不会有这样的一种分工模式呢,比如,团队有两个前端开发,一个前端开发A专注于composable function(hook)的开发,开发好之后将这些composable function(hook)提供给另一个前端开发B,前段开发B专注于view层(template+style)的开发,再或者说view基本交给设计师通过figma之类的设计软件来生成,前端开发从设计师那边拿到html+css后,通过composable function(hook)的开发来最终整合和开发前端程序呢?大家怎么觉得呢🤔?

最后

如果你觉得这篇文章不错,欢迎点赞,也欢迎给我个github star⭐支持我,谢谢。
项目地址:github.com/HC200ok/vue…