下面给你 两种常见实现方式 的完整示例代码与注意点 —— 都基于 uni-app(Vue 风格)。

  • 方法 A:点击“加载更多”按钮(用户主动点击)
  • 方法 B:滚动到底部自动加载(onReachBottom / scroll-view 的 scrolltolower)

示例包含:数据结构、请求示例、去重/节流、加载状态、无更多提示、错误处理。可直接复制到 pages/list/list.vue 里运行(替换为你自己的接口)。


后端约定(示例)

假设后端分页接口 /api/items 支持 pagepageSize 参数,返回示例:

{
  "code": 0,
  "data": {
    "items": [{ "id":1, "title":"A" }, ...],
    "total": 123
  }
}


方法 A:点击“加载更多”按钮(适合用户主动加载)

<template>
  <view class="page">
    <view class="list">
      <block v-for="item in items" :key="item.id">
        <view class="item">{{ item.title }}</view>
      </block>

      <!-- 空数据提示 -->
      <view v-if="items.length === 0 && !loading" class="empty">暂无数据</view>

      <!-- 加载中指示 -->
      <view v-if="loading" class="loading">加载中...</view>

      <!-- 错误提示 -->
      <view v-if="error" class="error">{{ error }}</view>

      <!-- 加载更多按钮 / 无更多提示 -->
      <view class="more-area">
        <button v-if="!noMore && !loading" @click="loadMore">加载更多</button>
        <view v-else-if="noMore" class="no-more">没有更多了</view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      items: [],
      page: 1,
      pageSize: 10,
      loading: false,
      noMore: false,
      total: 0,
      error: null,
      // 防止重复请求的标志
      isRequesting: false
    };
  },
  onLoad() {
    // 首次加载第一页
    this.fetchPage(1);
  },
  methods: {
    async fetchPage(page) {
      if (this.isRequesting) return;
      this.isRequesting = true;
      this.loading = true;
      this.error = null;

      try {
        const res = await uni.request({
          url: 'https://api.example.com/items',
          method: 'GET',
          data: {
            page,
            pageSize: this.pageSize
          }
        });

        // 假设后端返回 res[1].data
        const body = res[1].data;
        if (body.code !== 0) {
          throw new Error(body.message || '服务器返回错误');
        }
        const { items: newItems, total } = body.data;
        this.total = total ?? this.total;

        if (page === 1) {
          this.items = newItems;
        } else {
          this.items = this.items.concat(newItems);
        }

        this.page = page;
        // 判断是否到尾
        if (this.items.length >= this.total || newItems.length < this.pageSize) {
          this.noMore = true;
        } else {
          this.noMore = false;
        }
      } catch (e) {
        console.error('fetchPage error', e);
        this.error = e.message || '请求失败';
      } finally {
        this.loading = false;
        this.isRequesting = false;
      }
    },

    // 点击加载下一页
    loadMore() {
      if (this.noMore || this.loading) return;
      this.fetchPage(this.page + 1);
    },

    // 下拉刷新(可选)
    async onPullDownRefresh() {
      await this.fetchPage(1);
      uni.stopPullDownRefresh();
    }
  }
};
</script>

