一、 原生跳转方式 vs uni-app 统一映射
先看全景对照表,建立直觉:
| 原生概念 | iOS | Android | 鸿蒙 | uni-app 对应 API | 说明 |
|---|---|---|---|---|---|
| 普通跳转(入栈) | pushViewController |
startActivity / Fragment push |
router.pushUrl |
uni.navigateTo() |
保留当前页,可返回 |
| 重定向(替换) | setViewControllers |
startActivity + FLAG_CLEAR_TOP |
router.replaceUrl |
uni.redirectTo() |
关闭当前页,打开新页 |
| Tab 切换 | UITabBarController |
BottomNavigationView + Fragment |
Tabs |
uni.switchTab() |
只能跳 tabBar 页面 |
| 重启到首页 | popToRootViewController |
FLAG_ACTIVITY_CLEAR_TASK |
router.clear() |
uni.reLaunch() |
清空所有页面栈 |
| 返回上一页 | popViewController |
finish() / onBackPressed |
router.back() |
uni.navigateBack() |
支持 delta 回退多层 |
| 模态弹出 | presentViewController |
DialogFragment / BottomSheet |
CustomDialog |
❌ 无直接对应 | 用组件模拟(见下文) |
| 预加载 | prefetchViewController |
Preloading |
preloader |
uni.preloadPage() |
App/H5 端支持 |
⚠️ 核心认知: uni-app 的路由是基于页面栈 的,最大深度为 10 层。这与小程序的限制一致,App 端也遵守此约束。
二、 uni-app 五大路由 API 详解
2.1 uni.navigateTo() --- 最常用
javascript
uni.navigateTo({
url: '/pages/detail/detail?id=123&name=test',
animationType: 'slide-in-right', // ← 跳转动画(App端生效)
animationDuration: 300, // ← 动画时长 ms
events: { // ← 页面间通信(监听目标页 emit)
onDetailLoaded: (data) => {
console.log('详情页加载完成', data);
}
},
success: (res) => {
// res.eventChannel 可用于向目标页传数据
res.eventChannel.emit('initData', { id: 123 });
}
});
原生对照:
- iOS:
pushViewController:animated: - Android:
startActivity+ slide 动画 - 鸿蒙:
router.pushUrl({ url }, RouterMode.Standard)
2.2 uni.redirectTo() --- 替换当前页
css
uni.redirectTo({
url: '/pages/login/login'
});
💡 使用场景: 登录成功后跳转到主页、表单提交成功后跳转到结果页。当前页不会保留在栈中,用户点返回不会回到这个页面。
2.3 uni.switchTab() --- TabBar 专属
arduino
uni.switchTab({
url: '/pages/index/index' // ← 注意:不能带参数!
});
⚠️ 限制: URL 不能携带查询参数。如需传参,使用全局状态(Pinia/Vuex)、
eventChannel或uni.$emit/uni.$on。
2.4 uni.reLaunch() --- 清空栈重启
css
uni.reLaunch({
url: '/pages/index/index'
});
💡 使用场景: 退出登录、Token 过期强制重新登录、支付完成后回到首页。
2.5 uni.navigateBack() --- 返回
php
// 返回上一级
uni.navigateBack();
// 返回上三级
uni.navigateBack({ delta: 3 });
// 带自定义动画(App端)
uni.navigateBack({
animationType: 'slide-out-right',
animationDuration: 300
});
三、 不同设备上的跳转差异与适配
这是你最关心的部分。虽然 API 统一了,但各端的实际表现确实不同。
3.1 跳转动画差异
| 平台 | navigateTo 默认动画 | navigateBack 默认动画 | 可自定义 |
|---|---|---|---|
| iOS | 右侧滑入 | 右侧滑出 | ✅ animationType |
| Android | 底部弹起/淡入 | 淡出 | ✅ animationType |
| 鸿蒙 | 右侧滑入 | 右侧滑出 | ✅ animationType |
| 微信小程序 | 右侧滑入 | 右侧滑出 | ❌ 系统固定 |
| H5 | 无动画 | 无动画 | ⚠️ 需自行实现 CSS 过渡 |
App 端可用的 animationType:
arduino
// 进入动画
'slide-in-right' // 从右滑入(最常用)
'slide-in-left' // 从左滑入
'slide-in-top' // 从上滑入
'slide-in-bottom' // 从下滑入
'fade-in' // 淡入
'zoom-in' // 缩放进入
'none' // 无动画
// 退出动画(navigateBack 时使用)
'slide-out-right' // 向右滑出
'slide-out-left' // 向左滑出
'slide-out-top' // 向上滑出
'slide-out-bottom' // 向下滑出
'fade-out' // 淡出
'zoom-out' // 缩放退出
3.2 页面间传参方式的跨端差异
| 传参方式 | App | H5 | 微信小程序 | 推荐度 |
|---|---|---|---|---|
URL Query (?id=1) |
✅ | ✅ | ✅ 有长度限制 | ⭐⭐⭐ 简单数据 |
| eventChannel | ✅ | ✅ | ✅ | ⭐⭐⭐⭐⭐ 复杂对象 |
| Pinia/Vuex | ✅ | ✅ | ✅ | ⭐⭐⭐⭐ 全局状态 |
| uni. emit/on | ✅ | ✅ | ✅ | ⭐⭐⭐ 事件通知 |
| globalData | ✅ | ⚠️ | ✅ | ⭐⭐ 不推荐 |
eventChannel 最佳实践(推荐):
php
// 源页面:跳转时传复杂对象
uni.navigateTo({
url: '/pages/detail/detail',
success: (res) => {
// 向目标页发送数据
res.eventChannel.emit('passData', {
product: { id: 1, name: 'iPhone', specs: [...] },
fromPage: 'home'
});
}
});
// 目标页:接收数据
onLoad(() => {
const eventChannel = getOpenerEventChannel();
eventChannel.on('passData', (data) => {
console.log('收到数据:', data.product);
});
// 也可以向源页面回传数据
eventChannel.emit('onDetailLoaded', { loaded: true });
});
💡 为什么推荐 eventChannel? URL 传参有长度限制(小程序约 2KB),且只能传字符串。eventChannel 可以传任意 JS 对象,且双向通信。
3.3 小程序端的特殊限制
| 限制项 | 说明 | 解决方案 |
|---|---|---|
| 页面栈上限 10 层 | 超过后 navigateTo 静默失败 | 合理使用 redirectTo/reLaunch |
| switchTab 不能带参 | 设计如此 | 用 Pinia 或 eventChannel |
| 无法自定义转场动画 | 微信系统控制 | 接受或改用自定义组件模拟 |
| 返回按钮由系统渲染 | 左上角胶囊不可自定义 | 使用自定义导航栏组件 |
| 不支持 onPageScroll 节流 | 频繁触发 | 使用 IntersectionObserver 替代 |
四、 原生 Modal/BottomSheet 怎么办?
uni-app 没有 uni.presentModal() 这样的 API。因为小程序没有真正的模态页面概念。需要用以下方式模拟:
方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| uni-popup 插件 | 弹窗/底部抽屉 | 开箱即用,跨端一致 | 不是真正的新页面 |
| 自定义 overlay 组件 | 高度定制的底部面板 | 完全可控 | 需要自己写动画和遮罩 |
| navigateto + 透明背景 | 需要独立生命周期的"伪模态" | 有独立页面栈 | 小程序不支持透明背景页 |
| 原生插件 | 必须用原生 UI 的场景 | 性能最好 | 失去跨端能力 |
推荐:使用 uni-popup 模拟 BottomSheet
xml
<template>
<view @tap="openSheet">打开底部面板</view>
<uni-popup ref="popupRef" type="bottom" :safe-area="true">
<view class="bottom-sheet">
<view class="sheet-header">选择操作</view>
<view class="sheet-item" @tap="onEdit">编辑</view>
<view class="sheet-item" @tap="onDelete">删除</view>
<view class="sheet-cancel" @tap="closeSheet">取消</view>
</view>
</uni-popup>
</template>
<script setup>
const popupRef = ref(null);
const openSheet = () => popupRef.value.open();
const closeSheet = () => popupRef.value.close();
</script>
五、 路由拦截与守卫(类似原生 AppDelegate/RouterInterceptor)
uni-app 没有 Vue Router 那样的全局守卫,但可以通过以下方式实现:
5.1 封装路由工具(推荐)
typescript
// utils/router.ts
const WHITE_LIST = ['/pages/login/login', '/pages/index/index'];
export const navigateTo = (url: string, options?: any) => {
const token = uni.getStorageSync('token');
// 未登录且不在白名单 → 拦截跳转登录页
if (!token && !WHITE_LIST.some(path => url.startsWith(path))) {
uni.navigateTo({ url: `/pages/login/login?redirect=${encodeURIComponent(url)}` });
return;
}
uni.navigateTo({ url, ...options });
};
// 使用时
import { navigateTo } from '@/utils/router';
navigateTo('/pages/order/confirm'); // 自动鉴权
5.2 App 端拦截器
javascript
// main.ts 中(仅 App 端生效)
// #ifdef APP-PLUS
uni.addInterceptor('navigateTo', {
invoke(args) {
console.log('即将跳转:', args.url);
// 可在此修改参数或阻止跳转
},
complete(res) {
console.log('跳转完成:', res);
}
});
// #endif
六、 给原生工程师的路由心智模型
perl
┌─────────────────────────────────────────────────┐
│ uni-app 路由抽象层 │
│ navigateTo / redirectTo / switchTab / reLaunch │
├──────────┬──────────┬───────────┬───────────────┤
│ iOS │ Android │ 鸿蒙 │ 小程序/H5 │
│ NavCtrl │ Activity │ Router │ Page Stack │
│ push/pop │ start/ │ pushUrl/ │ navigateTo/ │
│ present │ finish │ back │ switchTab │
│ modal→ │ Dialog→ │ Dialog→ │ overlay 组件 │
│ 组件模拟 │ 组件模拟 │ 组件模拟 │ (非真页面) │
└──────────┴──────────┴───────────┴───────────────┘
💡 一句话总结: uni-app 的路由是 "小程序页面栈模型 + App 原生转场动画 + 组件模拟模态" 的混合体。它牺牲了部分原生跳转的灵活性(如真正的 modal page、自定义转场手势),换来了跨端一致性。对于绝大多数业务场景,这套抽象足够用;对于极端原生体验需求,通过条件编译 + 原生插件兜底。
需要我继续深入讲分包路由(subPackages)或者路由传参的性能优化吗?这两个话题在大项目中非常关键。