Skip to content

tabbar配置与开发首页的业务(Day7 1/2) ​

本页对应 day7,共 6 段视频,约 1 小时 39 分。

学习目标 ​

  • 能够独立在 pages.json 中完成 3 个 tabBar 页面的路由、文字、默认图标与选中图标配置,并运行验证切换效果。
  • 能够使用 swiper、image 及 Flex 布局完成首页轮播图与学习分类区域的静态搭建,并正确设置尺寸、间距与居中样式。
  • 能够引入首页 banner 与分类数据,使用 ref、v-for、动态 src 和插值语法完成轮播图与学习分类的数据渲染。
  • 能够按照 uni-app 组件规范在 components 目录创建并直接使用组件,说明组件名与目录名一致时无需手动引入注册的规则。
  • 能够拆分“热门课程”卡片组件,使用 slot 预留内容区域,并完成课程列表静态结构、图片、标题、价格及学习人数的样式搭建。
  • 能够使用 ES6 class、Promise 和 uni.request 二次封装网络请求类,提供 get、post、delete 等方法,并在请求前后调用 uni.showLoading 与 uni.hideLoading 验证加载效果。

知识点 ​

本节范围与同音字纠正 ​

本节笔记标题是“一、tabbar配置!!!”,所以正文只聚焦 tabBar 配置及其前置的项目运行、页面创建、静态资源准备。转写稿后续的首页轮播、组件封装、网络请求二次封装属于后续内容,这里不展开,避免把教程写成大杂烩。转写稿里的“胚配置资/配置之点解释”应读作 pages.json,“喷空 bar/天空板/退棒”应读作 tabBar,“MPV信”应读作 mp-weixin,“NPM roundDV冒号MP”应读作 npm run dev:mp-weixin。本节笔记没有单独贴图片,故不虚构图片;代码按转写稿中的配置项整理。

项目初始化与运行:先拿到可调试的骨架 ​

为什么需要:tabBar 不是孤立配置,必须先有一个能运行到微信开发者工具的 uni-app 项目,否则改完 pages.json 也看不到效果。
怎么想:讲师采用 CLI 模板 + Vue3 + TS 的方式创建项目,安装依赖后运行到微信端;微信开发者工具导入的是编译产物,不是源码根目录。
怎么做:安装依赖后执行 npm run dev:mp-weixin,把生成的 dist/dev/mp-weixin 导入微信开发者工具;先看模板页面能否正常显示,再删掉模板中无用的配置。
易错点:不要导入项目根目录;要导入 dist/dev/mp-weixin。如果页面空白,先看终端是否编译成功,再看微信开发者工具控制台报错。

先配导航再写页面:为什么从 pages.json 开始 ​

为什么需要:页面标题、导航栏颜色、底部 tabBar 都属于应用级骨架,先配骨架,后面写页面时才知道每个页面在哪里、叫什么。
怎么想:转写稿先改 globalStyle 的标题和文字颜色,再改首页页面自身的标题。页面自身 style 有标题时覆盖全局,没有才用全局。
怎么做:在 pages.json 的 globalStyle 中设置 navigationBarTitleText: "硅谷学堂"、navigationBarTextStyle: "black"、navigationBarBackgroundColor: "#3cc";首页页面 style 中把标题改成“首页”。
易错点:globalStyle 不是每个页面都强制生效;页面 style 会覆盖全局。改完保存后要在微信开发者工具里刷新看效果。

tabBar 三页配置:从页面路由到 list ​

为什么需要:项目有三个主页面:首页、课程、个人中心,必须让用户能在底部直接切换。
怎么想:tabBar 至少 2 个、最多 5 个;每个 list 对象就是一个 tab 页面,pagePath 必须和 pages 中的 path 一致。
怎么做:先在 pages 中注册三个页面,再在 tabBar.list 中写三项;CLI 创建页面时不会自动补路由,需要手动补。核心结构如下,图标名按你 static 下实际文件替换:

json
{
  "pages": [
    { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页" } },
    { "path": "pages/course/index", "style": { "navigationBarTitleText": "课程" } },
    { "path": "pages/center/index", "style": { "navigationBarTitleText": "个人中心" } }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "硅谷学堂",
    "navigationBarBackgroundColor": "#3cc"
  },
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "/static/images/home.png",
        "selectedIconPath": "/static/images/home-active.png"
      },
      {
        "pagePath": "pages/course/index",
        "text": "课程",
        "iconPath": "/static/images/course.png",
        "selectedIconPath": "/static/images/course-active.png"
      },
      {
        "pagePath": "pages/center/index",
        "text": "个人中心",
        "iconPath": "/static/images/center.png",
        "selectedIconPath": "/static/images/center-active.png"
      }
    ]
  }
}

