> For the complete documentation index, see [llms.txt](https://534502520.gitbook.io/vue/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://534502520.gitbook.io/vue/di-yi-juan/shu-ju-dai-li.md).

# 数据代理

在日常开发中，列表数据的分页查询是最为常见的场景，本章的目的是实现一个通用的请求数据代理，减少代码量，提高效率。 [最终实现](https://github.com/jy02534655/data-proxy)

## 本章目标

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

例:

```javascript
<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上面，那么如何实现呢。

例:

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

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

例:

```javascript
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类

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

modern类

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

#### 预留扩展

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

例

```javascript
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;
        }
    },
    // 省略一系列函数
}
```

#### 整体结构

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

例:

```javascript
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);
        });
    }
}
```

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

![avatar](/files/-MEuTdLCSrlaklsb-gXP)

### 填充代码

#### 入口模块

**代理可用配置**

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扩展配置。

最终代码如下:

```javascript
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扩展配置。

最终代码如下:

```javascript
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请求数据并不做任何处理

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

代理中reader配置如下即可

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

#### promise.classic代理

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

```javascript
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命令直接引入。

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

```javascript
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即可 代码如下:

```javascript
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进行封装，定制一个通用场景呢？


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://534502520.gitbook.io/vue/di-yi-juan/shu-ju-dai-li.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