<style>
.page { padding: 16px; }
.item { padding: 12px; border-bottom: 1px solid #eee; }
.loading, .no-more, .empty, .error { text-align:center; padding:12px; color:#888; }
.more-area { display:flex; justify-content:center; padding:12px; }
button { padding:8px 20px; }
</style>


方法 B:滚动到底部自动加载(适合长列表、无限滚动)

有两种实现:

  • 页面级 onReachBottom()(H5、小程序都支持)
  • 使用 <scroll-view @scrolltolower>(更灵活,可放固定高度区块)

页面级 onReachBottom 示例(推荐简洁):

&lt;template>
  &lt;view class="page">
    &lt;view class="list">
      &lt;block v-for="item in items" :key="item.id">
        &lt;view class="item">{{ item.title }}&lt;/view>
      &lt;/block>

      &lt;view v-if="loading" class="loading">加载中...&lt;/view>
      &lt;view v-if="noMore &amp;&amp; items.length>0" class="no-more">没有更多了&lt;/view>
      &lt;view v-if="items.length===0 &amp;&amp; !loading" class="empty">暂无数据&lt;/view>
    &lt;/view>
  &lt;/view>
&lt;/template>

&lt;script>
export default {
  data() {
    return { items: [], page: 1, pageSize: 10, loading: false, noMore: false, total: 0, isRequesting: false };
  },
  onLoad() { this.fetchPage(1); },
  onReachBottom() {
    // 页面滚动到底触发(H5 在某些平台需要配置)
    if (!this.noMore &amp;&amp; !this.loading) this.fetchPage(this.page + 1);
  },
  methods: {
    async fetchPage(page) {
      if (this.isRequesting) return;
      this.isRequesting = true;
      this.loading = true;

      try {
        const res = await uni.request({
          url: 'https://api.example.com/items',
          method: 'GET',
          data: { page, pageSize: this.pageSize }
        });
        const body = res[1].data;
        if (body.code !== 0) throw new Error(body.message || '服务器返回错误');
        const { items: newItems, total } = body.data;
        this.total = total ?? this.total;

        if (page === 1) this.items = newItems;
        else this.items = this.items.concat(newItems);

        this.page = page;
        if (this.items.length >= this.total || newItems.length &lt; this.pageSize) this.noMore = true;
      } catch (e) {
        console.error(e);
      } finally {
        this.loading = false;
        this.isRequesting = false;
      }
    }
  }
};
&lt;/script>

&lt;style>
.item { padding: 10px; border-bottom: 1px solid #eee; }
.loading, .no-more, .empty { text-align:center; padding:12px; color:#888; }
&lt;/style>

使用 <scroll-view> 的 scrolltolower(局部滚动区域)

&lt;template>
  &lt;scroll-view class="scroll" scroll-y="true" :scroll-top="0" @scrolltolower="onScrollToLower" lower-threshold="100">
    &lt;block v-for="item in items" :key="item.id">
      &lt;view class="item">{{ item.title }}&lt;/view>
    &lt;/block>
    &lt;view v-if="loading" class="loading">加载中...&lt;/view>
    &lt;view v-if="noMore" class="no-more">没有更多了&lt;/view>
  &lt;/scroll-view>
&lt;/template>

&lt;script>
export default {
  data() { return { items: [], page:1, pageSize:10, loading:false, noMore:false, isRequesting:false } },
  onLoad() { this.fetchPage(1); },
  methods: {
    onScrollToLower() {
      if (!this.noMore &amp;&amp; !this.loading) this.fetchPage(this.page+1);
    },
    async fetchPage(page){ /* 同上 */ }
  }
}
&lt;/script>

&lt;style>
.scroll { height: 80vh; }
.item { padding: 12px; border-bottom: 1px solid #f0f0f0; }
&lt;/style>


常见注意点(实战要点)

  1. 防止重复请求:用 isRequesting / loading 标志阻止重复触发。
  2. 节流/去抖:快速滚动或连点时建议对触发函数做节流(lodash/throttle)。
  3. 总数判断:如果返回 total,用 items.length >= total 判断无更多;否则根据 newItems.length < pageSize 判断。
  4. 空数据处理:列表为空时显示“暂无数据”。
  5. 错误与重试:请求失败时显示错误,允许用户重试。
  6. 体验优化:加载骨架屏、占位、上拉加载动画。
  7. 兼容性:微信小程序 onReachBottom 在页面配置与模板中正常支持;H5 下需注意页面高度与滚动容器(推荐使用 scroll-view 或监听页面滚动)。
  8. 后端支持:后端分页接口要幂等且稳定,避免重复返回数据或顺序不一致。建议后端返回 pagepageSizetotalitems