易错点:pagePath 不要带开头斜杠;pages 里的 path 也不要带开头斜杠;tabBar 页面必须先在 pages 注册;list 数量不能少于 2 个、多于 5 个。

图标与静态资源:路径是高频错点 ​

为什么需要:tabBar 只有文字不够,图标能区分普通态和选中态。
怎么想:转写稿把已写好的静态资源拷到项目 static 下,再在 iconPath、selectedIconPath 中分别指向普通图和高亮图。
怎么做:先确认图片真实存在于 static/images/,再写路径;保存后刷新,看底部是否出现三个 tab。
易错点:图标路径写错、文件名大小写不一致、普通图和高亮图写反,都会导致 tabBar 不显示或选中态异常。路径以项目实际文件为准,不要照抄示例名。

调试与验收:刷新后看什么 ​

为什么需要:配置类代码没有明显运行日志,必须通过界面验收。
怎么想:先看导航标题和颜色是否变化,再看底部 tabBar 是否出现,最后点三个 tab 看能否切换。
怎么做:保存 pages.json 后刷新微信开发者工具;若 tabBar 不出现,先检查 JSON 语法和 list 数量,再检查 pagePath 与页面是否一致,最后检查图标是否存在。
易错点:不要只改源码不重新编译;不要忽略控制台的 JSON 解析错误。转写稿中“刷新看一下,不就有了”对应的就是这一步验收。

先搭静态:tabBar 与首页骨架 ​

为什么需要:首页业务一开始不要急着接接口,先把静态结构搭出来,才能快速对齐布局、组件拆分和后续数据渲染的位置。转写稿里反复强调“先搭建静态”。

怎么想:先配好三个 tabBar 页面:首页、课程、个人中心;再处理首页轮播和学习分类。页面根节点要与设备同宽高,这类样式应放全局,而不是每个页面重复写。

怎么做:在 pages.json 中配置 tabBar,并给三个页面补路由;在 App.vue 写全局页面宽高样式。tabBar 配置思路如下:

json
{
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/images/home.png",
        "selectedIconPath": "static/images/home-active.png"
      },
      {
        "pagePath": "pages/course/course",
        "text": "课程",
        "iconPath": "static/images/course.png",
        "selectedIconPath": "static/images/course-active.png"
      },
      {
        "pagePath": "pages/center/center",
        "text": "个人中心",
        "iconPath": "static/images/center.png",
        "selectedIconPath": "static/images/center-active.png"
      }
    ]
  }
}
css
/* App.vue 中按转写稿思路写全局样式 */
page {
  width: 100%;
  height: 100%;
}

易错点:tabBar 的 pagePath 必须和页面路由一致;图标路径要写对;tabBar 页面数量至少 2 个、最多 5 个;页面样式不要都堆到全局,页面独有样式放在页面内。

2.1 uniapp 中组件的使用 ​

为什么需要:首页里“热门课程”等区域结构相似,如果不拆组件,模板会重复且难维护。

怎么想:先复习 Vue2 和 Vue3 的差异。Vue2 使用组件是“引入、注册、使用”;Vue3 是“引入、使用”,不再需要注册。uni-app 更进一步:组件放在 components 目录下,并符合目录名与组件名一致的规范时,可以直接在页面中使用,不需要引入注册。

怎么做:例如目录 components/card/card.vue,页面中可直接写:

vue
<template>
  <view>
    <card />
  </view>
</template>

易错点:组件目录名和组件文件名要一致;不是随便放一个目录都能自动使用;页面有路由,组件没有路由,这一点和微信小程序一致。

首页轮播与学习分类静态 ​

为什么需要:首页顶部轮播和学习方向分类是首页最直观的结构,先搭出来才能继续渲染数据。

怎么想:轮播用 swiper、swiper-item、image;分类用 flex 布局,一个大盒子包含 5 个 item,每个 item 内是图片和文字。

怎么做:

vue
<template>
  <view class="home">
    <swiper class="swiper" indicator-dots autoplay circular>
      <swiper-item v-for="(item, index) in bannerList" :key="index">
        <image class="banner-img" :src="item.src" />
      </swiper-item>
    </swiper>

    <view class="study">
      <view
        class="item"
        v-for="(item, index) in categoryList"
        :key="index"
      >
        <image class="item-img" :src="item.src" />
        <text class="title">{{ item.name }}</text>
      </view>
    </view>
  </view>
