# 第7次课 讲稿

> 课程：小程序开发
> 主题：网络请求与分页加载
> 教材：《微信小程序开发实战（第2版）》第3章
> 时长：3学时（理论 1 + 实践 2）
> 重点：wx.request、Promise封装、分页加载、下拉刷新
> 难点：异步请求处理、分页逻辑

---

## 一、开场（0:00）

到目前为止，我们页面上显示的数据都是写死在 js 里的。真实的应用不可能这样——商品列表、订单、消息，全都来自服务器。

今天要打通的是小程序和后端之间那条路：**wx.request**。

先提醒一个课堂上一定会遇到的坑：小程序要求请求地址必须是 **https**，而且域名要在后台配置白名单。开发阶段可以在开发者工具里勾"不校验合法域名"临时绕过，但上线前必须配好。这一条现在记住，能省你们后面两个小时。

今天的第二个重点是分页。为什么要分页？因为一个列表可能有一万条数据，一次全拉回来手机会卡死。分页的本质就是"用户滚到哪儿，我加载到哪儿"。

---

## 二、明确本课目标（约 5:00）

把目标写在白板上或直接翻到课件第 2 页，逐条念一遍：

- 掌握wx.request网络请求的用法
- 理解Promise与异步请求封装
- 掌握分页加载与下拉刷新的实现
- 能够对接后端API获取数据
- 案例：案例3-3 美食列表

本次课 3 学时的安排是：**理论讲解 1 学时，上机实践 2 学时**——网络请求与分页讲解 1 学时，案例 3-3 美食列表实操 2 学时。

---

## 三、知识点讲解

### 幻灯片 3：wx.request 网络请求

**【8:00 讲解】**

**url**

请求地址(必须HTTPS)

**method**

请求方法(GET/POST)

**data**

请求参数

**header**

请求头(content-type默认application/json)

- success / fail / complete 回调

### 幻灯片 4：Promise封装请求

**【13:00 讲解】**

- 封装为Promise: return new Promise((resolve, reject) => {})
- async/await语法糖: const res = await request(url)
- 统一错误处理: 封装公共fail回调
- 统一请求头: 封装token等认证信息
- 请求模块化: 按功能拆分API文件

### 幻灯片 5：分页加载与下拉刷新

**【18:00 讲解】**

- 上拉加载: onReachBottom触发, 请求下一页数据
- 下拉刷新: onPullDownRefresh触发, 重置数据
- 分页参数: page / pageSize
- 加载状态: isLoading防止重复请求
- 触底距离: onReachBottomDistance配置

### 幻灯片 6：案例3-3 美食列表

**【23:00 讲解】**

- 使用wx.request请求美食数据API
- 实现上拉加载更多(分页)
- 实现下拉刷新(重置列表)
- 加载状态提示(loading/没有更多了)
- 封装请求模块统一管理API

---

## 四、难点突破：用类比讲清楚

下面这几个点是学生最容易卡住的地方，抽象讲不通就换成类比。

### 回调地狱与 Promise

回调嵌套像俄罗斯套娃，拆到第四层你已经不知道自己在哪儿了。Promise 把它拉平成一条流水线，async/await 更进一步，让异步代码读起来像同步代码——写法上就是加两个关键字，理解成本却降了一大截。

### 分页参数

像图书馆借书：page 是"第几摞"，pageSize 是"一摞几本"。服务端按这两个数字给你切一段出来。关键是客户端要自己记住"我现在翻到第几摞了"。

### isLoading 标志位

像门口挂的"正在配货"牌子。用户手快连滑三下，如果没有这个牌子，就会同时发出三个一模一样的请求，回来的数据还会重复追加。加一个布尔值当锁，是最简单有效的防抖。

---

## 五、代码演示环节

以下每一段都建议**现场敲**，不要直接贴完整代码。边敲边问"下一行该写什么"，让学生跟上节奏。

### 演示 1：wx.request网络请求

**文件：** `request.js`　**预期效果：** wx.request 请求流程

