主题
开发首页的业务与接口与文档(Day7 2/2)
本页对应 day7,共 5 段视频,约 1 小时 31 分。
学习目标
- 能够依据接口文档在
api/index中统一管理首页与课程接口,并通过继承请求基类的实例调用get方法完成带参请求,正确传入分页参数并获取返回数据。 - 能够在首页
onLoad生命周期中触发数据请求,将课程与教师数据分别存入响应式变量,并通过父子组件props与插槽完成首页课程、教师列表渲染。 - 能够通过数据标识区分课程与教师数据,在复用组件中按字段差异正确渲染
cover/avatar等数据,并使用条件渲染区分不同展示结构。 - 能够封装并接入首页置顶组件,监听页面滚动位置控制组件显示与隐藏,点击后调用滚动 API 返回页面顶部。
- 能够完成课程列表静态结构与样式搭建,接入
uni-ui图标与easycom配置,并按接口字段渲染课程封面、标题、价格、购买人数。 - 能够实现课程分页加载业务,维护页码、每页条数、加载状态与“是否还有更多”状态,在触底生命周期中判断并追加下一页数据,正确展示“没有更多”提示。
知识点
先搭静态,再接数据
这一节的主线是开发首页业务。转写稿反复强调“先搭建静态”,因为首页结构没定下来就接接口,后面字段一改、布局一调,数据渲染会反复返工。怎么想:先把首页拆成“热门课程/名师大咖列表 + 课程卡片 + 置顶按钮”,静态结构跑通后再用接口数据替换。怎么做:在首页中先写列表容器和卡片结构,再通过 onLoad 获取首页数据。易错点:不要一开始就把请求、分页、组件通信全混在一起;先确认页面能显示,再逐项接数据。
uniapp 中组件的使用差异
笔记 2.1 要区分 Vue2、Vue3 和 uniapp 的组件使用。为什么需要:不同版本注册方式不同,写错会直接不渲染。怎么想:Vue2 是“引入、注册、使用”;Vue3 是“引入、使用”;uniapp 中内置组件和扩展组件按平台规则使用。怎么做:Vue2 在 components 中注册;Vue3 组合式写法引入后直接使用;uniapp 页面中可直接使用内置组件,扩展组件如 uni-ui 配好 easycom 后也可直接用。易错点:不要把小程序的 {{}} 语法和 Vue 指令混用;转写稿中特别提醒“这不是小程序,不能加双括号”,应按 Vue 模板语法写。
请求统一管理:类、继承与 API 文件
为什么需要:项目接口多,地址散落在页面里,改一个地址要全项目搜。怎么想:先封装请求类,再建 api 目录统一管理接口。转写稿中的做法是让 Request 继承已封装好的 Service,这样 Request 的实例能复用父类的 get/post/delete 方法;API 文件对外暴露实例或方法,页面只调 API。怎么做:在 api/index 中引入请求服务,定义 reqHome 之类方法;页面引入后调用。易错点:继承后页面调的是实例方法,不要重复写一套请求逻辑;实例化时是否需要传参要看封装,转写稿中“不加小括号”是在讨论初始化参数,不能机械照抄。调试时先打印返回结果,确认是 Promise 结果再取 data。
首页 onLoad 获取数据
为什么需要:首页一挂载就要拿到课程和数据。怎么想:用 uniapp 页面生命周期 onLoad,不要直接在页面顶层写 await。怎么做:onLoad 中调用 getHome(),函数内 await reqHome(),先 console.log 看结构,再分别存入课程数组和数组。易错点:转写稿中先打印 123 验证 onLoad 是否执行,再发请求;字段名不要手写猜,必须看接口文档,例如课程封面是 cover,头像是 avatar,写错就渲染不出来。
复用卡片组件与插槽区分课程/
为什么需要:热门课程和名师大咖结构相似,重复写两套维护成本高。怎么想:复用同一个 card 组件,标题通过 props 传入;中间内容用插槽。难点是课程和字段不同,同一个组件拿两个数组会混。怎么做:给数据加 type 标识,课程用 course,用 teacher;模板中按 type 判断取 cover 还是 avatar。转写稿还通过 type 判断课程才显示“多少人购买”等底部结构。易错点:不要以为同一个组件给两个数组就能自动适配;字段不同必须区分。调试时先刷新看图片是否出来,发现图不出,就是字段名不对。
置顶组件与滚动监听
为什么需要:移动端页面长,置顶按钮方便回顶部。怎么想:封装 back-top 组件,固定到右下角;监听页面滚动,超过一定距离显示,点击滚回顶部。怎么做:用 onPageScroll 拿 scrollTop,用响应式变量控制显示;点击时调 uni.pageScrollTo({ scrollTop: 0, duration: 200 })。易错点:Vue3 中不要写 this;pageScrollTo 参数是配置对象;转写稿调试时先打印滚动值,再设 200 像素阈值。
课程静态搭建与分页加载
为什么需要:课程列表要支持首屏 10 条和触底加载更多。怎么想:先搭左右结构:左图、右标题/价格/购买人数;再接分页接口。怎么做:onLoad 拿第一页,onReachBottom 判断状态为 more 时页码加一并再次请求;用 status 控制“没有更多”。易错点:转写稿中第一次请求 10 条、总数 12 条,触底后只返回 2 条;必须根据返回条数判断是否还有下一页,不能无脑加页码。uni-ui 图标可按文档使用,如 uni-icons 配 type、color、size;两行省略用 display: -webkit-box、-webkit-line-clamp: 2、-webkit-box-orient: vertical,记不住可查,不要死背。
接口与文档:先确定请求边界
本节服务器地址是 https://gmall-prod.atguigu.cn/skb,接口文档是 https://www.yuque.com/xiumubai/fe/xc573qhbzqepug47#aU4qO。做接口开发前,先看文档确认三件事:请求地址、请求方式、返回数据结构。转写里反复强调“首页接口返回包含课程和两个数组”,所以页面挂载后要先拿首页数据,再分别存课程数据和数据。
为什么需要:不看文档就写字段,最容易把 cover、avatar、title 这类字段写错。
怎么想:文档是接口契约,页面字段必须从文档和打印结果里来。
怎么做:先打开文档找“首页数据”接口,再在代码里打印 result 看结构。
易错点:小程序开发者工具里不一定能直接看到接口数据时,不要猜字段,先打印、再看文档;data 不要写成 date。
3.1 网络请求二次封装:用类复用 get/post
转写里说,公司内部封装经常喜欢用“类”,看起来类似 axios 的 get、post 套路。这里的思路是:底层 Service 类提供请求方法,Request 类继承 Service,业务侧导出 Request 的实例,页面调用实例方法发请求。
为什么需要:每个页面都写一遍 uni.request,地址、参数、错误处理会重复;统一封装后,页面只关心“调哪个接口”。
怎么想:Service 负责“怎么发请求”,Request 负责“业务请求类”,API 文件负责“接口地址统一管理”。
怎么做:按转写整理成如下结构(接口地址按文档填写,不在这里编造具体路径):
js
// utils/service.js
export default class Service {
get(url, options = {}) {
return uni.request({
url,
method: 'GET',
...options
})
}
post(url, data, options = {}) {
return uni.request({
url,
method: 'POST',
data,
...options
})
}
}js
// utils/request.js
import Service from './service'
class Request extends Service {}
// 导出实例,页面直接 request.get / request.post
export default new Request()易错点:导出时不要只导出类却让页面自己 new,转写里明确说“对外暴露类的实例”;new Request() 的小括号要写,表示初始化实例;继承后 Request 实例才能借用 Service 的 get、post、delete 方法。
API 层统一管理接口
转写里说“API 说白了还是要统一管理整个项目当中所有的接口”,这样某个接口变化时只改一处。
为什么需要:页面里散落接口地址,后期改地址会到处找。
怎么想:页面不直接写请求地址,只调 API 函数。
怎么做:
js
// api/index.js
import request from '@/utils/request'
// 首页接口地址按接口文档填写
export const reqHome = () => request.get('文档中的首页接口地址')页面里引入 API:
js
import { onLoad } from '@dcloudio/uni-app'
import { reqHome } from '@/api/index'
onLoad(() => {
getHome()
})
const getHome = async () => {
const result = await reqHome()
console.log('首页数据', result)
// 再按文档字段取课程数组、数组
}易错点:onLoad 是 uni-app 页面生命周期,转写里特别说从 @dcloudio/uni-app 引入;await 外层函数要 async;onLoad 里不要堆大段业务逻辑,转写里建议抽 getHome。
调试顺序:先确认钩子,再确认数据
转写里的调试过程是:先在 onLoad 打印 123,确认页面挂载钩子执行;再发请求打印首页数据,确认拿到课程和两个数组;最后把数据存到响应式变量并渲染。这个顺序不要反。
为什么需要:如果直接渲染,字段错、钩子没执行、请求没回来会混在一起。
怎么想:先验证“钩子执行”,再验证“请求返回”,最后验证“字段渲染”。
怎么做:先 console.log(123),再 console.log(result),确认结构后再写 result.data 下的课程、数据。
易错点:转写里提到同一个组件接收课程和两类数据时字段不一样,要用类型标识区分;字段名最好从文档复制,别自己拼写。
课堂画面





