数据代理

在日常开发中,列表数据的分页查询是最为常见的场景,本章的目的是实现一个通用的请求数据代理,减少代码量,提高效率。 最终实现

本章目标

开发无须关心分页查询,条件查询等代码逻辑,只需要配置即可快速实现列表数据的分页查询功能。

例:

<template>
    <div>
        <div>
            <!-- 省略查询html代码 -->
        </div>
        <el-table
            :data="tableList.data"
        >
            <!-- 省略代码 -->
        </el-table>
        <el-pagination
            @size-change="onSizeChange"
            @current-change="onCurrentChange"
            :current-page="tableList.pagination.curr"
            :page-sizes="[5, 10, 20 ,30]"
            :page-size="tableList.pagination.limit"
            :total="tableList.pagination.total"
            layout="total, sizes, prev, pager, next, jumper"
        ></el-pagination>
    </div>
</template>
<script lang='ts'>
import { Component, Vue } from "vue-property-decorator";
import { mixinDataStore } from "@/mixin/view/Store";
import { Action } from "vuex-class";

@Component({
    name: "GridDemo",
    mixins: [mixinDataStore]
})
export default class GridDemo extends Vue {
    // 定义在vuex中的请求数据函数,只要返回的是Promise类型即可
    @Action("list") gridList: any;
    // 预留配置-列表配置
    // 列表代理对象
    tableList: any = {
        // 列表数据源
        data: [],
        // 代理配置
        proxy: {
            // 请求数据函数
            requestFun: this.gridList,
            // 分页每页显示条数字段名称,默认为limit,此参数传递到服务端
            limitParam: "pageSize",
            // 分页页码字段名称,默认为page,此参数传递到服务端
            pageParam: "current",
            // 初始化后自动加载数据
            autoLoad: true,
            // 读取数据相关配置
            reader: {
                // 数据根节点
                rootProperty: "data.data.records",
                successProperty: "data.code",
                totalProperty: "data.data.total",
                messageProperty: "data.data.msg"
            }
        }
    };
}
</script>

具体实现

扩展模块结构设计

挂载代理

如上述代码所示我们通过proxy.init(this.tableList);将相应的函数挂在到了tableList上面,那么如何实现呢。

例:

import { mixin } from "lodash";
// 通过Promise函数请求数据代理
export default {
    /**
       * 初始化
       *
       * @param {*} store,数据源对象
       */
    init(store: any) {
        console.log('proxy.promise.init');
        // 将当前代理对象的函数挂载到数据源对象,代理对象的函数会覆盖代理对象原有的函数
        mixin(store, this);
    }
    // 省略一系列函数
}

列表数据请求场景一般分为移动端与web端,而这两者对数据结果集的处理逻辑是不一样的,所以这里我们可以把移动端与web端分成两个类,通过代理配置来挂在对应的类。

例:

import { mixin } from "lodash";
import classic from "./classic";
import modern from "./modern";
// 通过Promise函数请求数据代理
export default {
    /**
       * 初始化
       *
       * @param {*} store,数据源对象
       */
    init(store: any) {
        console.log('proxy.promise.init');
        // 将当前代理对象的函数挂载到数据源对象,代理对象的函数会覆盖代理对象原有的函数
        mixin(store, this);
        // 根据代理类型挂载代理对象
        // 默认挂载经典代理
        switch (store.proxy.type) {
            case 'modern':
                mixin(store, modern);
                break;
            default:
                mixin(store, classic);
                break;
        }
    }
    // 省略一系列函数
}

classic类

export default {
   // 省略一系列函数
}

modern类

// todo
// 用于移动端请求数据,移动端数据一般为追加模式
export default {
   // 暂未实现
}

预留扩展

以上代码能够覆盖大部分场景,并不能覆盖全部场景,所以我们需要预留出可扩展部分,将挂载代理中的类放置于promise文件夹中,我们再实现一个代理父类

