主题
首页列表与云开发环境(一)
本页对应 首页列表与云开发环境,共 7 段视频,约 1 小时 9 分。
学习目标
- 能够将首页列表区域抽取为
feed-list组件,在首页正确引入,并将其放置在与吸顶元素同级的容器中,验证列表正常渲染且吸顶效果不失效。 - 能够为首页四个 tab 设计并补全包含
type、current、pageSize、list、hasMore、rendered的数据结构,并说明各字段在列表请求、分页和缓存中的作用。 - 能够基于 tab 点击返回的索引,使用计算属性获取当前
type、current页码和统一pageSize,验证切换不同 tab 时取值随索引正确变化。 - 能够封装
feedListAPI,以 GET 方式通过params传递type、current、pageSize,并在页面加载和 tab 切换时按条件发起请求,检查请求参数与返回的rows数据正确。 - 能够利用
rendered配合v-show与v-if控制列表组件的首次加载和缓存,验证初始仅当前 tab 组件加载、切换后组件不重复加载。 - 能够根据 tab 索引将接口返回的
rows追加到对应 tab 的list中,验证不同 tab 的数据分别存储且后续分页追加不会覆盖已有数据。
知识点
首页导航栏与列表组件拆分
为什么需要:首页静态结构已经粘好后,剩余核心是列表区域。列表在“推荐/关注/减脂/饮食”等 tab 下布局一致、数据不同,因此应把列表区域抽成组件,避免重复写四套结构。
怎么想:先隐藏系统导航栏,使用自定义导航栏;再把列表主体做成可复用组件。组件只负责展示,数据由页面通过 props 传入。
怎么做:在首页配置中加入:
json
{
"navigationStyle": "custom"
}然后在首页目录下建立 components/feed-list/feed-list.vue,组件内声明:
js
props: {
list: {
type: Array,
default: () => []
}
}页面中导入并使用。转写稿特别提醒:这种局部组件按常规方式导入即可,且组件默认导出通常只有一个,不要习惯性加花括号:
js
import FeedList from '@/components/feed-list/feed-list.vue'使用时要注意位置:列表组件必须与吸顶元素同级,放在同一个父盒子内。否则吸顶盒子的父级高度不够,之前的吸顶效果会失效。
易错点:一是导入组件误加 {};二是把列表组件放到吸顶盒子内部或错误层级,导致吸顶失效;三是误以为所有组件都能像 easycom 那样不注册直接用。
列表组件的数据结构
为什么需要:每个 tab 都有自己的类型、页码、列表数据和“是否已加载”状态。如果只用一个页码或一个数组,切换 tab 后分页会串。
怎么想:把 tab 数据从只有 label 扩展为包含 type、current、hasMore、list、rendered 等字段。type 决定请求哪类数据,current 记录该分类当前页码,hasMore 表示是否还有下一页,list 存该分类列表,rendered 表示该组件是否已渲染/请求过。
怎么做:在首页数据中维护 tab 数组,例如:
js
{
label: '推荐',
type: 'recommend',
current: 1,
hasMore: true,
list: [],
rendered: true
}其他 tab 初始 rendered: false。类型值必须以接口文档为准,转写稿强调不要自己猜,文档写什么就复制什么。
易错点:把页码写成全局一个;切换 tab 后仍从第一页请求,导致之前分页数据丢失;类型名手写拼错。
tab 切换后取出当前分类
为什么需要:点击 tab 后,页面只应处理当前 tab 对应的数据,而不是整个数组。
怎么想:tab 组件点击回调会给出索引 index。根据索引从 tab 数组中取当前项,再用计算属性派生当前 type、current 和 pageSize。
怎么做:
js
computed: {
feedType() {
return this.feedTabs[this.typeIndex].type
},
feedCurrent() {
return this.feedTabs[this.typeIndex].current
}
}调试时先打印 index,确认点击推荐、关注、减脂时索引分别为 0、1、2、3。转写稿中出现过报错:对数组使用点语法访问属性。原因是 feedTabs 是 ref 数据,必须先 .value 再取索引:
js
this.feedTabs.value[this.typeIndex].value具体写法取决于响应式定义方式,核心是:先取 ref 的 .value,再取数组项。
易错点:忘记 .value,报“访问 undefined 的属性”;v-for 时忘记取 index,导致 v-show 判断失效;数据名写错,页面一个列表也不显示。
uni-app 中的列表组件缓存
为什么需要:切换 tab 时如果组件反复销毁重建,已加载的分页数据会丢失,也会重复请求。Vue 中可用 keep-alive,但转写稿明确指出:在 uni-app 中该方案不能直接照搬,需要自己处理。
怎么想:用 v-show 控制显示隐藏,用 v-if 控制“只首次渲染”。rendered 为 false 时组件不渲染;用户首次切到该 tab 后,把对应 rendered 置为 true,之后只切换 v-show,组件不再重新加载。
怎么做:模板中遍历 tab:
vue
<FeedList
v-for="(item, index) in feedTabs"
:key="index"
v-if="item.rendered"
v-show="typeIndex === index"
:list="item.list"
/>点击 tab 时标记已渲染:
js
this.feedTabs.value[this.typeIndex].rendered = true调试时观察 mounted:如果页面一刷新就执行 4 次,说明四个列表组件都被加载了,不合理;加上 v-if="item.rendered" 后应只执行 1 次。之后切换 tab 不再重复执行 mounted,说明缓存生效。
易错点:只写 v-show 不写 v-if,首次加载全部组件;只写 v-if 不写 v-show,切换时组件反复重建。
封装接口、首次请求与按分类存数据
为什么需要:列表数据来自接口,且不同 tab 参数不同;分页时还要把新数据追加到对应分类的 list 中。
怎么想:按老套路先在 service 中封装请求方法,再到页面调用。请求参数至少包括 type、current、pageSize。页面加载时请求当前分类;切换 tab 时,只在 rendered 为 false 时请求,避免重复请求第一页。
怎么做:封装方法时按接口文档填路径,GET 参数通过 params 传递:
js
export const getFeedList = (params) => {
return request.get('接口文档中的路径', { params })
}页面请求:
js
const { data } = await getFeedList({
type: this.feedType,
current: this.feedCurrent,
pageSize: this.pageSize
})拿到 data.rows 后,按当前索引追加到对应列表:
js
const oldList = this.feedTabs.value[this.typeIndex].value.list
const newList = [...oldList, ...data.rows]
this.feedTabs.value[this.typeIndex].value.list = newList易错点:直接用 = 覆盖 list,分页数据越翻越少;重复切换 tab 时重复请求第一页;忘记从 data.rows 取列表;请求前未判断 rendered,导致缓存失效。
本节要做什么
首页列表的核心不是“把静态页面摆出来”,而是让不同 Tab 共用同一套列表布局,同时各自拥有独立的数据、页码和加载状态。为什么要这样做?因为推荐、关注、减脂、饮食的布局结构相同,只是请求参数和数据不同;如果每个 Tab 各写一套列表,后续分页和缓存会非常难维护。怎么想?把下方列表抽成 feed-list 组件,页面只负责传 list 数据;组件必须放在吸顶元素同级的位置。怎么做?先在 pages.json 对应页面加:
json
{
"navigationStyle": "custom"
}用来隐藏系统导航栏。然后新建 components/feed-list/feed-list.vue,页面中常规导入组件。注意:这种局部组件导入时不要习惯性加花括号;组件位置不要放错,否则吸顶会失效——它要和吸顶元素同级,让父盒子的高度被撑开。
数据结构:一个 Tab 一条记录
为什么需要专门设计数据结构?因为每个 Tab 都有自己的页码、列表数据、是否还有更多、是否已经首次渲染。转写稿里反复推导:饮食翻到第 5 页,切到减脂再切回来,继续滚动应从第 6 页开始;减脂到第 3 页,切回来应从第 4 页开始。所以不能只用一个全局页码,而要让每个 Tab 各存一份状态。怎么做?在页面数据中给每个 Tab 增加字段,示意如下:
js
{
label: '推荐',
type: 'recommand',
current: 1,
pageSize: 5,
hasMore: true,
list: [],
rendered: true // 默认第一个为 true,其余为 false
}type 是接口类型,必须按接口文档复制:推荐是 recommand,关注是 like,饮食是 food;减脂也以文档为准,不要自己猜。易错点:type 写错会导致参数传错;rendered 用来标记组件是否已经加载过,不能和“当前显示哪个 Tab”混为一谈。
根据索引取当前类型和页码
为什么需要计算属性?点击 Tab 时组件抛出的是索引值 index,而调接口需要的是当前 Tab 的 type 和 current。怎么想?索引变化后,从数组中取对应记录,这是“由已有数据派生新数据”,适合用 computed。怎么做:
js
computed: {
feedType() {
return this.feedTabs[this.typeIndex].type
},
feedCurrent() {
return this.feedTabs[this.typeIndex].current
}
}如果使用组合式 API 且 feedTabs、typeIndex 是 ref,必须写 .value:
js
feedTabs.value[typeIndex.value].type调试时先打印 index,点推荐、关注、减脂、饮食应分别得到 0、1、2、3。转写稿里报“访问 undefined 的属性”,原因就是 feedTabs 是 ref,却直接写了 feedTabs[typeIndex],漏了 .value。易错点:ref 数据取数组项前一定先 .value。
用 v-show + v-if 模拟组件缓存
为什么需要缓存?uni-app 中不能直接依赖 keep-alive 做组件缓存;如果每次切换都重新加载,之前的分页数据会丢失,性能也浪费。怎么想?用 rendered 控制“是否首次渲染”,用 typeIndex 控制“当前显示哪一个”。怎么做:页面中循环渲染四个 feed-list:
vue
<feed-list
v-for="(item, index) in feedTabs"
:key="item.type"
v-show="typeIndex === index"
v-if="item.rendered"
:list="item.list"
/>切换时把当前项标记为已渲染:
js
feedTabs.value[typeIndex.value].rendered = true调试方法:在组件 mounted 中打印日志。初始只应执行 1 次;首次切到新 Tab 再加 1 次;切回已渲染 Tab 不再执行。易错点:只用 v-show 会让四个组件都挂载;只用 v-if 会销毁重建,失去缓存效果。
请求缓存与数据追加
为什么请求也要缓存?切回已经请求过的 Tab 时,再请求第一页没有意义,还会覆盖已有数据。怎么想?rendered 为 false 时才请求;拿到 data.rows 后追加到对应 Tab 的 list。怎么做:
js
if (!feedTabs.value[typeIndex.value].rendered) {
getFeedList()
}追加数据不要覆盖:
js
const list = feedTabs.value[typeIndex.value].list
feedTabs.value[typeIndex.value].list = [...list, ...data.rows]易错点:不要写 list = data.rows,否则分页数据永远被覆盖;GET 请求参数要通过 params 传;接口返回的列表在 data.rows 里。复制代码后要检查变量名、索引和 .value,转写稿中的调试错误多来自这些细节。
先把系统导航栏藏起来
首页顶部已经用了自定义导航栏,如果系统的导航栏还在,就会出现“双导航”。所以第一步是在 pages.json 里给首页加配置:
json
{
"navigationStyle": "custom"
}为什么需要:自定义导航栏要自己控制高度和吸顶,系统导航栏会占位、会挤压布局。怎么想:这是页面级配置,写在首页对应项里,不是组件里。易错点:加完要刷新看效果;别加到别的页面去。
把列表抽成 feed-list 组件
推荐、关注、减脂、饮食四个 tab 下面的布局完全一样,只是数据不同,所以把这一整块抽成组件 feed-list.vue,放在首页目录的 components 下。
html
<feed-list :list="item.list" />组件里只声明一个 props:list,数据由外面传进来。
为什么需要:四个 tab 复用同一套结构,改一处即可。怎么想:局部组件只在某个页面用,按常规方式 import 即可,不必用 easycom 的约定。怎么做:import FeedList from './components/feed-list.vue'。易错点:默认导出不要加大括号 { },很多同学在这里写错;另外组件必须放在吸顶元素的同级位置——吸顶盒子要靠兄弟节点把父元素撑高,放错位置吸顶会失效,这是前面踩过的坑。
设计 tab 数据结构:一个 tab 一份状态
切 tab 时请求参数不同,每个 tab 还有自己的页码,所以不能只存 label。数据结构大致是:
js
const feedTabs = ref([
{ label: '推荐', page: 1, type: 'recommand', hasMore: true, list: [], rendered: true },
{ label: '关注', page: 1, type: 'like', hasMore: true, list: [], rendered: false },
// 减脂、饮食同理
])type:请求接口的类型,取值按接口文档抄,如推荐recommand、关注like,不要自己拼。page:当前页码,每个 tab 独立,饮食翻到第 5 页,切回来还能从第 6 页继续。hasMore:还有没有下一页。list:这个 tab 自己的列表数据。rendered:这个 tab 的组件是否已经渲染/请求过,默认只有第一个是true。
为什么需要:只靠一个页码变量,切 tab 就乱了。易错点:type 的值一定从接口文档复制,后端写错也跟着抄,保证能拿到数据。
用计算属性把“当前 tab”翻译成请求参数
点击 tab 组件会抛出索引 index,但请求要的是 type、current、pageSize,所以要算:
js
const bType = computed(() => feedTabs.value[tabIndex.value].type)
const feedCurrent = computed(() => feedTabs.value[tabIndex.value].page)
const pageSize = 5怎么想:由索引推出另一份数据,典型计算属性场景。易错点:feedTabs 和 tabIndex 都是 ref,取值必须带 .value:
js
feedTabs.value[tabIndex.value].type漏写 .value 会报“访问了 undefined 的属性”,因为 ref 对象本身没有数组下标。
用 v-show + v-if 模拟组件缓存
uni-app 里 keep-alive 不生效,得自己实现:v-show 控制显隐,v-if 控制是否首次渲染。
html
<feed-list
v-for="(item, index) in feedTabs"
:key="item.type"
v-show="tabIndex === index"
v-if="item.rendered"
:list="item.list"
/>为什么需要:如果每次切 tab 都重建组件,之前翻的几页数据全丢,还白白损耗性能。怎么想:rendered 从 false 变 true 只发生一次,组件只创建一次;之后只切 v-show,组件被保留下来。易错点:v-for 里别忘了取 index,也别把数组名写错(比如把 feedTabs 写成 feedList),这两处最容易复制粘贴出错。
封装接口并缓存请求
在 service 下新建 doctor.js:
js
import http from '@/utils/http'
export const feedListAPI = (params) => http.get('/feed/list', { params })GET 参数必须放在 params 里,不能直接传对象。
页面里请求前先判断:
js
if (!feedTabs.value[tabIndex.value].rendered) {
const { data } = await feedListAPI({
type: bType.value,
current: feedCurrent.value,
pageSize
})
// 存数据,见下一节
feedTabs.value[tabIndex.value].rendered = true
}为什么需要:来回切 tab 若重复请求第一页,既浪费又把已翻的页数冲掉。怎么想:组件没渲染过,说明没请求过;rendered 就是开关。易错点:rendered 要在请求成功后置 true,判断时别把 false/true 写反。
把数据分别塞进各自的 list
js
const list = feedTabs.value[tabIndex.value].list // 上一次的数据
const rows = data.rows // 新请求的数据
feedTabs.value[tabIndex.value].list = [...list, ...rows]为什么需要:分页是追加,不是覆盖。怎么想:用展开运算符合并新旧数组,长度才会越来越长。易错点:直接 = 赋值会把前面几页冲掉;接口真正的列表在 data.rows 里,别拿错层级。
代码示例
常见错误
1. 页面顶部仍然出现系统原生导航栏 现象:配置 navigationStyle: custom 后,顶部还是顶着一条系统导航。 原因:属性写在了错误的页面节点,或写成了 navigation-style / navigationStyle: "custom" 带引号位置错误,实际没进 pages 里该页面的配置对象。 修法:确认在 pages.json 中首页那一项对象内加 "navigationStyle": "custom",保存后重新编译(热更新有时不生效)。
2. 导入列表组件时页面直接报错或组件不渲染 现象:import { FeedList } from '@/components/feed-list.vue' 后模板里用不了。 原因:习惯性加了花括号,而该单文件组件是默认导出,没有具名导出。 修法:去掉花括号写成 import FeedList from '@/components/feed-list.vue'。
3. 加了列表组件后吸顶突然失效 现象:原本正常的吸顶导航在滚动时不再固定。 原因:列表组件被放到了吸顶盒子内部或更外层,吸顶元素的父级高度不再大于吸顶元素本身。 修法:把列表组件移到与吸顶元素同级(同一父级)的位置,让兄弟节点把父级撑高。
4. 计算属性里报 “Cannot read property of undefined / 读不到属性” 现象:切换 tab 时控制台报访问了 undefined 的属性,索引取不到数据。 原因:tabs 是 ref 包裹的数组,直接用 tabs.value[i] 之外的写法(如 tabs[i].type、tabIndex.value 漏 .value)导致拿到的是 ref 对象或 undefined。 修法:逐层补 .value:tabs.value[tabIndex.value].type;改完在 Vue Devtools 里确认取到的是字符串而非 ref。
5. 首次进入页面时四个 tab 的列表请求全部发出 现象:刷新页面,网络面板一次出现 4 条列表请求,onMounted 打印 4 次。 原因:v-for 渲染了 4 个组件,只用 v-show 控制显隐,组件依然全部创建并执行生命周期。 修法:外层同时加 v-if="item.rendered",只有被点击过(rendered 置 true)的 tab 才创建组件。
6. 来回切换 tab 时重复请求第一页,已加载数据被清空 现象:从推荐切到关注再切回推荐,又发一次 current=1 的请求,之前翻的页数白翻。 原因:切换回调里无条件调用取数方法;且列表赋值用了 list = res.rows 直接覆盖。 修法:请求前判断 if (!tabs.value[index].rendered) 取数;赋值改为 list = [...旧list, ...res.rows] 追加。
7. 分页数据渲染后只有最后一页的内容 现象:上拉加载第二页后,列表长度没变,只剩 5 条。 原因:用等号覆盖赋值,把上一次的数据冲掉了。 修法:用展开运算符合并新旧数组(或 push(...rows)),保证数组长度递增。
练习
- 给首页四个 tab 各自准备独立的分页状态(
current、hasMore、list、rendered),要求切回某个 tab 时页码接着上次继续,而不是回到第 1 页。 - 在
feed-list组件里用onReachBottom(或scroll-view的scrolltolower)实现上拉加载:每次current + 1,当hasMore为false时提示“没有更多了”并停止请求。 - 手动制造一次“访问 undefined 属性”的报错(例如故意漏写一个
.value),用 Devtools 定位并说明排查路径,然后修复。 - 把接口封装函数改成同时支持
type、current、pageSize三个参数,并在页面里打印每次请求实际发出的参数,验证切换 tab 时type确实不同。 - 思考题:如果不用
v-if + v-show组合,还有哪些方式能实现 uni-app 中的列表缓存?各有什么代价(内存、首屏、滚动位置保持)?
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 优医咨询-首页列表组件 | 07:43 |
| 2 | 优医咨询-列表组件交互的数据结构 | 08:11 |
| 3 | 优医问诊-列表表组件tab数据 | 12:56 |
| 4 | 优医咨询-列表组件缓存 | 17:10 |
| 5 | 优医咨询-获取列表数据 | 10:17 |
| 6 | 优医咨询-缓存列表数据请求 | 06:37 |
| 7 | 优医咨询-分别存储列表数据 | 06:46 |
小结
- uni-app 里
keep-alive对页面级组件不生效,缓存要靠v-if(只渲染一次)+v-show(只切显隐)配合rendered标记自己实现。 - tab 数据结构必须“一 tab 一份状态”:
type、current、hasMore、list、rendered缺一不可,否则分页和缓存都会串。 - 用索引找数据是典型的计算属性场景——依赖
tabIndex,派生type与current,不要在方法里重复算。 ref数据在 JS 中取值必须带.value,漏掉就是“访问 undefined 属性”这类报错的头号来源。- 请求时机要由
rendered把关:只有该 tab 首次被激活才发请求,重复切换只切显隐,不发请求也不丢数据。 - 列表赋值永远用合并(展开运算符 / push),不要用等号覆盖,否则分页加载等于白做。
- 组件摆放位置会影响布局:与吸顶元素保持同级,父级才有足够高度撑起 sticky 效果。
- 复制讲师的示例代码前先看清导出方式和
.value,讲义里本身可能埋着坑,验证要靠控制台和网络面板而不是“看起来对”。