```js
// wx.request 基本用法
wx.request({
url: 'https://api.example.com/data',
method: 'GET',
data: { page: 1, size: 10 },
header: {
'content-type': 'application/json'
},
success(res) {
console.log(res.data)
},
fail(err) {
console.error('请求失败', err)
},
complete() {
console.log('请求完成（无论成功失败）')
}
})

// ⚠️ 注意事项
// 1. 必须在 app.json 配置合法域名
// 2. 并发上限 10 个请求
// 3. 默认超时 60s，可通过 timeout 修改
wx.request({
url: 'https://api.example.com/data',
timeout: 10000,
method: 'POST',
data: { name: 'test' },
header: { 'Authorization': 'Bearer xxx' }
success(res) { console.log(res) }
})
```

**讲解口径：**

**wx.request** 是小程序发起网络请求的核心 API。主要参数有：**url** 请求地址，必须在后台配置合法域名；**method** 请求方法默认 GET；**data** 请求参数；**header** 请求头。三个回调函数：**success** 成功、**fail** 失败、**complete** 无论成败都执行。注意并发上限10个，默认超时60秒。

### 演示 2：Promise封装请求

**文件：** `http.js`　**预期效果：** Promise 封装流程

```js
// 封装 wx.request 为 Promise
const request = (options) => {
return new Promise((resolve, reject) => {
wx.request({
...options,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(`请求错误：${res.statusCode}`)
}
},
fail: (err) => reject(err)
})
})
}

// 封装常用方法
const http = {
get(url, data) {
return request({ url, method: 'GET', data })
},
post(url, data) {
return request({ url, method: 'POST', data })
}
}

// 使用 async/await 调用
async function loadData() {
try {
const res = await http.get('/api/list', { page: 1 })
console.log(res)
} catch (err) {
console.error(err)
}
}

module.exports = { http }
```

**讲解口径：**

将 **wx.request** 封装为 Promise，可以避免回调地狱。核心是用 **new Promise** 包裹 wx.request，成功时 **resolve**，失败时 **reject**。再封装 get、post 等快捷方法，使用时配合 **async/await** 让异步代码像同步一样清晰，用 **try/catch** 统一处理错误。

### 演示 3：分页加载与下拉刷新

**文件：** `page-scroll.js`　**预期效果：** 分页加载效果

```js
Page({
data: {
list: [],
page: 1,
pageSize: 10,
total: 0,
loading: false
},

// 页面加载时获取第一页数据
onLoad() {
this.getList()
},

// 获取列表数据
async getList() {
if (this.data.loading) return
this.setData({ loading: true })
const res = await http.get('/api/list', {
page: this.data.page,
pageSize: this.data.pageSize
})
this.setData({
list: this.data.list.concat(res.data),
total: res.total,
loading: false
})
},

// 触底加载更多
onReachBottom() {
if (this.data.list.length >= this.data.total) return
this.setData({ page: this.data.page + 1 })
this.getList()
},

// 下拉刷新
onPullDownRefresh() {
this.setData({ list: [], page: 1 })
this.getList()
wx.stopPullDownRefresh()
}
})

// app.json 中需开启下拉刷新
// { "enablePullDownRefresh": true }
```

**讲解口径：**

分页加载是列表页的核心功能。**onReachBottom** 是页面触底时自动触发的生命周期函数，用于加载下一页数据；**onPullDownRefresh** 是下拉刷新，需要先在 app.json 中开启。分页逻辑：维护 **page** 和 **pageSize**，触底时 page+1 再请求，用 **concat** 拼接新数据；下拉刷新时重置 page 为1、清空列表再请求，最后调用 **wx.stopPullDownRefresh** 停止刷新动画。

### 演示 4：案例3-3美食列表

**文件：** `shoplist.js`　**预期效果：** 美食列表页面

