微信小程序使用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可以监听的数据。
相关推荐
Martin -Tang25 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发26 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html