composition-api——对后台查询界面的一些封装探索

778 阅读4分钟

前言

在vue2中,我们可以通过引入composition-api的方式来享受主要升级,同时也能兼容生态。本文将记录笔者在尝试封装后台表格展示页面逻辑方面做的努力。

通用功能和属性分析

下图是一个常见的后台查询展示页面,我们可以将它分为三个部分:查询框、表格、分页控件三个部分。

image.png 对于表格组件,我们需要拿到表格的数据还需要一个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,
    };
}