</template>
css
.swiper {
  width: 100%;
  height: 300rpx;
}
.banner-img {
  width: 100%;
  height: 100%;
}
.study {
  display: flex;
  margin-top: 20rpx;
}
.study .item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.item-img {
  width: 72rpx;
  height: 72rpx;
}

易错点:图片地址要用动态绑定 :src,不能写成普通属性;分类图片和文字要居中,需要给 item 开 flex;v-for 没有唯一 id 时可用 index 作为 key。

首页假数据渲染 ​

为什么需要:首页轮播和分类没有接口,转写稿说“自己造一个假接口”,所以要先准备 mock 数据,才能练数据渲染。

怎么想:在 src 下准备 mock 文件夹,用 TS 文件分别暴露轮播数据和分类数据;页面中用 ref 接收,模板中 v-for 渲染。

怎么做:

ts
import { ref } from 'vue'
import { bannerList as banners, categoryList as categories } from '@/mock/index'

const bannerList = ref(banners)
const categoryList = ref(categories)

易错点:Vue3 模板中使用的数据尽量用 ref 或 reactive;插值语法只能放在内容区,不能直接写在属性里;Vue3 中 v-if 优先级高于 v-for,不要在同一元素上同时使用。

热门课程组件拆分与插槽 ​

为什么需要:热门课程和区域顶部结构相似,但底部内容不同,直接复制会重复。转写稿用“插槽”解决:顶部标题固定,底部留坑。

怎么想:把整体卡片封装成 card 组件,顶部写“热门课程 / 全部课程”,底部放 <slot />;课程项再拆成 course-item 组件填入插槽。

怎么做:

vue
<!-- components/card/card.vue -->
<template>
  <view class="card">
    <view class="header">
      <text>热门课程</text>
      <text>全部课程 ></text>
    </view>
    <slot />
  </view>
</template>
vue
<!-- 首页中使用 -->
<card>
  <course-item v-for="(item, index) in courseList" :key="index" />
</card>

易错点:插槽是组件通信方式之一,不要把上下不同结构硬塞进同一个组件;课程标题过长时要处理单行省略:不换行、溢出隐藏、显示省略号。

网络请求二次封装:ES6 类 + Promise ​

为什么需要:真实接口请求要统一 baseURL、统一 loading、统一简化 res.data,否则每个页面都写一遍 uni.request 很重复。

怎么想:用 ES6 类封装 api 方法,内部返回 Promise;get、post、delete 再调用 api,页面只关心成功和失败。请求前 uni.showLoading,无论成功失败都在 complete 中 uni.hideLoading。

怎么做:

ts
class Service {
  api(options: any) {
    return new Promise((resolve, reject) => {
      uni.showLoading({ title: '加载中' })
      uni.request({
        url: '基础地址' + options.url,
        method: options.method || 'GET',
        data: options.data || {},
        success: (res) => resolve(res.data),
        fail: (err) => reject(err),
        complete: () => uni.hideLoading()
      })
    })
  }

  get(options: any) {
    return this.api({ ...options, method: 'GET' })
  }

  post(options: any) {
    return this.api({ ...options, method: 'POST' })
  }

  delete(options: any) {
    return this.api({ ...options, method: 'DELETE' })
  }
}

易错点:uni.request 的地址要是绝对地址,所以封装时要拼基础地址;hideLoading 要放 complete,不能只放成功回调;页面拿数据仍然要 await 或 .then,因为封装返回的是 Promise;TS 类型如果没报红,不必强行补复杂类型。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. pages.json —— tabBar 配置 ​

json
{
  "pages": [
    {
      "path": "pages/index/index",
      "style": { "navigationBarTitleText": "首页" }
    },
    {
      "path": "pages/course/course",
      "style": { "navigationBarTitleText": "课程" }
    },
    {
      "path": "pages/center/center",
      "style": { "navigationBarTitleText": "个人中心" }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "硅谷学堂",
    "navigationBarBackgroundColor": "#3CCCCC"
  },
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/images/home.png",
        "selectedIconPath": "static/images/home-active.png"
      },
      {
        "pagePath": "pages/course/course",
        "text": "课程",
        "iconPath": "static/images/list.png",
        "selectedIconPath": "static/images/list-active.png"
      },
      {
        "pagePath": "pages/center/center",
        "text": "个人中心",
        "iconPath": "static/images/my.png",
        "selectedIconPath": "static/images/my-active.png"
      }
    ]
  }
}

