一、动机
在大多数的小程序开发过程中基本都包含列表操作,而列表操作的逻辑无非就是增、删、改、查。如果小程序中包含多个列表页面,每个列表都写一遍类似逻辑是非常繁琐的。所以封装通用逻辑是非常有必要的。
本案例通过behavior编写通用逻辑并注入到页面中。
不了解behavior?,点击了解
二、封装逻辑
1、定义生成behavior的工厂函数
type ListApiStatus = "ok" | "fail";
interface IListResult {
listData?: any[];
total?: number;
isLast?: boolean;
status: ListApiStatus;
}
interface IListOperationResult {
status: ListApiStatus;
data?: any;
}
interface IGetListParams {
pageSize: number;
pageNum: number;
type: GetListType;
[key: string]: any;
}
/**
* @description 控制获取列表接口是初始化数据,还是翻页操作。
*/
type GetListType = "initial" | "loadMore";
interface IBehaviorWithList {
/**
* @description 注入data的名称
*/
namespace: string;
/**
* @description 默认每页数量
*/
defaultPageSize?: number;
/**
* @description 每项数据的唯一标识,默认【id】
*/
key?: string;
/**
* @description 是否滚动到底部自动下一页,不用手动调用nextPageBehavior
*/
isAutoNextPage?: boolean;
/**
* @description 是否自动在onLoad中初始化第一页数据,不用手动调用。
*/
isAutoLoad?: boolean;
/**
* @description 获取列表数据的接口,调用成功时必须返回{isLast:boolean,listData:array,total:number}
*/
getListApi: (params: IGetListParams) => Promise<IListResult>;
/**
* @description 新增数据的接口
*/
addItemApi?: (data: any) => Promise<IListOperationResult>;
/**
* @param data:更新的数据
* @description 参数data必须包含唯一标识取参数key的值,默认为【id】
*/
updateItemApi?: (data: any) => Promise<IListOperationResult>;
/**
* @description 删除数据的接口
*/
deleteItemApi?: (id: any) => Promise<IListOperationResult>;
}
const BehaviorWithList = (params: IBehaviorWithList) => {
const {
key='id',
namespace,
isAutoNextPage,
isAutoLoad,
defaultPageSize=10,
getListApi,
deleteItemApi,
updateItemApi,
addItemApi,
} = params;
return Behavior({
具体逻辑...
})
}
2、List Behavior逻辑
1)定义data
Behavior({
data:{
//工厂函数传入的namespace,避免字段污染
[namespace]: {
pageSize: pageSize ?? 10,
pageNum: 1,
listData: [],
total: 0,
isLast: false,
isFetch: false,
},
},
...
})
2)定义列表逻辑方法
Behavior({
...
methods:{
/**
* @description 获取下一页数据
* @param extraData Record<string, any>【将会被注入到获取列表数据的接口参数中】
*/
nextPageBehavior(extraData?: Record<string, any>) {
const { pageNum, pageSize, isFetch, isLast } = this.data[namespace];
if (!isLast && !isFetch) {
this.getListBehavior({
...extraData,
pageNum: pageNum + 1,
pageSize,
type: "loadMore",
});
}
},
onSearchBehavior(searchData: Record<string, any> = {}) {
const { pageSize } = this.data[namespace];
this.getListBehavior({
pageNum: 1,
pageSize,
type: "initial",
...searchData,
});
this.setData({
_searchData: searchData,
});
},
/**
* @description 获取列表数据
* @param params pageSize?: number; pageNum: number; type?: 'initial' | 'loadMore', ...otherParams
*/
async getListBehavior(params?: IGetListParams) {
const { listData, isFetch } = this.data[namespace];
const { pageNum, pageSize, type, ...extraData } = params ?? {
type: "initial",
pageSize: defaultPageSize,
pageNum: 1,
};
if (!isFetch) {
this.setData({
[namespace]: { ...this.data[namespace], isFetch: true },
});
try {
const { status, ...data } = await getListApi({
pageNum,
pageSize,
...extraData,
...this.data._searchData,
});
if (status === "ok") {
if (
typeof data.isLast !== "boolean" ||
!(data.listData instanceof Array) ||
typeof data.total !== "number"
) {
throw new Error(
"请在【getListApi】返回成功的情况下返回{isLast:boolean,listData:array,total:number}数据"
);
}
const updateData = {
...this.data[namespace],
...(data ?? {}),
isFetch: false,
pageNum,
pageSize,
};
if (type === "initial") {
this.setData({
[namespace]: updateData,
});
} else {
this.setData({
[namespace]: {
...updateData,
listData: listData.concat(data.listData),
},
});
}
} else {
throw new Error(
"接口调用失败【getListApi】- 没传status值或者status值为fail"
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
}
},
/**
* @description 更新数据的方法
* @param data 更新的数据
*/
async updateItemBehavior(data: any) {
const { listData } = this.data[namespace];
try {
const res = await updateItemApi?.(data);
if (res?.status === "ok") {
const updateIdx = listData.findIndex(
(item: any) => item[key] === data[key]
);
if (updateIdx !== -1) {
listData[updateIdx] = { ...listData[updateIdx], data };
this.setData({
[namespace]: {
...this.data[namespace],
listData: [...listData],
},
});
}
return res?.data;
} else {
throw new Error(
"接口调用失败【updateItemBehavior】- 没传status值或者status值为fail"
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
},
/**
* @description 新增数据的方法
* @param data 新增的数据
*/
async addItemBehavior(data: any) {
try {
const res = await addItemApi?.(data);
if (res?.status === "ok") {
this.getListBehavior();
return res?.data;
} else {
throw new Error(
"接口调用失败【addItemBehavior】- 没传status值或者status值为failr"
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
},
/**
* @description 删除数据的方法
* @param id 删除数据的id
*/
async deleteItemBehavior(id: any) {
const { listData, pageSize, pageNum, isLast } = this.data[namespace];
try {
const res = await deleteItemApi?.(id);
if (res?.status === "ok") {
const deleteIdx = listData.findIndex(
(item: any) => item[key] === id
);
if (deleteIdx !== -1) {
//删除本地数据
listData.splice(deleteIdx, 1);
if (!isLast) {
//如果不是最后一页,将会请求当前页的最后一条数据补齐列表,保证页码准确
const {
total,
status,
isLast,
listData: newListData,
} = await getListApi({
pageSize: 1,
pageNum: pageNum * pageSize,
});
if (status === "ok" && newListData?.[0]) {
listData.push(newListData[0]);
this.setData({
[namespace]: {
...this.data[namespace],
listData: [...listData],
total,
isLast,
},
});
} else {
throw new Error(
"接口调用失败【getListApi】- 没传status值或者status值为fail"
);
}
} else {
this.setData({
[namespace]: {
...this.data[namespace],
listData: [...listData],
},
});
}
}
return res?.data;
} else {
throw new Error(
"接口调用失败【deleteItemBehavior】- 没传status值或者status值为fail"
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
},
},
}
})
3)自动化处理
主要利用behavior的definitionFilter能力。
定义了以下自动化处理:
- 自动加载首页列表数据,不需要在页面中手动调用
getListBehavior(自动在页面onLoad时调用)。 - 滚动到底部自动加载下一页,不需要在页面中手动调用
nextPageBehavior。 - 添加
listExtraData选项,选项中定义页面data的key,定义的data会注入到getListApi接口的参数中(例如页面的data中声明有test字段,声明listExtraData:["test"],test就会被注入到方法中),并且每次调用时会保证数据和页面一致。
Behavior({
definitionFilter(defFields: any) {
const onReachBottom = defFields?.methods?.onReachBottom;
const onLoad = defFields?.methods?.onLoad;
const getListExtraData = (pageData: Record<string, any>) => {
const listExtraData = defFields?.listExtraData as string[];
if (listExtraData && listExtraData.length > 0) {
return Object.entries(pageData).reduce(
(total: Record<string, any>, [key, val]) => {
if (listExtraData.includes(key)) {
total[key] = val;
}
return total;
},
{}
);
}
return {};
};
if (isAutoNextPage && defFields.methods) {
defFields.methods.onReachBottom = function () {
this.nextPageBehavior({ ...getListExtraData(this.data) });
onReachBottom && onReachBottom.call(this);
};
}
if (isAutoLoad && defFields.methods) {
//onLoad中如果想要注入listExtraData中定义的data,并保持和页面一致,页面中的onLoad必须为async方法,内部保持同步的方式写代码
defFields.methods.onLoad = async function () {
onLoad && (await onLoad.call(this));
wx.nextTick(() => {
this.getListBehavior({
pageNum: 1,
pageSize: defaultPageSize,
type: "initail",
...getListExtraData(this.data),
});
});
};
}
},
})
三、使用
const listBehavior = BehaviorWithList({
namespace: "list",
isAutoNextPage: true,
isAutoLoad: true,
getListApi: (data) => {
console.log(data);
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: "ok",
listData: new Array(10).fill(1),
total: 8,
isLast: false,
});
}, 1000);
});
},
});
Page<IListPageData, IListPageOption>({
behaviors: [listBehavior],
data: {
testListExtraData:{a:1}
},
async onLoad() {
const res= await getSingleImg();
this.setData({test:res?.data})
},
listExtraData:['testListExtraData','test'],
onReady() {},
onShow() {
console.log(this.data.list)
},
onReachBottom() {},
changeTestListExtraData(){
this.setData({
testListExtraData:{b:2}
})
},
});