前言
在vue2中,我们可以通过引入composition-api的方式来享受主要升级,同时也能兼容生态。本文将记录笔者在尝试封装后台表格展示页面逻辑方面做的努力。
通用功能和属性分析
下图是一个常见的后台查询展示页面,我们可以将它分为三个部分:查询框、表格、分页控件三个部分。
对于表格组件,我们需要拿到表格的数据还需要一个loading用于控制加载状态,这里命名为tableData和tableLoading;
对于分页组件,我们需要页数、页码、总数,这里命名为pageNum、pageSize、total;
同时,我们需要一个update函数控制上面的四个参数,我们可以先写出下面这样的初始版本:
初始版本:高阶函数
import { ref, Ref } from '@vue/composition-api';
export function useTableSearch<T>(loadingRef?: Ref<boolean>) {
const tableLoading = loadingRef || ref(false);
const tableData = ref<T[]>();
const pageNum = ref(1);
const pageSize = ref(10);
const total = ref(0);
/**
*
* options:
* silence:是否改变loading
*/
const update = async <U>(
fn: (opt: U & { page: number; size: number }) => Promise<{ list: T[]; total: number }>,
payload: U,
options: { silence: boolean; resetPage?: boolean },
) => {
if (!options.silence) {
tableLoading.value = true;
}
try {
const res = await fn({ ...payload, page: pageNum.value, size: pageSize.value });
tableData.value = res?.list ?? [];
total.value = res?.total ?? 0;
} catch (error) {
console.debug(error);
} finally {
if (!options.silence) {
tableLoading.value = false;
}
}
};
return {
tableData,
update,
tableLoading,
pageNum,
pageSize,
total,
};
}
export default useTableSearch;
例子:
const { tableData, pageNum, pageSize, tableLoading, total, update } = useTableSearch<DisplayFormat>();
const searchOptions = inject<Ref<any>>('searchForm') || ref({});
const fetchList = async (silence = false) => {
if (serviceId) {
await update<{serviceId: number} & SearchForm>(
getList,
{ ...searchOptions.value});
}
}
onMounted(() => {
fetchList(false);
});
传参方式改进
上面的代码中,update的传参方式是这样:
await update(getList,{ ...searchOptions.value})
这样理解性太差了,而且类型判断还需要泛型来实现,但是页数和页码这两个参数是在工厂函数里面,有什么办法能更加优雅地传参呢?
我们可以使用回调函数,将pageNum和pageSize两个参数作为回调函数的参数使用:
const _update = async (fetchCallback: (page: number, size: number) => Promise<T>, silence: boolean) => {
if (!silence) {
tableLoading.value = true;
}
try {
const res = await fetchCallback(pageNum.value, pageSize.value);
tableData.value = res?.list ?? [];
total.value = res?.total ?? 0;
} catch (error) {
console.debug(error);
} finally {
if (!silence) {
tableLoading.value = false;
}
}
};
// 使用例子
const fetchList = async (silence = false) => {
await update((page, size) => getList({page, size, ...searchOptions.value}));
}
至此,一个简单的封装就完成了,我们设计了一个update的高阶函数,用于很方便地构造一个刷新列表的函数,如例子中的fetchList。
提高封装度
这样的设计有个优点:灵活性高且学习成本低。但是缺点也是明显的,作为使用者,我们希望这个工厂函数直接为我们提供表格相关功能,而不是我还需要再去封装一个函数使用。因此,我们可以再提供更高的封装度,通过配置的方式应对不同场景。
import { ref, Ref, onMounted, watch } from '@vue/composition-api';
import { useInterval } from './index';
interface IntervalOptions {
/** 是否打开轮询 */
isOpen?: boolean;
time?: number;
}
interface TableSearchParams<Result> {
/** 必传参数,调用更新的回调函数 */
fetchCallback: (page: number, size: number) => Promise<Result>;
/** 可以将状态更新ref传入,会自动进行更新,如果不传,则会新建一个ref并返回*/
loadingRef?: Ref<boolean>;
/** 轮询配置 */
intervalOptions?: IntervalOptions;
/** 是否在挂载时默认加载 */
isLoadOnMount?: boolean;
/** 是否在刷新列表时上报雷达平台 */
isReportFMP?: boolean;
}
/**
*
* @example
* const {
tableData,
pageNum,
pageSize,
total,
update,
} = useTableSearch<AnalysisDisplayFormat, {serviceId: number} & AnalysisSearchForm >({
fetchCallback: (page, size) => getList({ ...searchOptions.value, page, size }),
loadingRef: tableLoading,
isLoadOnMount: true,
intervalOptions: {
open: true,
time: 10000,
},
});
*/
export function useTableSearch<Data>({
fetchCallback,
loadingRef,
intervalOptions,
isLoadOnMount,
isReportFMP,
}: TableSearchParams<Data & { list: Data[]; total: number }>) {
const intervalOpen = intervalOptions?.isOpen ?? false;
const intervaltime = intervalOptions?.time ?? 10000;
isLoadOnMount = isLoadOnMount ?? true;
isReportFMP = isReportFMP ?? true;
const tableLoading = loadingRef || ref(false);
const tableData = ref<Data[]>();
const pageNum = ref(1);
const pageSize = ref(10);
const total = ref(0);
const resetPage = () => {
pageNum.value = 1;
};
const _update = async (silence: boolean, isResetPage: boolean) => {
if (!silence) {
tableLoading.value = true;
}
if (isResetPage) {
resetPage();
}
try {
const res = await fetchCallback(pageNum.value, pageSize.value);
tableData.value = res?.list ?? [];
total.value = res?.total ?? 0;
} catch (error) {
console.debug(error);
} finally {
if (!silence) {
tableLoading.value = false;
}
}
};
/**
*
* @param silence 为true时不更新loading
* @param resetPage 是否重置page(通常在查询时需要)
*/
const update = async (silence: boolean = false, isResetPage: boolean = false) => {
await _update(silence, isResetPage);
};
if (isLoadOnMount) {
onMounted(() => {
update(false);
});
}
// 轮询
let closeInterval: (() => void) | null = null;
if (intervalOpen) {
closeInterval = useInterval(() => {
update(true);
}, intervaltime);
}
// 翻页
watch([pageNum, pageSize], () => {
update(false);
});
return {
tableData,
update,
tableLoading,
pageNum,
pageSize,
total,
closeInterval,
};
}
export default useTableSearch;
拓展性设计
我们可以通过钩子函数的方式实现拓展性
class Hooks {
hooks: Array<() => void> = [];
use(hook: () => void): void {
this.hooks.push(hook);
}
forEach(fn: (hook: () => void) => void): void {
utils.forEach(this.hooks, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
}
}
export function useTableSearch<Data>({
fetchCallback,
loadingRef,
intervalOptions,
isLoadOnMount,
isReportFMP,
}: TableSearchParams<Data & { list: Data[]; total: number }>) {
......
/**
* 里面的钩子函数会在获取数据后依次执行
* @example
* updateHooks.use(report);
*/
const updateHooks = new Hooks();
......
/**
*
* @param silence 为true时不更新loading
* @param resetPage 是否重置page(通常在查询时需要)
*/
const update = async (silence: boolean = false, isResetPage: boolean = false) => {
await _update(silence, isResetPage);
updateHooks.forEach(hook => hook());
};
......
return {
......
updateHooks,
};
}