import promiseProxy from "./promise/index";
import { mixin, split, drop } from "lodash";
// 这是一个数据代理
// 俄罗斯套娃模式,支持向上向下扩展
// 数据源对象
// store={};
// 数据源对象挂载代理
// proxy.init(store);
// 数据源对象加载数据
// store.load(); => {data:[]}
export default {
    /**
        * 初始化
         *
         * @param {*} store,数据源对象
         */
    init(store: any) {
        console.log('proxy.init');
        const me = this as any,
            // 代理配置
            proxy = store.proxy,
            // 读取代理类型,用.分割
            key = split(proxy.type, '.');
        // 将当前代理对象的函数挂载到数据源对象,代理对象的函数会覆盖代理对象原有的函数
        mixin(store, me);
        // 设置下一级代理类型
        store.proxy.type = drop(key).toString();
        // 根据代理类型第一级挂载代理对象
        switch (key[0]) {
            // 预留扩展,可以实现其他代理类
            default:
                // 初始化代理对象
                promiseProxy.init(store);
                break;
        }
    },
    // 省略一系列函数
}

整体结构

在代码实现过程中,我们会封装一些公用函数,那么创建一个帮助类是一个很好的选择

例:

import {  isEmpty, isNumber, pickBy } from "lodash";
export default {
    /**
     * 判断是否为空对象,空字符串,null
     * {},[],'',null会返回true
     * @param {*} v
     * @returns
     */
    isEmpty(v: any) {
        if (isNumber(v)) {
            // 只要是数字就不算空
            return false;
        }
        return isEmpty(v);
    },
    // 清除对象中空数据
    clearObject(o: any) {
        const me = this as any;
        return pickBy(o, (item: any) => {
            return !me.isEmpty(item);
        });
    }
}

如此,整个数据代理模块目录结构如下

填充代码

入口模块

代理可用配置

  1. type

    为了方便扩展,我们需要一个type配置,用来确定代理类型,默认为经典代理即promise.classic

  2. 分页配置

    为了处理分页等逻辑需要以下配置 1. requestFun -> 请求数据函数 1. pageSize -> 每次加载几条数据,默认为10 1. page -> 当前页码,默认为1 1. limitParam -> 分页每页显示条数字段名称,默认为limit,此参数传递到请求数据函数 1. pageParam -> 分页页码字段名称,默认为page,此参数传递到请求数据函数 1. paginationParam -> 数据源对象接收分页配置节点名称,默认为pagination

  3. defaultParams

    默认参数,默认参数会被相同名称新参数覆盖

  4. autoLoad

    初始化成功后是否自动调用load函数

  5. reader

    读取数据相关配置 1. reader.rootProperty -> 数据根节点名称 1. reader.successProperty -> 判断请求是否成功的节点名称 1. reader.totalProperty -> 数据总数节点名称 1. reader.messageProperty -> 请求失败后失败消息节点名称

  6. 扩展配置

    预留出一些扩展函数,用来处理一些额外的需求 1. disposeItem -> 处理单个数据对象的函数

数据源可用配置

  1. 扩展配置

    预留出一些扩展函数,用来处理一些额外的需求 1. failure -> 请求失败后执行函数 1. writerTransform -> 请求数据前处理请求参数函数 1. readerTransform -> 请求数据成功后处理数据结果函数

    函数

    入口模块我们可以放一些通用逻辑函数,我们可以在里面实现failure、writerTransform扩展配置。

最终代码如下:

