数据代理
在日常开发中,列表数据的分页查询是最为常见的场景,本章的目的是实现一个通用的请求数据代理,减少代码量,提高效率。 最终实现
本章目标
开发无须关心分页查询,条件查询等代码逻辑,只需要配置即可快速实现列表数据的分页查询功能。
例:
<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);
});
}
}
如此,整个数据代理模块目录结构如下
填充代码
入口模块
代理可用配置
type
为了方便扩展,我们需要一个type配置,用来确定代理类型,默认为经典代理即promise.classic
分页配置
为了处理分页等逻辑需要以下配置 1. requestFun -> 请求数据函数 1. pageSize -> 每次加载几条数据,默认为10 1. page -> 当前页码,默认为1 1. limitParam -> 分页每页显示条数字段名称,默认为limit,此参数传递到请求数据函数 1. pageParam -> 分页页码字段名称,默认为page,此参数传递到请求数据函数 1. paginationParam -> 数据源对象接收分页配置节点名称,默认为pagination
defaultParams
默认参数,默认参数会被相同名称新参数覆盖
autoLoad
初始化成功后是否自动调用load函数
reader
读取数据相关配置 1. reader.rootProperty -> 数据根节点名称 1. reader.successProperty -> 判断请求是否成功的节点名称 1. reader.totalProperty -> 数据总数节点名称 1. reader.messageProperty -> 请求失败后失败消息节点名称
扩展配置
预留出一些扩展函数,用来处理一些额外的需求 1. disposeItem -> 处理单个数据对象的函数
数据源可用配置
扩展配置
预留出一些扩展函数,用来处理一些额外的需求 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: '您的网络不佳,请检查您的网络'
})
});
});
}
}
请求数据的函数与返回的数据需要遵循以下规则
此帮助类只是一个代理类,具体分页查询函数还是需要axios等扩展来实现,但是因为设计时考虑了扩展性,可以自定义一些扩展来实现请求数据的功能
返回数据必须是标准json格式数据,并且有以下字段,对应字段名称可以在reader配置中灵活配置,如果返回数据不标准可以用readerTransform函数处理成表格格式
success -> 用于判断请求是否成功
data -> 最终数据结果集
total -> 满足当前条件的数据总数,用于分页
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);
}
}
结语
移动端代理并未实现,你想好怎么实现了吗?
假如我们需要对本地数据进行分页查询,并且也分区分web端与移动端,你应该怎么去实现这个扩展
你是否尝试封装一个查询组件,这个组件支持校验,条件级联等常用功能呢,你要如何去实现才能保证它具有通用性与可扩展性呢
你是否尝试封装一个列表组件?也许用现成的第三方组件是一个更好的选择哦。
你是否尝试对css进行封装,定制一个通用场景呢?
最后更新于
这有帮助吗?