Skip to content

首页列表与云开发环境(一) ​

本页对应 首页列表与云开发环境,共 7 段视频,约 1 小时 9 分。

学习目标 ​

  1. 能够将首页列表区域抽取为 feed-list 组件,在首页正确引入,并将其放置在与吸顶元素同级的容器中,验证列表正常渲染且吸顶效果不失效。
  2. 能够为首页四个 tab 设计并补全包含 type、current、pageSize、list、hasMore、rendered 的数据结构,并说明各字段在列表请求、分页和缓存中的作用。
  3. 能够基于 tab 点击返回的索引,使用计算属性获取当前 type、current 页码和统一 pageSize,验证切换不同 tab 时取值随索引正确变化。
  4. 能够封装 feedListAPI,以 GET 方式通过 params 传递 type、current、pageSize,并在页面加载和 tab 切换时按条件发起请求,检查请求参数与返回的 rows 数据正确。
  5. 能够利用 rendered 配合 v-show 与 v-if 控制列表组件的首次加载和缓存,验证初始仅当前 tab 组件加载、切换后组件不重复加载。
  6. 能够根据 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)),保证数组长度递增。

练习 ​

  1. 给首页四个 tab 各自准备独立的分页状态(current、hasMore、list、rendered),要求切回某个 tab 时页码接着上次继续,而不是回到第 1 页。
  2. 在 feed-list 组件里用 onReachBottom(或 scroll-view 的 scrolltolower)实现上拉加载:每次 current + 1,当 hasMore 为 false 时提示“没有更多了”并停止请求。
  3. 手动制造一次“访问 undefined 属性”的报错(例如故意漏写一个 .value),用 Devtools 定位并说明排查路径,然后修复。
  4. 把接口封装函数改成同时支持 type、current、pageSize 三个参数,并在页面里打印每次请求实际发出的参数,验证切换 tab 时 type 确实不同。
  5. 思考题:如果不用 v-if + v-show 组合,还有哪些方式能实现 uni-app 中的列表缓存?各有什么代价(内存、首屏、滚动位置保持)?

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1优医咨询-首页列表组件07:43
2优医咨询-列表组件交互的数据结构08:11
3优医问诊-列表表组件tab数据12:56
4优医咨询-列表组件缓存17:10
5优医咨询-获取列表数据10:17
6优医咨询-缓存列表数据请求06:37
7优医咨询-分别存储列表数据06:46

小结 ​

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