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