微信小程序Behavior-封装列表通用逻辑

502 阅读4分钟

一、动机

在大多数的小程序开发过程中基本都包含列表操作,而列表操作的逻辑无非就是增、删、改、查。如果小程序中包含多个列表页面,每个列表都写一遍类似逻辑是非常繁琐的。所以封装通用逻辑是非常有必要的。

本案例通过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)自动化处理

主要利用behaviordefinitionFilter能力。 定义了以下自动化处理:

  • 自动加载首页列表数据,不需要在页面中手动调用 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}
    })
  },
});

源代码