第2章 2.5 综合实战:个人主页(多页面跳转)
🎯 开场:为什么你的个人主页需要"会跳转"?
你有没有这种感觉:看了一个很棒的个人主页,点来点去,发现每个板块都能跳转——点头像进个人资料,点作品进作品集,点设置进设置页。但自己写的时候,所有内容全堆在一个页面上,往下滑半天找不到重点。
这就是多页面跳转的价值:让信息有序组织,用户想看什么就点什么,而不是被海量信息埋没。
上一章我们学会了把数据存到本地(uni.setStorage),但存了数据还得能跳转去不同页面展示吧?这一章我们就把「存」和「跳」串起来,做一个真正的多页面个人主页。
学完本文,你将能够:
- 说出页面跳转的原理(类比:快递从A站发到B站)
- 写出带参数跳转的完整代码
- 独立做出一个 3-4 个页面的个人主页 demo
🧱 基础:页面跳转核心概念(25分钟)
什么是页面跳转?
生活类比:页面就像房间,A房间的人想去B房间,得先「开门」再「走过去」。在 uniapp 里,这个「开门走过去」的动作就是 uni.nav\n\n\n\n\n\nigateTo。
3种跳转方式对比
| 方式 | 适合场景 | 特点 |
|---|---|---|
navigateTo |
一般跳转 | 能返回,保留上一页 |
redirectTo |
替代当前页 | 关闭当前页,新页无法返回 |
reLaunch |
清空所有页 | 相当于重启,什么都没有了 |
说白了:
- navigateTo = 你去隔壁房间串门,随时能回来
- redirectTo = 你从这个门出去,把这门焊死
- reLaunch = 你直接把所有门都炸了,从头来过
核心代码:最简单的跳转
页面A(index.vue)
<template>
<view class="container">
<text class="title">我是主页</text>
<!-- 点击这个按钮,跳转到个人资料页 -->
<button @tap="goToProfile">查看个人资料</button>
</view>
</template>
<script>
export default {
data() {
return {
username: "小明"
}
},
methods: {
goToProfile() {
// 跳转到 profile 页面
uni.navigateTo({
url: '/pages/profile/profile?name=' + this.username
})
}
}
}
</script>
页面B(profile.vue)
<template>
<view class="container">
<text class="title">个人资料</text>
<text>用户名:{{ username }}</text>
<button @tap="goBack">返回主页</button>
</view>
</template>
<script>
export default {
data() {
return {
username: ""
}
},
onLoad(options) {
// 接收从主页传来的参数
this.username = options.name || "未填写"
},
methods: {
goBack() {
uni.navigateBack()
}
}
}
</script>
这 20 行代码实现了什么?
- 点击主页按钮 → 自动拼接 URL 参数 ?name=小明 → 跳转到 profile 页
- profile 页在 onLoad 生命周期里,读取 URL 参数,显示「用户名:小明」
- 点击返回 → navigateBack 回到主页
传参的两种方式
方式1:URL 参数拼接(上面用的)
uni.navigateTo({
url: '/pages/profile/profile?name=小明&age=18'
})
// 获取:onLoad(options) { console.log(options.name) }
方式2:本地存储传参(更灵活)
// A页面:存
uni.setStorage({
key: 'userInfo',
data: { name: '小明', age: 18 }
})
uni.navigateTo({ url: '/pages/profile/profile' })
// B页面:取
onLoad() {
const res = uni.getStorageSync('userInfo')
console.log(res.name) // 小明
}
什么时候用哪种?
- 参数简单(就一两个字符串)→ URL 拼接
- 参数复杂(对象、数组、或者数据量大)→ 本地存储
页面栈是什么?
你可以理解为浏览器记录你走过哪些房间的清单:
页面栈 = [首页, 个人资料页, 设置页]
↓
当前在设置页
navigateTo:往栈里加一页(栈长度 +1)navigateBack:弹出一页(栈长度 -1)redirectTo:替换当前页(栈长度不变)
reLaunch 最狠,直接清空栈,重新开始。
TabBar 页面跳转(特殊但常用)
如果你的个人主页底部有「首页/个人/我的」这种 Tab 栏,用 switchTab:
// 跳转到 tabBar 页面(不能用 navigateTo!!!)
uni.switchTab({
url: '/pages/index/index'
})
注意! tabBar 页面不能在 URL 里带参数,因为 switchTab 根本不支持传参。这时候就必须用本地存储:
// 跳转前存数据
uni.setStorage({ key: 'tabData', data: '要传的东西' })
uni.switchTab({ url: '/pages/index/index' })
// 目标 tabBar 页的 onLoad 里取
onLoad() {
const data = uni.getStorageSync('tabData')
}
🔥 实战:3个递进项目(35分钟)
项目1(5分钟):两个页面的互相跳转
目标:主页点击「关于我」跳转到关于页,关于页点「返回」回来。
项目结构:
pages/
├── index/
│ └── index.vue # 主页
└── about/
└── about.vue # 关于页
主页代码(index.vue)
<template>
<view class="container">
<text class="big-title">🏠 个人主页</text>
<view class="info-box">
<text>这里是主页内容</text>
</view>
<button type="primary" @tap="goAbout">关于我 →</button>
</view>
</template>
<script>
export default {
methods: {
goAbout() {
uni.navigateTo({ url: '/pages/about/about' })
}
}
}
</script>
<style>
.container { padding: 40rpx; }
.big-title { font-size: 48rpx; font-weight: bold; }
.info-box {
background: #f5f5f5;
padding: 30rpx;
margin: 30rpx 0;
border-radius: 16rpx;
}
</style>
关于页代码(about.vue)
<template>
<view class="container">
<text class="big-title">👤 关于我</text>
<view class="info-box">
<text>我是uniapp学习者,正在做个人主页练习。</text>
</view>
<button type="warn" @tap="goBack">← 返回主页</button>
</view>
</template>
<script>
export default {
methods: {
goBack() {
uni.navigateBack()
}
}
}
</script>
<style>
.container { padding: 40rpx; }
.big-title { font-size: 48rpx; font-weight: bold; }
.info-box {
background: #fff3e0;
padding: 30rpx;
margin: 30rpx 0;
border-radius: 16rpx;
}
</style>
预期输出:主页显示「🏠 个人主页」→ 点击按钮 → 显示「👤 关于我」→ 点击返回 → 回到主页。
一句话解释:主页用 navigateTo 跳过去,关于页用 navigateBack 弹回来,这就是最基础的「来"回"。
项目2(15分钟):带用户信息的个人主页(3页面)
目标:做一个完整的个人主页,包含:首页、个人资料页、作品集页。
页面关系:
首页 ←→ 个人资料(互跳)
↓
作品集
pages.json 配置(必须先配置!)
{
"pages": [
{ "path": "pages/index/index" },
{ "path": "pages/profile/profile" },
{ "path": "pages/works/works" }
],
"tabBar": {
"list": [
{ "text": "首页", "pagePath": "pages/index/index" },
{ "text": "资料", "pagePath": "pages/profile/profile" },
{ "text": "作品", "pagePath": "pages/works/works" }
]
}
}
首页(index.vue)
<template>
<view class="container">
<view class="header">
<image class="avatar" src="/static/avatar.png" mode="aspectFill"></image>
<text class="name">{{ userInfo.name }}</text>
<text class="bio">{{ userInfo.bio }}</text>
</view>
<view class="section">
<text class="section-title">📌 快捷入口</text>
<view class="btn-row">
<button size="mini" @tap="goProfile">编辑资料</button>
<button size="mini" @tap="goWorks">查看作品</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
userInfo: {
name: "小明",
bio: "uniapp学习者 · 正在成长"
}
}
},
onLoad() {
// 从本地存储读取最新资料
const info = uni.getStorageSync('userInfo')
if (info) {
this.userInfo = info
}
},
methods: {
goProfile() {
uni.switchTab({ url: '/pages/profile/profile' })
},
goWorks() {
uni.switchTab({ url: '/pages/works/works' })
}
}
}
</script>
<style>
.container { padding: 40rpx; min-height: 100vh; background: #fafafa; }
.header { text-align: center; padding: 60rpx 0; }
.avatar {
width: 160rpx; height: 160rpx;
border-radius: 80rpx; background: #ddd;
}
.name { display: block; font-size: 40rpx; font-weight: bold; margin: 20rpx 0 10rpx; }
.bio { display: block; color: #666; font-size: 28rpx; }
.section { margin-top: 40rpx; }
.section-title { font-size: 32rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
.btn-row { display: flex; gap: 20rpx; }
</style>
个人资料页(profile.vue)
<template>
<view class="container">
<text class="title">👤 个人资料</text>
<view class="form-item">
<text class="label">昵称</text>
<input v-model="formData.name" placeholder="请输入昵称" />
</view>
<view class="form-item">
<text class="label">简介</text>
<textarea v-model="formData.bio" placeholder="介绍一下自己" />
</view>
<view class="form-item">
<text class="label">城市</text>
<input v-model="formData.city" placeholder="你所在的城市" />
</view>
<button type="primary" @tap="saveInfo">保存</button>
<text class="hint">保存后首页会同步更新</text>
</view>
</template>
<script>
export default {
data() {
return {
formData: {
name: "",
bio: "",
city: ""
}
}
},
onLoad() {
// 读取已有数据
const info = uni.getStorageSync('userInfo')
if (info) {
this.formData = info
}
},
methods: {
saveInfo() {
uni.setStorage({
key: 'userInfo',
data: this.formData,
success: () => {
uni.showToast({ title: '保存成功!', icon: 'success' })
}
})
}
}
}
</script>
<style>
.container { padding: 40rpx; }
.title { font-size: 40rpx; font-weight: bold; display: block; margin-bottom: 40rpx; }
.form-item { margin-bottom: 40rpx; }
.label { display: block; font-size: 28rpx; color: #666; margin-bottom: 10rpx; }
input, textarea {
border: 1px solid #ddd;
padding: 20rpx;
border-radius: 8rpx;
font-size: 32rpx;
}
textarea { height: 160rpx; }
.hint { display: block; text-align: center; color: #999; font-size: 24rpx; margin-top: 20rpx; }
</style>
作品集页(works.vue)
<template>
<view class="container">
<text class="title">🎨 我的作品</text>
<view class="works-list">
<view v-for="(work, index) in works" :key="index" class="work-item">
<image :src="work.image" mode="aspectFill" class="work-image"></image>
<view class="work-info">
<text class="work-title">{{ work.title }}</text>
<text class="work-desc">{{ work.desc }}</text>
</view>
</view>
</view>
<view v-if="works.length === 0" class="empty">
<text>还没有作品,先去创作吧!</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
works: [
{ title: "待办清单App", desc: "用uniapp做的第一个完整应用", image: "/static/work1.png" },
{ title: "天气查询工具", desc: "调用免费API显示天气", image: "/static/work2.png" },
{ title: "个人主页", desc: "本章的综合实战项目", image: "/static/work3.png" }
]
}
}
}
</script>
<style>
.container { padding: 40rpx; }
.title { font-size: 40rpx; font-weight: bold; display: block; margin-bottom: 40rpx; }
.work-item {
display: flex;
gap: 20rpx;
margin-bottom: 30rpx;
padding: 20rpx;
background: #fff;
border-radius: 12rpx;
}
.work-image { width: 120rpx; height: 120rpx; border-radius: 8rpx; background: #eee; }
.work-info { flex: 1; }
.work-title { display: block; font-size: 32rpx; font-weight: bold; }
.work-desc { display: block; font-size: 26rpx; color: #666; margin-top: 8rpx; }
.empty { text-align: center; color: #999; padding: 100rpx 0; }
</style>
预期输出:
- 底部 Tab 栏:首页 / 资料 / 作品
- 首页显示头像和昵称
- 在资料页修改昵称保存 → 切到首页看到昵称已更新
- 作品页显示3个作品卡片
一句话解释:三个页面通过 Tab 栏串联,资料页修改数据后存到本地,首页读取显示——这就是「数据跟着页面跑」的感觉。
项目3(15分钟):给作品集加"收藏功能"
需求:在作品集页点击❤️收藏按钮,收藏状态存入本地,首页底部显示收藏数量。
改进 works.vue(加收藏功能)
<template>
<view class="container">
<text class="title">🎨 我的作品</text>
<view class="works-list">
<view v-for="(work, index) in works" :key="index" class="work-item">
<image :src="work.image" mode="aspectFill" class="work-image"></image>
<view class="work-info">
<text class="work-title">{{ work.title }}</text>
<text class="work-desc">{{ work.desc }}</text>
</view>
<!-- 收藏按钮 -->
<view class="action" @tap="toggleFavorite(index)">
<text :class="work.favorited ? 'heart active' : 'heart'">
{{ work.favorited ? '❤️' : '🤍' }}
</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
works: []
}
},
onLoad() {
// 读取作品列表
const savedWorks = uni.getStorageSync('myWorks')
if (savedWorks) {
this.works = savedWorks
} else {
// 默认作品
this.works = [
{ title: "待办清单App", desc: "用uniapp做的第一个完整应用", image: "/static/work1.png", favorited: false },
{ title: "天气查询工具", desc: "调用免费API显示天气", image: "/static/work2.png", favorited: false },
{ title: "个人主页", desc: "本章的综合实战项目", image: "/static/work3.png", favorited: false }
]
}
},
methods: {
toggleFavorite(index) {
// 反转收藏状态
this.works[index].favorited = !this.works[index].favorited
// 保存到本地
uni.setStorage({
key: 'myWorks',
data: this.works
})
// 通知首页更新
uni.$emit('worksUpdated', this.works)
}
}
}
</script>
改进 index.vue(显示收藏数量)
<template>
<view class="container">
<!-- 原有内容... -->
<view class="stats-bar">
<text>作品数:{{ worksCount }}</text>
<text>收藏数:{{ favoritesCount }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
worksCount: 0,
favoritesCount: 0
}
},
onLoad() {
this.loadData()
// 监听收藏变化
uni.$on('worksUpdated', (works) => {
this.worksCount = works.length
this.favoritesCount = works.filter(w => w.favorited).length
})
},
onShow() {
this.loadData()
},
methods: {
loadData() {
const works = uni.getStorageSync('myWorks') || []
this.worksCount = works.length
this.favoritesCount = works.filter(w => w.favorited).length
}
}
}
</script>
<style>
.stats-bar {
display: flex;
justify-content: space-around;
margin-top: 40rpx;
padding: 30rpx;
background: #fff;
border-radius: 12rpx;
}
.heart.active { color: #f00; }
</style>
预期输出:
- 作品卡片右侧有🤍按钮
- 点击后变成❤️
- 首页底部显示「收藏数:1」
- 退出再打开,收藏状态保留
一句话解释:作品数据存本地,状态变化实时同步到首页——这就是「本地存储 + 页面通信」的经典组合。
💪 进阶:常见坑 + 调试技巧(20分钟)
坑1:tabBar 页面用 navigateTo 跳转 ❌
// ❌ 错误:tabBar页面不支持navigateTo
uni.navigateTo({ url: '/pages/index/index' })
// ✅ 正确:tabBar页面只能用switchTab
uni.switchTab({ url: '/pages/index/index' })
原因:tabBar 是小程序/uniapp的底部导航,机制和普通页面不同,navigateTo 会被框架直接无视。
坑2:URL 参数带中文没 encode ❌
// ❌ 错误:中文参数会乱码
uni.navigateTo({ url: '/pages/profile/profile?name=小明' })
// ✅ 正确: encodeURIComponent 处理中文
uni.navigateTo({
url: '/pages/profile/profile?name=' + encodeURIComponent('小明')
})
// 接收时不用解码,框架自动处理
坑3:onLoad 和 onShow 混淆 ❌
// ❌ 错误:在 onShow 里读取onLoad设置的data(会拿不到)
onShow() {
console.log(this.username) // undefined!因为还没执行onLoad
}
// ✅ 正确:数据初始化放 onLoad
onLoad() {
this.username = '小明'
}
| 生命周期 | 什么时候调用 | 适合做什么 |
|---|---|---|
onLoad |
页面首次加载 | 获取参数、读取数据、初始化 |
onShow |
每次页面显示 | 刷新数据、监听变化 |
onReady |
页面渲染完成 | 操作DOM(uniapp里一般不用) |
坑4:navigateBack 不知道传 delta ❌
// 如果你用 redirectTo 跳转了2次,想一口气返回2页:
// ❌ 错误:navigateBack() 默认只返回1页
uni.navigateBack()
// ✅ 正确:指定 delta 参数
uni.navigateBack({ delta: 2 })
坑5:页面跳转前没等异步存储完成 ❌
// ❌ 错误:跳转和存储同时发生,可能跳转时数据还没存好
uni.setStorage({ key: 'info', data: 'xxx' })
uni.switchTab({ url: '/pages/index/index' })
// ✅ 正确:在 success 回调里跳转
uni.setStorage({
key: 'info',
data: 'xxx',
success: () => {
uni.switchTab({ url: '/pages/index/index' })
}
})
调试技巧:用 console.log 打印页面参数
onLoad(options) {
console.log('页面参数:', options)
console.log('name参数是:', options.name)
// 加上这行,你可以在HBuilderX控制台看到实际传了什么
}
✏️ 练习题 + 作业题(7分钟)
练习1(1分钟):修改跳转目标
- 输入:在 index.vue 里把
goAbout()改成跳转到/pages/profile/profile - 预期输出:点击按钮跳转到个人资料页
- 提示:改 url 路径即可
练习2(2分钟):加一个判断
- 输入:在项目1的 about.vue 里,加一个判断,如果用户名为空则显示「匿名用户」
- 预期输出:没传参数时显示「匿名用户」
- 提示:使用三元运算符
this.username || '匿名用户'
练习3(2分钟):给作品集加个「删除」功能
- 输入:在 works.vue 的每个作品卡片上加删除按钮
- 预期输出:点击删除后作品从列表消失
- 提示:用
splice方法删除数组元素,然后重新存本地
练习4(2分钟):统计访问次数
- 输入:在首页用本地存储记录用户访问首页的次数
- 预期输出:显示「你已经来过 X 次」
- 提示:用
getStorageSync读取后 +1,再用setStorage存回去
练习5(3分钟):分析报错
- 输入:运行以下代码,页面停留在A,按钮点击没反应
// A页面
uni.switchTab({ url: '/pages/profile/profile' })
// B页面(profile.json里配置了tabBar)
onLoad(options) {
console.log(options.name) // 想获取name但得不到
}
- 预期输出:说出哪里错了并修复
- 提示:switchTab 不支持 URL 参数,需要用什么方式传参?
作业:做一个「个人简历展示器」
需求描述:做一个包含4个页面的个人简历展示工具,页面包括:首页(封面)、教育经历、工作经历、技能特长。
功能点:
1. 首页点击「查看简历」跳转到教育经历页
2. 教育经历页点击「下一步」跳转到工作经历(URL参数传学校名)
3. 工作经历页点击「下一步」跳转到技能特长
4. 技能特长页点击「返回首页」用 reLaunch 回到首页
加分项:
1. 每个页面能编辑自己的信息,存到本地
2. 首页能展示所有页面填写的数据摘要
验收标准:
- 4个页面能正常跳转
- 数据编辑后能保存
- 再次打开能从本地读取之前填写的内容
📚 总结 + 资源(5分钟)
本章核心3点
- 页面跳转3兄弟:
navigateTo(能返回)、redirectTo(替代)、reLaunch(清空) - 传参两种方式:URL拼接适合简单数据,本地存储适合复杂数据
- tabBar跳转用switchTab:别用错API,不然页面根本没反应
延伸学习资源
- uniapp 官方文档 - 页面路由
- uniapp 官方文档 - 生命周期
- 《uniapp入门与项目实战》- 第3章(页面通信部分)
互动钩子
你在写个人主页的时候,最纠结的是哪个页面之间的跳转逻辑?是信息传不过去,还是返回的时候数据丢了?评论区聊聊,老粉优先回复!
下章预告:第三章我们要学一个新技能——怎么从网上"拿"数据(uni.request 封装)。学会了它,你的个人主页就能展示实时天气、新闻资讯,而不只是本地存的那点东西了。从「存数据」到「拿数据」,敬请期待!

评论(0)