> 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](https://760650585-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MEuTIqYChilFP75xEgL%2Fsync%2Fe404950c887ce22e89495f63a21fcd0405b6ce67.png?generation=1597635205646065\&alt=media)

### 填充代码

#### 入口模块

**代理可用配置**

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