```js
// shoplist.js - 美食列表页
const http = require('../../utils/http')

Page({
data: {
shopList: [],
page: 1,
pageSize: 10,
total: 0
},

onLoad(options) {
this.getShopList()
},

async getShopList(callback) {
const res = await http.get('https://api.example.com/shops', {
page: this.data.page,
pageSize: this.data.pageSize
})
this.setData({
shopList: [...this.data.shopList, ...res.data],
total: res.total
})
callback && callback()
},

onReachBottom() {
if (this.data.shopList.length >= this.data.total) {
return wx.showToast({ title: '没有更多了', icon: 'none' })
}
this.setData({ page: this.data.page + 1 })
this.getShopList()
},

onPullDownRefresh() {
this.setData({ page: 1, shopList: [] })
this.getShopList(() => {
wx.stopPullDownRefresh()
})
}
})

<!-- shoplist.wxml -->
<view class="shop-item" wx:for="{{shopList}}" wx:key="id">
<image src="{{item.image}}" />
<view>{{item.name}} - ¥{{item.price}}</view>
</view>
```

**讲解口径：**

案例3-3美食列表完整实现了网络请求加分页加载。使用封装好的 **http.get** 请求数据，**onLoad** 时加载第一页。触底时 **onReachBottom** 判断是否还有数据，有则 page+1 继续请求，用展开运算符 **[...oldList, ...newList]** 拼接数据；没有则提示"没有更多了"。下拉刷新 **onPullDownRefresh** 重置 page 和列表，请求完成后调用 **wx.stopPullDownRefresh**。

---

## 六、常见错误与排查

上机环节学生一定会遇到下面这些问题，提前讲一遍能省掉大量重复答疑。

| # | 现象 | 原因与解决 |
|:-:|------|------------|
| 1 | 请求成功但页面没变 | success 回调里忘了 setData，或者用了箭头函数以外的写法导致 this 指向丢失。用箭头函数或先存 const that = this。 |
| 2 | 上拉加载重复请求 | onReachBottom 会连续触发多次，必须用 isLoading 加锁，并且在"没有更多了"之后直接 return。 |
| 3 | 下拉刷新转圈不停 | onPullDownRefresh 里数据回来后必须调 wx.stopPullDownRefresh()，否则那个圈会一直转。 |
| 4 | POST 请求后端收不到参数 | header 的 content-type 默认是 application/json。如果后端按表单格式解析，要改成 application/x-www-form-urlencoded 并自行拼接 body。 |

---

## 七、上机实践指引

本次课对应教材案例 1 个，源码已放在站点 `code/session07/` 下：

- **【案例3-3】美食列表** — 含 知识储备、案例实现、服务器端 共 3 个工程

建议顺序：先带学生跑一遍「知识储备」里的最小示例，确认每个知识点都能单独跑通，再进「案例实现」做完整案例。直接上完整案例，出错了不知道错在哪。

---

## 八、本课小结

回到课件小结页，逐条回顾：

- wx.request网络请求
- Promise封装请求
- 分页加载与下拉刷新
- 案例3-3美食列表

今天这节课的分量很重——网络请求是所有联网应用的命脉，后面每一个案例几乎都要用到。

案例3-3 美食列表把请求、分页、下拉刷新、加载状态全串起来了，是本课程第一个"像真实产品"的案例。挑战任务让大家封装 request 模块，这一步做扎实了，第13次课做点餐系统会轻松很多。

---

## 九、布置作业

三档任务，基础必做，进阶建议做，挑战选做。

### 【基础】完成美食列表

按照案例3-3，完成美食列表的分页加载与下拉刷新功能

### 【进阶】加载状态优化

增加"加载中"提示、到底提示"没有更多数据"、网络错误提示

### 【挑战】请求模块封装

把 wx.request 封装为返回 Promise 的 utils/request.js，统一注入请求头与错误提示，并用 async/await 重写美食列表的分页请求

---

> 配套资源：课件 `slides-v2/session07.pptx`　课堂视频 `videos-output/session07.mp4`　扩展阅读 `docs/第7次课_扩展阅读.md`
