vue3封装Hooks实现更好的逻辑复用
在构建前端应用中,经常会需要复用公共任务的逻辑。例如在一个管理后台中会存在很多的表格,其中的增删查改的内容就是可复用的部分,我们在写页面的过程中如果不封装的话会发现我们经常复制/粘贴。
在Vue3的Composition API中引入了Hooks的概念,为我们提供了更灵活和可组合的代码组织方式。其中的一个重要概念就是Hooks。
Hooks
是一种函数,以use
开头,用于封装一些可复用的逻辑。它们提供了一种在函数组件中复用状态逻辑的方式,使我们能够更好地组织组件代码,将相关的逻辑聚合在一起,实现更高水平的可维护性。
Vue3官网的案例分析
在vue3的官网中为我们介绍了一个案例,我们先分析这个案例后再实战自己封装一个useTable
的逻辑。
引入官网的鼠标跟踪案例:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
在上述代码中实现了对鼠标位置实时跟踪的功能。我们发现案例中的x、y的状态在鼠标移动时发生变化。不同于封装一个时间格式化的函数,我们只需要传入时间和传出时间,其中的时间是无状态的。因此我们要如何封装这样的公共逻辑呢?
我们将上面的逻辑以一个组合式函数的形式提取到外部文件中:
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
在组件中的使用方式:
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
我们可以看到,上面封装的函数中向外暴露了我们组件中需要使用的返回值x和y,并且在组件中是可以实现响应式变化的。
上述案例大概让你明白了为什么要封装Hooks函数和如何封装和使用一个Hooks函数。
接下来我们分析一下怎么将前端项目中的Table使用逻辑封装成一个useTable
的组合式函数。
封装useBorder Hooks
因为项目中使用了dataV组件,当页面宽高发生变化时组件的宽高总是无法自适应,官网给出了一个解决方案:
实现代码如下:
<BorderBox8 class="borderbox8" :key="key"></BorderBox8>
...
// dataV宽高自适应的key
let key = ref(1);
// 宽高改变框图自适应
function initBorder() {
window.addEventListener('resize', function () {
nextTick(() => {
key.value++;
});
});
}
initBorder();
我们在多个页面都使用了dataV的组件,因此我们在多个地方都需要上述代码来实现dataV的宽高自适应。因此就想重用这段代码。在页面发生resize
后key值是在发生变化的,因此我们可以用组合式函数来实现复用:
/*
* @Description: 公用的Datav初始化逻辑,在使用dataV组件时使用
*/
import { ref, onMounted, nextTick } from 'vue';
export function useBorder() {
const key = ref<number>(1);
function initBorder() {
nextTick(() => {
console.log('resize', key.value);
key.value++;
});
}
onMounted(() => {
window.addEventListener('resize', initBorder);
});
return { key };
}
在页面中使用:
import { useBorder } from '@/hooks/useBorder';
const { key } = useBorder();
封装useTable Hooks
以下案例用我目前在开发的项目作为示例,采用Typescrip
t进行函数封装。
需要提供的功能:请求表格数据,传递分页信息
在useTable
中我们要实现请求接口数据,并暴露请求后的数据。因此我们需要向里面传递请求api,通过在不同的地方传入不同的请求api来实现请求不同的数据。
export interface ListResponse<R extends object> {
code: number;
data: {
list: R[];
};
}
/*
* @Description: 公用的table逻辑,在使用表格时使用
*/
import { HttpResponse } from '@/utils/request';
import { Paging, ListResponse } from '@/api/types';
import { computed, reactive, toRefs } from 'vue';
interface TableState<R extends object> {
tableData: R[];
loading: boolean;
}
interface OtherSearchParams {
[key: string]: unknown;
}
interface TableBaseMethodsType<R extends object> {
requestApi: (
page: number,
keyword?: string
) => Promise<HttpResponse<ListResponse<R>>>;
otherSearchParams?: OtherSearchParams;
}
// table的常见数据请求和分页交互
// 接收:表格数据的范型T, 请求方法以及参数
// 抛出以下:
export default function useTable<R extends object>(
config: TableBaseMethodsType<R>
) {
const { otherSearchParams = {}, requestApi } = config;
const tableState = reactive<TableState<R>>({
loading: false,
tableData: []
});
// 分页参数
const pageParams = reactive<Paging>({
page: 1
});
// 分页器配置项
const pagination = computed(() => {
return {
total: tableState.tableData.length,
curruntPage: pageParams.page
};
});
/**
* @description: 请求加载数据
* @return {*}
*/
const loadList = async () => {
const page = pageParams.page;
const params = { ...otherSearchParams };
// console.log(params, 'params');
tableState.loading = true;
try {
const res = await requestApi(page, params.keyword);
console.log(res);
if (res.status === 200) {
console.log(res.data.data.list, 'data');
tableState.tableData = res.data.data.list;
console.log(tableState.tableData, '111');
}
} catch (error) {
console.log(error);
tableState.tableData = [];
} finally {
tableState.loading = false;
}
};
return {
...toRefs(tableState),
...toRefs(pageParams),
pagination,
loadList
};
}
h (error) {
console.log(error);
tableState.tableData = [];
} finally {
tableState.loading = false;
}
};
return {
…toRefs(tableState),
…toRefs(pageParams),
pagination,
loadList
};
}
向外暴露请求数据,分页项。基本就实现了我们想要的逻辑了。