import promiseProxy from "./promise/index";
import util from './utils/index'
import { cloneDeep, isObjectLike, mixin, defaultsDeep, split, isFunction, drop, defaults } from "lodash";
// 数据源对象可用配置
// const defaultStore = {
//     // 扩展,请求失败后执行函数
//     failure: null,
//     // 扩展,请求数据前处理请求参数函数
//     writerTransform: null,
//     // 扩展,请求数据成功后处理数据结果函数
//     readerTransform: null
// }
// 默认配置参数
const defaultProxy = {
    // 代理类型,默认为经典代理
    type: 'promise.classic',
    // 每次加载几条数据,默认为10
    pageSize: 10,
    // 当前页码,默认为1
    page: 1,
    // 分页每页显示条数字段名称,默认为limit,此参数传递到请求数据函数
    limitParam: 'limit',
    // 分页页码字段名称,默认为page,此参数传递到请求数据函数
    pageParam: 'page',
    // 数据源对象接收分页配置节点名称,默认为page
    paginationParam: 'pagination',
    // 默认参数,默认参数会被相同名称新参数覆盖,此参数传递到请求数据函数
    defaultParams: null,
    // 初始化后是否自动加载数据
    autoLoad: false,
    // 扩展 处理单个数据对象的函数
    disposeItem: null,
    // 读取数据相关配置
    reader: {
        // 数据根节点名称
        rootProperty: "data",
        // 判断请求是否成功的节点名称
        successProperty: "success",
        // 数据总数节点名称
        totalProperty: "total",
        // 请求失败后失败消息节点名称
        messageProperty: 'message'
    }
};
// 这是一个数据代理
// 俄罗斯套娃模式,支持向上向下扩展
// 数据源对象
// store={};
// 数据源对象挂载代理
// proxy.init(store);
// 数据源对象加载数据
// store.load(); => {data:[]}
export default {
    /**
         * 初始化,每个数据源对象必须初始化
         *
         * @param {*} store,数据源对象
         */
    init(store: any) {
        console.log('proxy.init');
        const me = this as any,
            // 代理配置
            proxy = store.proxy,
            // 读取代理类型,用.分割
            key = split(proxy.type, '.');
        // 读取并设置默认配置,默认配置会被新配置覆盖
        store.proxy = defaultsDeep(proxy, defaultProxy);
        // 将当前代理对象的函数挂载到数据源对象,代理对象的函数会覆盖代理对象原有的函数
        mixin(store, me);
        // 设置下一级代理类型
        store.proxy.type = drop(key).toString();
        // 根据代理类型第一级挂载代理对象
        switch (key[0]) {
            // 预留扩展,可以实现其他代理类
            default:
                // 初始化代理对象
                promiseProxy.init(store);
                break;
        }
        // 根据配置决定是否自动加载数据
        if (proxy.autoLoad) {
            store.load();
        }
    },
    /**
     * 数据加载结束执行
     *
     * @param {*} proxy 数据源对象代理
     * @param {*} { res 结果数据集, isError = false 是否加载失败}
     */
    loadEnd(proxy: any, { res, isError = false }) {
        // 标识请求数据完成
        proxy.isLoading = false;
        // 如果数据加载失败
        if (isError) {
            const me = this as any;
            // 如果有请求失败执行函数,执行它
            if (isFunction(me.failure)) {
                // 有时候请求失败需要额外的处理逻辑
                me.failure(res);
            }
        }
    },
    /**
     * 数据源对象加载数据,页码重置为1
     *
     * @param {*} [params 参数]
     */
    load(params?: any) {
        const me = this as any,
            proxy = me.proxy,
            // 获取默认参数
            { defaultParams } = proxy;
        if (params) {
            // 深度拷贝并处理掉空数据,避免数据变化引起bug
            params = util.clearObject(cloneDeep(params));
        }
        // 如果存在默认参数,则添加默认参数
        if (isObjectLike(defaultParams)) {
            // 默认参数会被新参数覆盖
            params = defaults(params, defaultParams);
        }
        // 存储参数(排除分页参数)
        proxy.extraParams = cloneDeep(params);
        proxy.params = params
        proxy.page = 1;
        me.loadByProxy();
    },
    /**
     * 数据源对象重载数据,页码重置为1
     *
     */
    reLoad() {
        const me = this as any;
        me.load(me.proxy.extraParams);
    },
    /**
     * 获取当前参数(排除分页参数)
     *
     * @returns
     */
    getParams() {
        return (this as any).proxy.extraParams;
    }
}

promise代理入口模块