要点:

  • tabBar.list 每一项对应一个 tab 页,最少 2 个、最多 5 个,本项目是 3 个。
  • pagePath 必须与 pages 数组里的 path 完全一致,多写或少写一个字符都不显示。
  • iconPath / selectedIconPath 写的是相对项目根目录的路径,图标放到 static/images/ 下(官方建议 81×81px、大小不超过 40KB)。
  • 页面自己 style 里写了标题就用页面的,没写才用 globalStyle 的全局标题,但 navigationBarBackgroundColor 是全局生效的。
  • 新建页面时 uni-app 不会像微信开发者工具那样自动补路由,必须手动在 pages 里登记,否则跳转 404。

2. 首页:轮播 + 学习分类 + 卡片组件(插槽) ​

vue
<!-- components/card/card.vue -->
<template>
  <view class="card">
    <view class="header">
      <text class="left">{{ title }}</text>
      <text class="right">全部课程 ></text>
    </view>
    <slot></slot>
  </view>
</template>

<script setup lang="ts">
defineProps<{ title: string }>()
</script>

<style scoped>
.card {
  width: 100%;
  padding: 20rpx;
  box-sizing: border-box;
}
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 80rpx;
  padding: 0 20rpx;
  background-color: #3cc;
  border-radius: 12rpx;
  box-sizing: border-box;
}
.header .left {
  font-size: 30rpx;
  color: #fff;
}
.header .right {
  font-size: 24rpx;
  color: #fff;
}
</style>

<!-- components/view-item/view-item.vue -->
<template>
  <view class="item">
    <image class="image" src="/static/images/course.png" mode="widthFix" />
    <view class="title">HTML5 前端</view>
    <view class="footer">
      <text class="price">¥1516</text>
      <text class="buy">已有999人购买</text>
    </view>
  </view>
</template>

