微信小程序使用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可以监听的数据。
相关推荐
Momo__5 分钟前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富11 分钟前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇11 分钟前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇11 分钟前
React中的forwardRef
前端·react.js·面试
槑有老呆20 分钟前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马22 分钟前
Verilog开发常见问题汇总解析
前端
子兮曰24 分钟前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端
weedsfly28 分钟前
语法糖褪去之后——Babel 转译产物中的 JavaScript 本貌
前端·javascript
JustHappy30 分钟前
「软件设计思想杂谈🤔」“切图仔”也能懂编译原理?框架源码也许没那么难。聊聊 Vue 的编译(上)
前端·javascript·vue.js