函数

不管是移动端还是web端,读取数据的逻辑函数应该是通用的,我们可以在里面实现disposeItem、readerTransform扩展配置。

最终代码如下:

import { mixin, get, forEach, isFunction } from "lodash";
import classic from "./classic";
import modern from "./modern";
// 通过Promise函数请求数据代理
export default {
    /**
       * 初始化
       *
       * @param {*} store,数据源对象
       */
    init(store: any) {
        console.log('proxy.promise.init');
        // 将当前代理对象的函数挂载到数据源对象,代理对象的函数会覆盖代理对象原有的函数
        mixin(store, this);
        // 根据代理类型挂载代理对象
        // 默认挂载经典代理
        switch (store.proxy.type) {
            case 'modern':
                mixin(store, modern);
                break;
            default:
                mixin(store, classic);
                break;
        }
    },
    /**
     *
     *
     * @param {*} {
     *         requestFun 获取数据的函数,必须返回Promise函数对象
     *         params 获取数据的函数所需的参数
     *         disposeItem 扩展 处理单个数据对象的函数
     *         reader 读取数据相关配置
     *     }
     * @returns 成功回调 resolve({ data, total }); data数据结果集
     *          失败回调 reject({
                    message: '您的网络不佳,请检查您的网络'
                }) message 提示
     */
    readData({
        requestFun, params, disposeItem, reader
    }) {
        return new Promise((resolve, reject) => {
            // 通过代理函数获取数据
            requestFun(params).then((res: any) => {
                const me = this as any,
                    // 读取数据相关配置
                    {
                        // 数据根节点名称
                        rootProperty,
                        // 用于判断请求是否成功的节点名称
                        successProperty,
                        // 数据总数节点名称
                        totalProperty,
                        // 请求失败后失败消息节点名称
                        messageProperty } = reader;
                // 如果有请求数据成功后处理数据结果函数,执行它
                if (isFunction(me.readerTransform)) {
                    // 有时候后端返回的数据可能并不符合规范,可以用这个扩展函数处理一下
                    res = me.readerTransform(res);
                }
                // 获取请求数据结果状态
                const success = get(res, successProperty);
                if (success) {
                    // 获取数据
                    const data = get(res, rootProperty),
                        // 获取数据总数
                        total = get(res, totalProperty);
                    // 如果有遍历单条数据的函数,那么遍历处理数据
                    if (disposeItem) {
                        forEach(data, disposeItem);
                    }
                    // 成功回调
                    resolve({ data, total });
                } else {
                    // 失败回调
                    reject({
                        message: get(res, messageProperty)
                    });
                }
            }).catch(() => {
                // 失败回调
                reject({
                    message: '您的网络不佳,请检查您的网络'
                })
            });
        });
    }
}

请求数据的函数与返回的数据需要遵循以下规则

  1. 此帮助类只是一个代理类,具体分页查询函数还是需要axios等扩展来实现,但是因为设计时考虑了扩展性,可以自定义一些扩展来实现请求数据的功能

  2. 返回数据必须是标准json格式数据,并且有以下字段,对应字段名称可以在reader配置中灵活配置,如果返回数据不标准可以用readerTransform函数处理成表格格式

    1. success -> 用于判断请求是否成功

    2. data -> 最终数据结果集

    3. total -> 满足当前条件的数据总数,用于分页

    4. message -> 用于请求失败消息提示

假如后端返回数据格式如下,使用axios请求数据并不做任何处理

{
    "code": 1,
    "msg": "查询成功",
    "data": {
        "records": [{
            "id": 119,
            "name": "的鹅鹅鹅饿鹅",
            "telephone": "18888888888"
        }, {
            "id": 118,
            "name": "未命名",
            "telephone": "18899999999"
        }],
        "total": 62
    }
}

代理中reader配置如下即可

    reader: {
        // 数据根节点
        rootProperty: "data.data.records",
        successProperty: "data.code",
        totalProperty: "data.data.total",
        messageProperty: 'data.data.msg'
    }