<style scoped>
.item {
  width: 350rpx;
  margin: 20rpx 0;
  background-color: #fff;
}
.item .image {
  width: 100%;
}
.item .title {
  font-size: 34rpx;
  font-weight: 900;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.item .footer {
  display: flex;
  justify-content: space-between;
}
.item .footer .price {
  color: red;
}
.item .footer .buy {
  font-size: 24rpx;
}
</style>

<!-- pages/index/index.vue -->
<template>
  <view class="container">
    <!-- 轮播图 -->
    <swiper class="swiper" indicator-dots autoplay circular>
      <swiper-item v-for="(item, index) in bannerList" :key="index">
        <image :src="item.src" />
      </swiper-item>
    </swiper>

    <!-- 学习分类 -->
    <view class="study">
      <view class="item" v-for="(item, index) in categoryList" :key="index">
        <image class="icon" :src="item.src" />
        <text class="title">{{ item.name }}</text>
      </view>
    </view>

    <!-- 卡片组件 + 插槽填内容 -->
    <card title="热门课程">
      <view class="box">
        <view-item v-for="i in 4" :key="i" />
      </view>
    </card>
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { bannerList as bannerData, categoryList as categoryData } from '@/mock/index'

const bannerList = ref(bannerData)
const categoryList = ref(categoryData)
</script>

<style scoped>
.swiper {
  width: 100%;
  height: 300rpx;
}
.swiper image {
  width: 100%;
  height: 100%;
}
.study {
  display: flex;
  margin: 20rpx 0;
}
.study .item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.study .item .icon {
  width: 72rpx;
  height: 72rpx;
}
.study .item .title {
  color: hotpink;
}
.box {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
}
</style>

要点:

  • 全局样式要写在 App.vue 里,用 page { width: 100%; height: 100%; } 让页面和设备一边宽一边高;<style scoped> 只作用于当前页面,不会污染别的页面。
  • easycom 规范:组件放在 components/组件名/组件名.vue,且文件夹名和文件名必须一致,页面里就能直接写 <card> / <view-item>,不需要 import 和注册。对比:vue2 要“引入→注册→使用”,vue3 只需“引入→使用”,uni-app 符合 easycom 规范时连引入都省了。
  • swiper 必须显式给高度(这里 300rpx),否则默认高度撑不开、看不见图;image 也要设宽高或 mode="widthFix"。
  • 差值语法 {{ }} 只能写在标签内容里,属性要写动态绑定 :src="item.src",写成 src="{{ }}" 是错的。
  • 数据用 ref 包一层再渲染(vue3 组合式 API);v-for 必须加 :key,这份 mock 数据没有 id,用 index 兜底。
  • vue3 中 v-for 优先级高于 v-if,别把两者写在同一元素上。
  • 列表用 flex: 1 让 5 个分类均分父容器,比写死 width: 20% 更稳;卡片里两列用 flex-wrap: wrap + justify-content: space-between 换行排布。
  • 顶部标题结构一样、底部内容不一样,所以把公共部分(header)封装成 card 组件并留 <slot> 当坑,课程区、区用同一组件往坑里填不同结构——插槽也是组件通信手段之一(props / 自定义事件 / ref / 插槽 / provide-inject)。

3. 网络请求二次封装(ES6 类 + Promise) ​

ts
// src/service/index.ts
const BASE_URL = 'https://api.example.com' // 只写服务器地址,业务接口只传后半段

interface RequestOptions {
  url: string
  method?: string
  data?: Record<string, any>
}

class Service {
  // 发请求的基础方法:结合 Promise 封装
  request(options: RequestOptions) {
    return new Promise((resolve, reject) => {
      uni.showLoading({ title: '加载中...' }) // 发请求前开启加载
      uni.request({
        url: BASE_URL + options.url,          // 基础路径 + 业务路径
        method: (options.method || 'GET') as any,
        data: options.data || {},             // 不带参数就给空对象
        success: (res) => {
          resolve(res.data)                   // 简化数据,页面直接拿 data
        },
        fail: (err) => {
          reject(err)                         // 失败返回失败的 promise
        },
        complete: () => {
          uni.hideLoading()                   // 不管成功失败,加载效果都要消失
        }
      })
    })
  }

  get(options: RequestOptions) {
    return this.request({ ...options, method: 'GET' })
  }

  post(options: RequestOptions) {
    return this.request({ ...options, method: 'POST' })
  }

  delete(options: RequestOptions) {
    return this.request({ ...options, method: 'DELETE' })
  }
}

export default new Service() // 导出类的实例,页面直接用 api.get(...)

要点:

  • 用 class 封装,get/post/delete 挂在类的原型对象上,实例化后才能调用;export default new Service() 直接导出实例,页面里 import api from '@/service' 后 api.get({ url: '/api/edu/index' }) 即可。
  • 必须用 Promise 包一层:uni.request 是回调式的,不包 Promise 页面拿不到返回值,也没法 .then / await。
  • BASE_URL 只在封装里拼一次,业务侧只传后半段路径,避免每次写绝对地址。
  • uni.hideLoading() 一定要放在 complete 里,不能分别写在 success 和 fail 里,否则容易漏关导致加载框一直转。
  • method 默认 GET,data 默认 {},这样调用方只传 url 也能跑。
  • uni.request 在小程序端要求绝对地址,所以 BASE_URL 不能省;换成 uni 的 API 时注意把 wx.showLoading / wx.request 全部替换成 uni. 前缀。
  • success 里先 resolve(res.data) 做一层数据简化,页面就不用每次写 res.data 了。

常见错误 ​

1. tabbar 只显示文字、图标空白或整条不出现

  • 现象:底部导航渲染出来了,但图标是空白方块;或干脆一条都不显示。
  • 原因:iconPath / selectedIconPath 写成了相对路径或漏了开头的 /;list 少于 2 项或多于 5 项;页面没有先在 pages 数组里注册。
  • 修法:图标统一写 /static/... 从项目根开始;确认 list 长度在 2–5 之间;三个 tab 页面必须先出现在 pages 里,再写进 tabBar.list。

2. 改了全局标题,某个页面却没变化

  • 现象:globalStyle 里改了 navigationBarTitleText,但首页标题还是旧的。
  • 原因:该页面的 style 里自己写了 navigationBarTitleText,页面级配置优先级高于全局。
  • 修法:要么改页面级那一条,要么删掉页面级覆盖,让它继承全局。

3. 组件写好了,页面里报“组件未注册”

  • 现象:<Card /> 用了但控制台提示找不到组件。
  • 原因:文件没放在 src/components 下;或目录名与文件名不一致(如 card/card-item.vue);或手动引入时路径、大小写写错。
  • 修法:遵循 components/xxx/xxx.vue 同名约定走自动引入;否则老老实实 import + 注册。命名对不上时,先怀疑单词拼写。

4. 图片全部不显示,但数据确实有

  • 现象:轮播和分类区域一片空白,console.log 数据正常。
  • 原因:模板里写成了 src="{ item.src }" 字符串插值,而不是动态绑定。
  • 修法:改成 :src="item.src"。属性位置永远用 : 绑定,{{ }} 只能出现在内容区。

5. v-for 渲染后顺序错乱或控制台警告

  • 现象:列表能出来,但更新数据后错位,控制台提示 key 相关警告。
  • 原因:没写 :key,或用了 index 当 key 而数据会增删。
  • 修法:优先用数据里的唯一 id;确实没有 id 再用 index,但要在数据不会重排的前提下。

6. 请求封装后页面拿不到数据,返回 undefined

  • 现象:const res = await api.get(...),res 是 undefined,页面渲染不出来。
  • 原因:new Promise 里写了 resolve 但外层方法没 return;或 get/post 里没 return this.request(...);或调用时把方法解构出来单独执行,this 丢了。
  • 修法:每一层都补 return;调用一律走实例(api.get(...)),不要 const { get } = api。

7. loading 转圈不消失

  • 现象:请求失败后加载提示一直挂着。
  • 原因:hideLoading 只写在成功回调里,失败分支没处理。
  • 修法:收尾统一放 complete / finally,无论成功失败都关。

8. 一个页面的样式影响了别的页面

  • 现象:改了首页样式,课程页跟着变形。
  • 原因:样式写进了 App.vue 的全局样式,或页面 <style> 忘了加 scoped。
  • 修法:全局样式只保留 page 撑满一屏这类基础规则,其余一律 scoped。

练习 ​

练习 1|tabbar 扩展 给现有三个 tab 增加第四个页面「我的课程」。写出完整改动清单:pages 注册、tabBar.list 新增项、图标资源放置位置、页面标题在哪一级配置。做完后自检:切换四个 tab 时高亮图标是否都正确。

练习 2|从假数据到真接口 把首页轮播图从 mock 的 bannerList 换成接口数据。要求列出改动点:数据用 ref 声明、在哪个生命周期发请求、请求前后的 loading 处理、失败时的兜底展示。写完后对比 mock 版本,确认首屏不闪白。

练习 3|组件与插槽复用 把「热门课程」和「名师」两块抽成同一个 Card 组件,顶部标题通过 props 传入,下半部分通过插槽填入。完成后回答:如果两块连标题栏样式都不同,这个组件还该不该抽?

练习 4|请求类补全 在现有 request 类上补齐 get / post / delete 三个方法,要求:统一拼接 baseURL、参数可传可不传、成功 resolve 业务数据、失败 reject 错误、loading 必关。再用一个页面调用验证,打印返回值和错误分支各一次。

练习 5|找错题 下面这段代码有 4 处问题,逐一指出并改正:

vue
<view v-for="item in list">
  <image src="{ item.src }"></image>
</view>
js
class Api {
  request(url) { new Promise((resolve) => { resolve(1) }) }
  get(url) { this.request(url) }
}

原视频对照 ​

#视频时长
1uniapp项目tabbar的配置19:06
2首页轮播与学习分类静态搭建11:49
3首页学习分类的数据的渲染09:45
4uniapp中组件在页面中使用08:26
5热门课程静态的搭建-组件的拆分24:09
6网络请求二次封装-es6类26:02

小结 ​

  1. 配置先行:pages 注册 → tabBar → 全局/页面标题,这条链路不通,后面写多少页面都白搭。开新项目先让 tab 能点通,再动业务。
  2. 静态先行:页面结构、布局、样式全部用假数据搭完再对接接口,能把「样式问题」和「数据问题」彻底分开排查。
  3. 组件化的判据是结构,不是内容:外壳一致、内部不同,就该抽组件 + 留插槽;外壳都不一样,硬抽只会得到一堆 props 开关。
  4. 约定优于配置:components/xxx/xxx.vue 同名自动引入是 uni-app 的硬约定,命名不一致不是"写法不同",是"不能用"。这类问题排查顺序永远是:路径 → 文件名 → 大小写。
  5. 请求层封装一次,收益贯穿全项目:baseURL 只写一次、loading 只处理一次、错误只兜一次。用 ES6 类还是函数都行,关键是每层都 return。
  6. 样式默认 scoped:App.vue 里只放全局基础样式,页面和组件样式一律 scoped,能省掉大量"改 A 坏 B"的排查时间。
  7. 模板语法只有一条铁律:内容区用 {{ }},属性区用 :。图片、链接、类名全部走绑定。
  8. 报错先看三样:控制台第一条红字、网络面板的请求、元素面板的实际 DOM。绝大多数问题在这三处已经写明了答案。