代码示例
示例一:网络请求二次封装 + 接口统一管理(utils/request.js + api/index.js)
js
/* ============ utils/request.js ============ */
class Service {
constructor() {
this.baseURL = 'https://gmall-prod.atguigu.cn/skb'
}
// 真正发请求的地方:uni.request 的二次封装,返回 Promise
request({ url, method = 'GET', data = {} }) {
return new Promise((resolve, reject) => {
uni.request({
url: this.baseURL + url,
method,
data,
success: (res) => {
if (res.statusCode === 200) resolve(res.data)
else reject(res)
},
fail: reject
})
})
}
get(options) { return this.request({ ...options, method: 'GET' }) }
post(options) { return this.request({ ...options, method: 'POST' }) }
delete(options) { return this.request({ ...options, method: 'DELETE' }) }
}
export default Servicejs
/* ============ api/index.js ============ */
import Service from '@/utils/request.js'
// 继承:Request 的实例可以直接用父类原型上的 get / post / delete
class Request extends Service {
// 首页数据(课程 + )
getHome() {
return this.get({ url: '/index/index' })
}
// 课程列表:page / limit 拼在路径上
getCourse(page, limit) {
return this.get({ url: `/course/list?page=${page}&limit=${limit}` })
}
}
// 对外暴露的是【实例】,不是类
export default new Request()关键行与易错点
class Request extends Service:这就是“类 + 继承”的套路,Request的实例能直接调this.get(...),写法和ajax.get / ajax.post一模一样。export default new Request():注意后面要加小括号。漏了括号暴露的就是“类”,页面里还得new一次才能发请求,这是最常见的坑。api/index.js是接口统一出口:将来某个接口地址变了,只改这一个文件,页面不用动。get({ url })传的是对象,page / limit直接拼在路径上,不要再塞进data。
示例二:首页数据获取渲染 + 卡片组件复用(pages/index/index.vue + components/card/card.vue)
vue
<!-- ============ pages/index/index.vue ============ -->
<template>
<view class="home">
<Card title="热门课程" :list="courseArr" type="course" />
<Card title="名师大咖" :list="teacherArr" type="teacher" />
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import ajax from '@/api/index.js'
import Card from '@/components/card/card.vue'
// 两个数组分别存课程、
const courseArr = ref([])
const teacherArr = ref([])
const getHome = async () => {
const result = await ajax.getHome()
console.log('首页数据', result)
courseArr.value = result.data.course
teacherArr.value = result.data.teacher
}
// 页面一挂载就发请求
onLoad(() => {
getHome()
})
</script>
<style lang="scss">
.home { background-color: #ccc; }
</style>vue
<!-- ============ components/card/card.vue ============ -->
<template>
<view class="card">
<!-- 标题通过 props 传进来,组件才能复用 -->
<view class="title">{{ title }}</view>
<view class="item" v-for="item in list" :key="item.id">
<!-- 同一个组件给两个数组,字段不一样:靠 type 区分 -->
<image class="cover" :src="type === 'course' ? item.cover : item.avatar" />
<!-- 课程结构 -->
<view class="right" v-if="type === 'course'">
<view class="name">{{ item.title }}</view>
<view class="bottom">
<text class="price">¥{{ item.price }}</text>
<text class="buy">已有{{ item.buyCount }}人购买</text>
</view>
</view>
<!-- 结构 -->
<view class="right" v-else>
<view class="intro">{{ item.intro }}</view>
<view class="name">{{ item.name }}</view>
</view>
</view>
</view>
</template>
<script setup>
defineProps({
title: { type: String, default: '' },
list: { type: Array, default: () => [] },
type: { type: String, default: 'course' }
})
</script>
<style lang="scss">
.card { background: #fff; margin-bottom: 20rpx; padding: 20rpx; }
.title { font-weight: 700; font-size: 32rpx; margin-bottom: 20rpx; }
.item { display: flex; padding: 20rpx 0; }
.cover { width: 220rpx; height: 160rpx; }
.right {
flex: 1; /* 右侧占满剩余空间 */
margin-left: 20rpx;
display: flex;
flex-direction: column; /* 一上一下 */
justify-content: space-between;
}
.name { font-weight: 700; }
.bottom { display: flex; justify-content: space-between; }
.price { color: #f00; }
/* 介绍:两行超出隐藏,不用背,抄一次就会了 */
.intro {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
</style>关键行与易错点
onLoad从@dcloudio/uni-app引入(不是 vue),这是页面生命周期;页面挂载后发请求,不要直接把await写在onLoad里。- 父子通信:父组件
:list="courseArr"+type="course",子组件用defineProps接收;title由父组件传入,标题就不再写死,组件才能复用两次。 - 同一个组件给两个数组,字段不一样(课程
cover、avatar),必须打type标识做区分,否则图片渲染不出来。 - 结构不同就用
v-if="type === 'course'"/v-else分两套结构,没必要为此再拆两个组件。 - 两行省略三件套:
display: -webkit-box+-webkit-box-orient: vertical+-webkit-line-clamp: 2,再配overflow: hidden。
示例三:置顶组件 + 课程列表分页加载(components/back-top/back-top.vue + pages/course/course.vue)
vue
<!-- ============ components/back-top/back-top.vue ============ -->
<template>
<image class="btn" src="/static/backTop.png" @click="toTop" />
</template>
<script setup>
const toTop = () => {
// 点击后页面滚回顶部
uni.pageScrollTo({ scrollTop: 0, duration: 200 })
}
</script>
<style lang="scss">
.btn {
position: fixed; /* 固定定位 */
right: 12rpx;
bottom: 100rpx;
width: 100rpx;
height: 100rpx;
}
</style>vue
<!-- ============ 在首页 pages/index/index.vue 中追加 ============ -->
<template>
<view class="home">
<!-- 其它内容 -->
<BackTop v-if="backFlag" />
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onPageScroll } from '@dcloudio/uni-app'
import BackTop from '@/components/back-top/back-top.vue'
const backFlag = ref(false)
// 监听页面滚动,滚动距离超过 200 再显示
onPageScroll((e) => {
backFlag.value = e.scrollTop >= 200
})
</script>vue
<!-- ============ pages/course/course.vue ============ -->
<template>
<view class="course">
<view class="item" v-for="item in courseList" :key="item.id">
<image class="cover" :src="item.cover" />
<view class="right">
<view class="title">{{ item.title }}</view>
<view class="bottom">
<text class="price">¥{{ item.price }}</text>
<text class="buy">已有{{ item.buyCount }}人购买</text>
</view>
</view>
</view>
<!-- 没有更多了:左右两条线用两个 view 画 -->
<view class="no-more" v-if="status === 'noMore'">
<view class="line"></view>
<text>没有更多了</text>
<view class="line"></view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import ajax from '@/api/index.js'
const page = ref(1) // 默认第一页
const limit = ref(10) // 每页 10 条
const status = ref('more') // more / noMore
const courseList = ref([])
const getCourse = async () => {
const result = await ajax.getCourse(page.value, limit.value)
console.log('课程数据', result)
// 追加,不是覆盖
courseList.value = [...courseList.value, ...result.data.items]
// 返回条数等于 10 说明还有下一页
status.value = result.data.items.length === limit.value ? 'more' : 'noMore'
}
onLoad(() => {
getCourse()
})
// 触底:有下一页才继续请求
onReachBottom(() => {
if (status.value === 'more') {
page.value++
getCourse()
}
})
</script>
<style lang="scss">
.course { background-color: #ccc; }
.item {
display: flex;
background: #fff;
padding: 20rpx;
margin: 20rpx 0;
}
.cover { width: 220rpx; height: 160rpx; }
.right {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.bottom { display: flex; justify-content: space-between; }
.price { color: #f00; }
.no-more {
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
.line {
width: 100rpx;
height: 2rpx;
background-color: #ccc;
margin: 0 20rpx;
}
</style>关键行与易错点
onPageScroll也是页面生命周期,从@dcloudio/uni-app引入;滚动距离在事件对象e.scrollTop上,backFlag为ref,赋值必须写.value。- 置顶组件用
position: fixed固定在右下角,显示隐藏用v-if控制即可,别在组件里写死display。 uni.pageScrollTo({ scrollTop: 0, duration: 200 })传的是配置对象,duration单位是毫秒,写太大回弹会很慢。- 分页两个坑:① 新数据要
[...老数据, ...新数据]追加,直接赋值会把前面的覆盖掉;② 触底时先判断status === 'more',page.value++之后再请求,否则会无限重复请求同一页。 - “没有更多了”的左右横线就是两个小
view设固定宽高加背景色,用 flex 居中排开。
常见错误
现象:页面里直接写
uni.request,或每次请求都手写 URL。
原因:绕过了网络请求二次封装和 API 统一管理。
修法:页面只调用 API 层暴露的实例方法,如req.getHome();URL 统一放api/index.ts。现象:API 层导出类本身,页面调用时报“方法不存在”或需要反复
new。
原因:对外暴露的是类,不是可直接使用的实例。
修法:统一导出实例,如export const req = new Request();页面引入后直接req.xxx()。现象:
onLoad里直接写await,或把大量请求逻辑堆在生命周期里。
原因:生命周期函数不适合承载复杂异步业务,且容易写成非async。
修法:onLoad只调用方法,如getHomeData();方法内部再async/await。现象:首页课程和复用同一个 Card 组件后,图片、标题、介绍显示空白或错位。
原因:两类数据字段不同,如课程用cover,用avatar,组件未区分数据类型。
修法:父组件传type标识;子组件按type取对应字段,并用v-if控制不同结构。现象:置顶按钮滚动后显示/隐藏逻辑反了,或滚动监听不生效。
原因:阈值判断写反;Vue3 中直接改this.xxx;生命周期未正确引入。
修法:用ref控制显隐并.value赋值;从@dcloudio/uni-app引入onPageScroll;点击回顶用uni.pageScrollTo({ scrollTop: 0, duration: 200 })。现象:课程列表触底后重复请求、页码不增加,或没有更多数据仍继续请求。
原因:未维护分页状态,未判断status/more,也未做请求防重。
修法:维护page、status;触底先判断是否还有更多;请求前页码自增,返回后合并数组并更新状态。现象:
uni-icons不显示,或 easycom 配置后仍报组件不存在。
原因:未安装依赖、未配置easycom,或组件名/图标名写错。
修法:按文档安装依赖并配置easycom;组件直接使用;图标名、颜色、大小对照文档检查。现象:价格、购买人数、课程标题等渲染不出来。
原因:字段名拼写错误或未对照接口文档。
修法:以接口文档返回字段为准,先打印结果确认结构,再绑定字段。
练习
- 完成网络请求二次封装:创建请求类并继承基础服务类,对外导出可直接调用的实例。
- 在首页
onLoad中拉取首页数据,分别存储课程和数据,并渲染到页面。 - 改造 Card 组件:通过
type区分课程和,分别处理图片、标题、介绍和底部结构。 - 实现置顶组件:页面滚动超过阈值显示,点击后平滑回到顶部。
- 完成课程列表分页:首屏加载第一页,触底加载下一页,无更多数据时展示“没有更多”。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 热门课程的数据获取与渲染 | 22:14 |
| 2 | 完成首页课程数据的渲染 | 16:52 |
| 3 | 首页的置顶组件 | 11:52 |
| 4 | 课程的静态搭建 | 12:23 |
| 5 | 课程业务 | 28:25 |
小结
- 首页业务先搭静态结构,再接接口和动态数据。
- 请求必须走二次封装,接口地址统一放 API 层管理。
- 页面生命周期只做调度,复杂异步逻辑抽成独立方法。
- 同一组件复用于多类数据时,用
type明确数据来源和渲染分支。 - 置顶、触底加载等交互要绑定对应页面生命周期,并控制好状态。
- 分页核心是页码、请求状态、数据合并和“没有更多”提示。
- 字段名以接口文档为准,渲染前先打印确认数据结构。
uni-ui组件优先按文档安装和配置,避免手写重复组件。