promise.classic代理

应该有一个基础函数,然后再此基础函数的基础上封装出相应的扩展函数 最终代码如下

import { set, get, defaults , isFunction} from "lodash";
// 用于web请求数据,web端数据一般为重置模式
export default {
    /**
     * 根据代理配置加载数据
     *
     */
    loadByProxy() {
        const me = this as any,
            proxy = me.proxy;
        // 当前代理状态
        console.log('proxy isLoading', proxy.isLoading);
        // 如果正在请求数据,不做任何操作,过滤高频请求
        if (!proxy.isLoading) {
            // 标识正在请求数据
            proxy.isLoading = true;
            // 读取store配置
            const {
                pageSize,
                page,
                paginationParam
            } = proxy
            // 读取参数
            let params = proxy.params || {};
            // 设置分页相关参数
            set(params, proxy.limitParam, pageSize);
            set(params, proxy.pageParam, page);
            // 如果有请求数据前处理请求参数函数,执行它
            if (isFunction(me.writerTransform)) {
                // 有时候需要在请求前处理参数
                params = me.writerTransform(params, proxy);
            }
            // 设置代理参数
            proxy.params = params;
            console.log(proxy.extraParams)
            // console.log(proxy, params)
            // 读取数据
            me.readData(proxy).then((res: any) => {
                me.data = res.data;
                // 获取并更新分页配置,用于分页组件处理数据
                const pagination = defaults({
                    total: res.total,
                    limit: pageSize,
                    curr: page
                }, get(me, paginationParam));
                // 更新分页配置
                set(me, proxy.paginationParam, pagination);
                // 获取数据成功
                me.loadEnd(proxy, {
                    res
                })
            }).catch((res: any) => {
                // 获取数据失败
                me.loadEnd(proxy, {
                    isError: true,
                    res
                })
            })
        }
    },
    /**
     * 数据源对象改变每页显示条数,页码重置为1
     *
     * @param {number} page
     */
    loadPageSize(pageSize: number) {
        const me = this as any;
        me.proxy.pageSize = pageSize;
        me.proxy.page = 1;
        me.loadByProxy();
    },
    /**
     * 数据源对象改变页码
     *
     * @param {number} page
     */
    loadPage(page: number) {
        const me = this as any;
        me.proxy.page = page;
        me.loadByProxy();
    },
    /**
     * 刷新数据源对象,用于编辑/新增/删除后调用
     * 编辑后直接重载数据,页码不变
     * 新增后直接重新加载数据,页码重置为1
     * 删除后根据剩余数据总数和页面等灵活设置页码,不变或减1
     *
     * @param {*} [{ isDel = false 是否删除数据, isAdd = false 是否新增数据}={}]
     */
    refresh({ isDel = false, isAdd = false } = {}) {
        const me = this as any,
            proxy = me.proxy;
        // 获取当前页码
        let page = proxy.page;
        if (isDel) {
            // 如果是删除并且页码大于1
            if (page > 1) {
                // 获取删除后当前数据总数
                const count = me.pagination.total - 1,
                    // 获取当前每页数据数
                    pageSize = proxy.pageSize;
                // 如果删除后当前页面无数据,页码减1
                if ((page - 1) * pageSize >= count) {
                    page--;
                }
            }
        } else if (isAdd) {
            // 新增后直接到第一页
            page = 1;
        }
        me.loadPage(me, page);
    }
}

promise.modern代理

待实现,预留的移动端代理

使用时二次扩展

将以上模块发布为npm包,可以通过npm install ux-data-proxy命令直接引入。

一般来说后端返回的数据格式是固定的,请求参数也是固定规则,在实际使用中,我们可以自定义一个帮助类来使用,具体代码如下:

import proxy from "ux-data-proxy";
import { defaultsDeep, mixin } from "lodash";
import { Message } from 'element-ui';
// 默认配置1
const currentProxy = {
    limitParam: 'pageSize',
    pageParam: "current",
    // 显示错误消息
    isErrorMessage: true,
    // 初始化后自动加载数据
    autoLoad: true,
    // 读取数据相关配置
    reader: {
        // 数据根节点
        rootProperty: "data.data.records",
        successProperty: "data.code",
        totalProperty: "data.data.total",
        messageProperty: 'data.data.msg'
    }
};
// 默认配置2
const defaultProxy = {
    limitParam: 'pageSize',
    pageParam: 'currentPage',
    autoLoad: true,
    // 读取数据相关配置
    reader: {
        // 数据根节点
        rootProperty: "data.records",
        successProperty: "code",
        totalProperty: "data.total",
        messageProperty: 'data.msg'
    }
};
// 扩展数据请求代理
export default {
    /**
    * 初始化
     *
     * @param {*} store,数据源对象
     */
    init(store: any) {
        // 根据配置类型读取不同的默认配置
        switch (store.proxy.configType) {
            case 'current':
                store.proxy = defaultsDeep(store.proxy, currentProxy);
                break;
            default:
                store.proxy = defaultsDeep(store.proxy, defaultProxy);
                break;
        }
        console.log('newStore.init');
        // 它本身的方法会被代理对象的方法覆盖,放在后面则相反
        mixin(store, this);
        // 将当前代理对象的方法挂载到数据源对象,代理对象的方法会覆盖代理对象原有的方法
        proxy.init(store);
        // 如果放在 proxy.init(store);之后执行
        // 如果设置了初始化自动加载,首次请求writerTransform不会触发
    },
    // 扩展,请求失败后执行函数
    failure(res: any) {
        const me = this as any;
        if (me.proxy.isErrorMessage) {
            // 显示错误提示
            Message({
                // duration:0,
                message: res.message,
                type: "error",
                customClass: "zZindex"
            });
        }
    },
    // 扩展,请求数据成功后处理数据结果函数
    readerTransform(res: any) {
        console.log('readerTransform')
        return res;
    },
    // 扩展,请求数据前处理请求参数函数
    writerTransform(params: any) {
        console.log('writerTransform')
        return params;
    }
}

在实际使用场景引入这个类即可,使用方式不用改变

mixin类

大多数情况下,分页查询逻辑都是固定的,所以我们可以封装一个mixin类,在估计视图中直接引入这个mixin即可 代码如下:

import { Component, Vue } from 'vue-property-decorator';
import proxy from "@/utils/newStore";
@Component({
})
export class mixinDataStore extends Vue {
    // 预留配置-列表配置
    tableList: any;
    created() {
        proxy.init(this.tableList);
    }

    // 每页显示数量变化
    onSizeChange(pageSize: number) {
        this.proxySizeChange(pageSize);
    }

    // 页码发生变化
    onCurrentChange(page: number) {
        this.proxyCurrentChange(page);
    }

    //根据条件查询
    proxyQuery(params: any, tabName = "tableList") {
        // console.log("onSizeChange", pageSize);
        this[tabName].load(params);
    }

    //每页显示数量变化
    proxySizeChange(pageSize: number, tabName = "tableList") {
        // console.log("onSizeChange", pageSize);
        this[tabName].loadPageSize(pageSize);
    }

    // 页码发生变化
    proxyCurrentChange(page: number, tabName = "tableList") {
        // console.log("onCurrentChange", page);
        this[tabName].loadPage(page);
    }

}

结语

  1. 移动端代理并未实现,你想好怎么实现了吗?

  2. 假如我们需要对本地数据进行分页查询,并且也分区分web端与移动端,你应该怎么去实现这个扩展

  3. 你是否尝试封装一个查询组件,这个组件支持校验,条件级联等常用功能呢,你要如何去实现才能保证它具有通用性与可扩展性呢

  4. 你是否尝试封装一个列表组件?也许用现成的第三方组件是一个更好的选择哦。

  5. 你是否尝试对css进行封装,定制一个通用场景呢?

最后更新于