微信小程序使用alova封装分页请求策略

0. 需求

上一篇文章,我们已经适配好了alovajs。现在需要一个分页策略。alova自带的请求策略并不支持原生微信小程序,所以需要结合源码自行开发。

1.官方例子

例子

ts 复制代码
// vue3中
const {  
    // 加载状态  
    loading,  
    // 列表数据  
    data,  
    // 是否为最后一页  
    // 下拉加载时可通过此参数判断是否还需要加载  
    isLastPage,  
    // 当前页码,改变此页码将自动触发请求  
    page,  
    // 每页数据条数  
    pageSize,  
    // 分页页数  
    pageCount,  
    // 总数据量  
    total  
} = usePagination(  
    // Method实例获取函数,它将接收page和pageSize,并返回一个Method实例  
    (page, pageSize) => queryStudents(page, pageSize),  
    {
        initialPage: 1, // 初始页码,默认为1  
        initialPageSize: 10 // 初始每页数据条数,默认为10  
        ....
    }
);

// 翻到上一页,page值更改后将自动发送请求  
const handlePrevPage = () => {  
    page.value--;  
};

简单的来说usePagination返回一系列状态,这些状态对于ui库来说是可识别的状态,当你修改了page值,也会触发重新请求。

2. 实现自己的版本

官方源码

官方的代码很多,主要是因为功能比较全,涉及到预加载上下页、翻页和追加两种模式。我的主要是实现了基础的分页请求,预加载我直接舍弃了。所以下面的代码只是思路,如果你需要预加载可以在此基础上改造。

注意:代码中同样使用了mobx作为状态管理,怎么接入可以参考上一篇文章

ts 复制代码
// 自己的usePagination
export default function (
  // 请求对象
  handle,
  // 配置
  config,
) {
  const {
    // 如何获取总量值
    // 自行根据项目实际情况
    total: totalGetter = (res: any) => res.data.total,
    // 如何获取响应数据
    data: dataGetter = (res: any) => res.data.records,
    // 是否是追加模式
    // 移动端肯定优先追加
    append = true,
    // 初始页码
    initialPage = 1,
    // 每页的size
    initialPageSize = 10,
    // 外部状态,通常是筛选条件
    watchingStates = [],
    // 初始数据
    initialData,
    // 是否立即请求
    immediate = true,
    // 请求中间件
    middleware = () => {},
    // 由于没有预请求功能,这里强制请求直接false
    force = () => false,
    ...rest
  } = config || {};
   ///////////////创建状态///////////////
   ....
   ....
  //////////////////监听变化/////////////////
  ....
  ....
  //////////////////请求处理/////////////////
  ...
  ...
  //////////////////导出操作函数/////////////////
  ...
  ...
  return {
    ...
  }

这里为了方便讲解,我删掉了ts类型。,如果你想要有类型提示,那你得做好抠脑壳的准备。alova为了能自动推断类型,有大量的泛型参数,怎么正确的传参需要研究一下。

2.1. 创建状态

ts 复制代码
///////////////创建状态///////////////
  // 用于控制是否重置
  const isReset = useRef(false);
  // 重置期间请求的次数,为了防止重置时重复请求,使用此参数限制请求
  const requestCountInReseting = useRef(0);
  const page = useState(initialPage);

  const pageSize = useState(initialPageSize);
  // 请求错误
  const isError = useState<boolean>(false);
  // 当前页,或者下拉加载下的所有数据
  const data = useState(initialData ? dataGetter(initialData) || [] : []);
  // 本次请求的数据
  const listDataGetter = (rawData: any) => dataGetter(rawData) || rawData;
  // 发送请求
  const getHandlerMethod = (refreshPage = page.get()) => {
    const pageSizeVal = pageSize.get();
    const handlerMethod = handler(refreshPage, pageSizeVal);
    return handlerMethod;
  };
  // 计算data、total、isLastPage参数
  const total = useState<number>(initialData ? totalGetter(initialData) : undefined);
  const pageCount = computed(() => {
    const totalVal = total.get();
    return totalVal !== undefined ? Math.ceil(totalVal / pageSize.get()) : undefined;
  });
  // 如果返回的数据小于pageSize了,则认定为最后一页了
  const isLastPage = computed(() => {
    const dataRaw = states.data.get();
    if (!dataRaw) {
      return true;
    }
    const statesDataVal = listDataGetter(dataRaw);
    const pageVal = page.get();
    const pageCountVal = pageCount.get();
    const dataLen = Array.isArray(statesDataVal) ? statesDataVal.length : 0;
    return pageCountVal ? pageVal >= pageCountVal : dataLen < pageSize.get();
  });

基本上就是抄的源码,删减了部分代码。下面解释一下。

  • useRef

    ts 复制代码
        /**
           * 模拟useRef,使得闭包也能获取最新的值
           */
      const useRef = <T>(value: T) => ({ current: value });
  • useState

    ts 复制代码
    /**
     * 生成mobx可监听的值
     */
    const useState = <T>(value: T, deep: boolean = false) => observable.box(value, { deep });
  • 有大量getterdataGetterlistDataGetter等等,这些函数可供外部自定义操作方法。

  • getHandlerMethod的作用是pageSize在外部被改变的时候能够获取到最新的值,而且能通过传递refreshPage调整请求的页码。

  • pageCount我沿用的源码,通过计算得出的。其实如果后端有返回全部页码可以直接设置。

2.2. 监听变化

ts 复制代码
  // 监听外部输入的状态,重置page为1
  reaction(
    () => watchingStates.map(i => i.get()),
    (value, prevValue) => {
      page.set(initialPage);
      isReset.current = true;
    },
  );
  // 监听分页参数变化重新请求
  // 请求状态
  const states = useWatcher<S, E, R, T, RC, RE, RH>(
    getHandlerMethod,
    [...watchingStates, page, pageSize] as any,
    {
      immediate,
      initialData,
      middleware(ctx, next) {
        // 监听值改变时将会重置为第一页,此时会触发两次请求,在这边过滤掉一次请求
        let requestPromise: Promise<any> = Promise.resolve();
        if (!isReset.current) {
          requestPromise = next();
        } else if (requestCountInReseting.current === 0) {
          requestCountInReseting.current++;
          requestPromise = next();
        }
        return requestPromise;
      },
      // false
      force,
      // 如果上一个请求没结束,不强制结束上一个请求
      // 如果上拉加载第二页把第一页的请求结束了
      // 那么数据肯定就错乱了
      abortLast: false,
      ...rest,
    },
  );
  const { send } = states;
  • 第一个reaction目的是监听外部状态,一般就是请求参数变化,如果变化了就重置页码为1
  • useWatcher是监听导出的状态变化如请求参数变化、page改变就重新请求。
  • middleware中的代码就是重置和刷新的逻辑

2.3. 请求处理

ts 复制代码
  states.onSuccess(({ data: rawData, sendArgs: [refreshPage, isRefresh] }) => {
    runInAction(() => {
      isError.set(false);
      // 更新总数
      const newTotal = totalGetter(rawData);
      if (total.get() !== newTotal) {
        total.set(newTotal);
      }
      const pageSizeVal = pageSize.get();
      const listData = listDataGetter(rawData); // 获取数据数组

      // 如果追加数据,才更新data
      if (append) {
        // 如果是reset则先清空数据
        isReset.current && data.set([]);
        const cacheData: any[] = data.get();
        if (refreshPage === undefined) {
          data.set([...cacheData, ...listData]);
        } else if (refreshPage) {
          // 如果是刷新页面,则是替换那一页的数据
          cacheData.splice((refreshPage - 1) * pageSizeVal, pageSizeVal, ...listData);
          // 直接设置cacheData无效的,mobx内部有判断是否变化
          data.set(Array.prototype.concat.apply([], cacheData));
        }
      } else {
        data.set(listData);
      }
    });
  });
  states.onError(({ error }) => {
    runInAction(() => {
      isError.set(true);
    });
  });
  // 请求成功与否,都要重置它们
  states.onComplete(() => {
    isReset.current = false;
    requestCountInReseting.current = 0;
    // 取消下拉刷新
    wx.stopPullDownRefresh();
  });
  • onSuccess中就是对返回的data做操作,如果是追加模式就组合在一起,翻页就直接替换。
  • onError就是设置error。这个回调我没有发现官方代码有使用,好像没有对错误做操作。
  • onComplete复原状态,这里我加了一个停止下拉刷新。

2.4. 导出操作函数

这里有点和官方源码不一样,小程序的状态并不是双向的,更像是react是单向的。所以直接修改page达到翻页会有点繁琐。

因为你得先把这个hook返回的page放入小程序的data中,然后setData修改page值,然后在修改page的同时修改hook返回的page。这样才能视图和hook的状态一致。

所以我将翻页的方法改成调用函数。

ts 复制代码
  const getItemIndex = (item: any) => data.get().findIndex((i: any) => i.id === item.id);
  /**
   * 加载下一页
   */
  const nextPage = () => {
    runInAction(() => {
      if (isError.get()) {
        // 上次请求失败,重发
        refresh();
      } else if (!isLastPage.get()) {
        page.set(page.get() + 1);
      }
    });
  };

  /**
   * 刷新指定页码数据,此函数将忽略缓存强制发送请求
   * 如果未传入页码则会刷新当前页
   * 如果传入一个列表项,将会刷新此列表项所在页,只对append模式有效
   * @param pageOrItemPage 刷新的页码或列表项
   */
  const refresh = async (pageOrItemPage = page.get()) => {
    let refreshPage = pageOrItemPage;
    if (append) {
      if (isNaN(pageOrItemPage)) {
        const itemIndex = getItemIndex(pageOrItemPage);
        refreshPage = Math.floor(itemIndex / pageSize.get()) + 1;
      }
      // 更新当前页数据
      await send(refreshPage, true);
    } else {
      // 整页翻页模式不支持传列表项
      if (!isNaN(refreshPage)) {
        // 页数相等,则刷新当前页,否则fetch数据
        if (refreshPage === page.get()) {
          await send(undefined, true);
        } else {
          await fetch(getHandlerMethod(refreshPage), true);
        }
      }
    }
  };
  /**
   * 从第${initialPage}页开始重新加载列表,并清空缓存
   */
  const reload = () => {
    runInAction(() => {
      isReset.current = true;
      page.get() === initialPage ? send() : page.set(initialPage);
    });
  };
  • 我只实现了常用的函数,其他的函数根据这个思路自行实现
  • nextPage主要是实现翻页请求
  • refresh总体是赋值的源码,所以除了刷新当前页,还有根据传入的数据项,刷新数据项所在的页面(我没测试过)。
  • reload重置页面

2.5. return

ts 复制代码
return {
    ...state,
    page,
    pageSize,
    data,
    pageCount,
    total,
    isLastPage,
    refresh,
    isError,
    reload,
    nextPage,
}

3. 使用

上一篇文章封装的mapRequestHook,使用这个hook

ts 复制代码
const priceRange = observable.box<undefined | number>(undefined);
Component({
  behaviors: [
    mapRequestHook(() => ({
      productListPage: usePagination(
        (page, size) => getProductList({ page, size, priceRange: priceRange.get() }),
        {
          initialPageSize: 10,
          // 有个可变参数priceRange
          watchingStates: [priceRange],
        },
      ),
    })),
  ],
  onPullDownRefresh() {
      // 下拉刷新
      this.data.productListPage.reload();
  },
  onReachBottom() {
      // 触底下一页
      this.data.productListPage.nextPage();
  },
})
html 复制代码
    <view
      wx:for="{{productListPage.data}}"
    ></view>
  • 这里注意一下,watchingStates中的值必须是observable.box后的数据,也就是mobx可以监听的数据。
相关推荐
掘金者阿豪1 小时前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
kyriewen1 小时前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端2 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员2 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为2 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid2 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger3 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang4533 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang4534 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户059540174464 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css