Uniapp路由相关功能及其应用深度解剖教程
书接上篇,我们今天继续来学习一下 UniApp 聊一个项目中必用的技术:UniApp路由进阶,路由模式、传参与拦截器跨平台实战,附带实现思路和所有源码示例,以及常见避坑指南和开发实践。
此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。
致开发者的忠告: AI编程盛行的今天,我们并不是不需要学习技术,而是更应该专研技术,拥有把控全局的架构设计思维才能在AI盛行的未来有立足之地。
言归正传,之前我们聊过UniApp的基础路由API,今天我们来深入探讨一些更进阶的话题:路由模式(hash/history)、各种传参方式的坑、以及路由拦截器的实现。这些知识在开发复杂应用时至关重要,尤其是在跨平台场景下,一不小心就会踩坑。
很多同学会遇到这些问题:
-
在H5用得好好的路由,部署到服务器刷新就404。
-
传参里带了特殊字符,接收时被截断。
-
想在微信小程序实现登录拦截,结果
switchTab绕过拦截。 -
微信公众号里跳转小程序,死活打不开。
-
从Vue转UniApp,总是不自觉地想用
this.$router.push。
别担心,今天我就带你系统地梳理一遍,把这些坑都填平。
一、路由模式:H5端特有的选择
在开始之前,我们要明确一点:路由模式(hash/history)只影响H5端。小程序和App端使用的是原生导航机制,不受这个配置影响。
1.1 Hash 模式 vs History 模式
| 对比维度 | Hash 模式 | History 模式 |
|---|---|---|
| URL 示例 | http://example.com/#/pages/index/index |
http://example.com/pages/index/index |
| 原理 | 利用URL中#后的部分模拟路由,改变#不触发页面刷新 |
利用HTML5 History API,修改浏览器历史记录 |
| 服务器配置 | 无需特殊配置 | 需要配置所有路由指向index.html |
| SEO | 搜索引擎可能忽略#后内容 |
URL规范,对SEO友好 |
| 兼容性 | 极佳(支持所有浏览器) | 需要支持History API的现代浏览器 |
| 常见问题 | URL带#不美观 |
刷新或直接访问子路由返回404 |
1.2 如何在UniApp中配置路由模式
在manifest.json的H5配置中设置:
{"h5": {"router": {"mode": "history"// 可选 "hash" 或 "history" } }}
1.3 History模式的后端配置(重要!)
如果你选择history模式,必须在服务器端配置重定向规则,否则用户直接访问子路由或刷新页面会返回404。
Nginx配置示例:
location / {try_files$uri $uri/ /index.html;}
如果部署在子目录(比如/app/下):
location^~ /app/ {try_files$uri $uri/ /app/index.html;}
Apache配置示例:
<IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L]</IfModule>
1.4 如何选择?
-
选Hash模式:追求部署简便、无需服务器配置,或需兼容老旧环境。
-
选History模式:需要美观的URL、优化SEO,且能配置服务器支持。
二、路由传参的N种姿势与避坑指南
2.1 URL传参(最常用)
// 发送页uni.navigateTo({url: '/pages/detail/detail?id=123&name='+encodeURIComponent('张三&李四')})// 接收页(onLoad中接收)onLoad(options) {console.log(options.id) // '123'console.log(options.name) // '张三&李四'(已自动解码)}
注意点:
-
参数值会被自动
decodeURIComponent,所以你不需要手动解码。 -
但发送前如果参数包含特殊字符(&、=、?、#),必须用
encodeURIComponent编码。 -
参数值默认都是字符串,数字需自行转换
parseInt。 -
URL长度有限制(小程序约2KB),不适合传大量数据。
2.2 通过query传参(另一种写法)
uni.navigateTo({url: '/pages/detail/detail',query: {id: 123,name: '张三' },success: (res) => {console.log('跳转成功') }})
接收方式同样是在onLoad中通过options获取。
2.3 事件总线传参(适合跨页面、返回传参)
// 页面A - 发送uni.$emit('updateUser', { name: '张三' })uni.navigateBack()// 页面B - 接收(在onLoad中监听)onLoad() {uni.$on('updateUser', (data) => {console.log(data) // { name: '张三' } })}onUnload() {uni.$off('updateUser') // 必须移除监听,否则内存泄漏!}
适用场景:
-
从详情页返回列表页,刷新列表。
-
兄弟组件通信。
2.4 全局存储传参(Vuex/Pinia/Storage)
适合传递需要长期保存或跨多页的数据,比如用户信息、购物车数据。
// 发送页uni.setStorageSync('orderInfo', { id: 123 })// 接收页onShow() {constorderInfo=uni.getStorageSync('orderInfo')// 使用后建议移除,避免污染uni.removeStorageSync('orderInfo')}
2.5 EventChannel传参(页面间双向通信)
这是UniApp提供的一种更高级的传参方式,适合需要从目标页返回数据给源页的场景。
// 源页面uni.navigateTo({url: '/pages/detail/detail',events: {// 监听目标页返回的数据acceptDataFromDetail: function(data) {console.log('从详情页返回的数据:', data) } },success(res) {// 通过eventChannel向目标页传递数据res.eventChannel.emit('acceptDataFromOpener', { from: '列表页' }) }})// 目标页面(detail.vue)onLoad(options) {consteventChannel=this.getOpenerEventChannel()// 向源页发送数据eventChannel.emit('acceptDataFromDetail', { result: '操作成功' })// 监听源页传来的数据eventChannel.on('acceptDataFromOpener', (data) => {console.log('收到源页数据:', data) })}
三、跨平台路由陷阱与解决方案
3.1 微信小程序:页面栈限制
问题:小程序页面栈最多10层,超过后navigateTo会失败。
解决方案:监听页面栈深度,超过阈值改用redirectTo或reLaunch。
constpages=getCurrentPages()if (pages.length>=8) { // 留点余量uni.redirectTo({ url })} else {uni.navigateTo({ url })}
3.2 微信小程序:switchTab不能传参
问题:uni.switchTab不支持在URL后带参数。
解决方案:用全局变量或storage。
// 发送页uni.setStorageSync('tabParams', { id: 123 })uni.switchTab({ url: '/pages/index/index' })// 接收页(在onShow中读取)onShow() {constparams=uni.getStorageSync('tabParams')if (params) {// 使用后移除uni.removeStorageSync('tabParams') }}
3.3 微信公众号:History模式导致JS-SDK签名失败
问题:在iOS下,微信会缓存第一次进入的页面地址。如果使用history模式,从一级页面跳到二级页面,路由地址变了,但微信缓存的地址没变,导致wx.config签名失败,进而导致wx-open-launch-weapp等标签无法使用。
原因:iOS有缓存机制,Android没有。跳转后,微信访问的当前页面url和调wx.config签名用到的url对不上。
解决方案:
-
在需要使用JS-SDK的每个页面都重新调用
wx.config。 -
或者改用hash模式(推荐)。
3.4 微信公众号:跳转小程序路径配置
问题:公众号自定义菜单或图文消息中配置跳转小程序,路径填写错误导致无法跳转。
解决方案:
-
路径区分大小写,必须与小程序代码中的路径完全一致。
-
如果需要带参数,格式如
pages/detail/detail?id=123。 -
确保小程序已关联到公众号。
3.5 App端:plus扩展与外部跳转
App端可以使用plus.runtime打开外部应用或网页:
// 打开外部浏览器plus.runtime.openURL('https://www.baidu.com')// 打开其他App(需知道包名)plus.runtime.launchApplication({pname: 'com.tencent.mm'// 微信包名})
3.6 与Vue Router的差异(让熟悉Vue的同学快速上手)
| 对比项 | Vue Router | UniApp 路由 | 避坑指南 |
|---|---|---|---|
| 路由声明 | 路由表配置 | pages.json声明页面 |
不要试图用Vue Router替代原生路由 |
| 跳转API | router.push |
uni.navigateTo等 |
小程序端不能用this.$router.push |
| 接收参数 | $route.query |
onLoad的options |
参数不会自动转数字 |
| 路由守卫 | beforeEach |
uni.addInterceptor |
需手动拦截所有跳转API |
| 动态路由 | 支持/user/:id |
不支持,只能通过URL参数 | 用onLoad的options获取 |
| 嵌套路由 | 支持 | 不支持 | 用组件嵌套模拟 |
重要提醒:虽然UniApp支持this.$router.push(会映射到原生跳转API),但不推荐混用,容易造成混乱。
四、路由拦截器:实现权限控制
UniApp没有内置类似Vue Router的路由守卫,但我们可以用uni.addInterceptor来实现。
4.1 基础实现
// utils/route-guard.jsconstrouteConfig= {// 白名单:无需登录即可访问的页面whiteList: newSet(['/pages/login/login','/pages/register/register','/pages/index/index' ]),loginPage: '/pages/login/login',tokenKey: 'token'}constcheckRoutePermission= (url) => {constpath=url.split('?')[0]// 放行白名单if (routeConfig.whiteList.has(path)) {returntrue }// 检查登录状态consttoken=uni.getStorageSync(routeConfig.tokenKey)return!!token}constrouteInterceptor= {invoke(args) {console.log('路由拦截:', args.url)if (checkRoutePermission(args.url)) {returntrue// 放行 }// 无权限:跳转到登录页,并携带redirect参数constredirectUrl=encodeURIComponent(args.url)uni.redirectTo({url: `${routeConfig.loginPage}?redirect=${redirectUrl}` })returnfalse// 阻止原跳转 }}exportconstinitRouteGuard= () => {constmethods= ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab']methods.forEach(method=> {uni.addInterceptor(method, routeInterceptor) })}
4.2 在App.vue中初始化
<script>import { initRouteGuard } from'@/utils/route-guard'exportdefault {onLaunch() {initRouteGuard() }}</script>
4.3 登录页处理(带重定向)
<scriptsetup>import { onLoad } from'@dcloudio/uni-app'constredirect=ref('')onLoad((options) => {if (options.redirect) {redirect.value=decodeURIComponent(options.redirect) }})constlogin= () => {// 模拟登录成功uni.setStorageSync('token', 'fake-token')if (redirect.value) {// 判断原页面类型if (redirect.value.includes('/pages/user/user')) {uni.switchTab({ url: redirect.value }) } else {uni.redirectTo({ url: redirect.value }) } } else {uni.switchTab({ url: '/pages/index/index' }) }}</script>
4.4 进阶:不同用户角色的权限控制
constcheckRoutePermission= (url) => {constpath=url.split('?')[0]if (routeConfig.whiteList.has(path)) returntrueconsttoken=uni.getStorageSync('token')if (!token) returnfalse// 根据角色检查constrole=uni.getStorageSync('userRole') ||'user'// 管理员专属页面if (path.startsWith('/pages/admin/') &&role!=='admin') {returnfalse }// VIP专属页面if (path.startsWith('/pages/vip/') &&role!=='vip') {returnfalse }returntrue}
五、完整代码示例:带权限控制的演示项目
项目结构
pages/ index/index.vue # 首页(公开) detail/detail.vue # 详情页(需登录) user/user.vue # 个人中心(需登录,tab页) login/login.vue # 登录页(公开)utils/ route-guard.js # 路由拦截器App.vue # 应用入口pages.json # 页面配置
5.1 pages.json配置
{"pages": [ {"path": "pages/index/index", "style": {"navigationBarTitleText": "首页"}}, {"path": "pages/detail/detail", "style": {"navigationBarTitleText": "详情"}}, {"path": "pages/user/user", "style": {"navigationBarTitleText": "我的"}}, {"path": "pages/login/login", "style": {"navigationBarTitleText": "登录"}} ],"tabBar": {"list": [ {"pagePath": "pages/index/index", "text": "首页"}, {"pagePath": "pages/user/user", "text": "我的"} ] }}
5.2 完整路由拦截器(utils/route-guard.js)
/** * 全局路由守卫 * 功能:实现页面跳转的权限校验 * 使用:在App.vue的onLaunch中调用initRouteGuard() */constrouteConfig= {whiteList: newSet(['/pages/index/index','/pages/login/login','/pages/register/register' ]),loginPage: '/pages/login/login',tokenKey: 'token'}constcheckRoutePermission= (url) => {constpath=url.split('?')[0]if (routeConfig.whiteList.has(path)) {returntrue }consttoken=uni.getStorageSync(routeConfig.tokenKey)if (token&&typeoftoken==='string'&&token.length>0) {returntrue }returnfalse}constrouteInterceptor= {invoke(args) {console.log('🚦 路由拦截:', args.url)if (checkRoutePermission(args.url)) {returntrue }console.warn('❌ 无权限,跳转登录页')constredirectUrl=encodeURIComponent(args.url)// 判断原跳转方法,保留跳转类型if (args.type==='switchTab') {uni.switchTab({url: `${routeConfig.loginPage}?redirect=${redirectUrl}` }) } else {uni.redirectTo({url: `${routeConfig.loginPage}?redirect=${redirectUrl}` }) }returnfalse }}exportconstinitRouteGuard= () => {try {constmethods= ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab']methods.forEach(method=> {uni.addInterceptor(method, routeInterceptor) })console.log('✅ 路由守卫已启用') } catch (error) {console.error('❌ 路由守卫初始化失败:', error) }}exportconstupdateWhiteList= (newRoutes) => {routeConfig.whiteList=newSet([...routeConfig.whiteList, ...newRoutes])}
5.3 首页(pages/index/index.vue)
<template><viewclass="p-20"><button@click="goToDetail">去详情页(需登录)</button><button@click="goToUser">去个人中心(需登录)</button><button@click="goToLogin">去登录页</button><button@click="goToDetailWithParams">传参示例</button></view></template><scriptsetup>constgoToDetail= () => {uni.navigateTo({ url: '/pages/detail/detail' })}constgoToUser= () => {uni.switchTab({ url: '/pages/user/user' })}constgoToLogin= () => {uni.navigateTo({ url: '/pages/login/login' })}constgoToDetailWithParams= () => {constname=encodeURIComponent('张三&李四')uni.navigateTo({ url: `/pages/detail/detail?id=123&name=${name}&from=index` })}</script>
5.4 详情页(pages/detail/detail.vue)
<template><viewclass="p-20"><view>商品ID:{{ id }}</view><view>名称:{{ name }}</view><view>来源:{{ from }}</view><button@click="addToCart">加入购物车并返回</button></view></template><scriptsetup>import { ref } from'vue'import { onLoad } from'@dcloudio/uni-app'constid=ref('')constname=ref('')constfrom=ref('')onLoad((options) => {console.log('接收参数:', options)id.value=options.idname.value=options.name||''from.value=options.from||''})constaddToCart= () => {// 模拟加入购物车uni.showToast({ title: '已加入购物车', icon: 'success' })// 返回上一页并传递数据(事件总线示例)uni.$emit('cart-updated', { count: 1 })uni.navigateBack()}</script>
5.5 个人中心(pages/user/user.vue)- tabBar页
<template><viewclass="p-20"><textv-if="user">欢迎 {{ user.name }}</text><textv-else>未登录(理论上不会显示,因为拦截器会拦截)</text><button@click="logout"v-if="user">退出登录</button></view></template><scriptsetup>import { ref } from'vue'import { onShow } from'@dcloudio/uni-app'constuser=ref(null)onShow(() => {consttoken=uni.getStorageSync('token')if (token) {user.value= { name: '张三' } } else {user.value=null }})constlogout= () => {uni.removeStorageSync('token')uni.showToast({ title: '已退出', icon: 'success' })user.value=null}</script>
5.6 登录页(pages/login/login.vue)
<template><viewclass="p-20"><button@click="login">模拟登录</button></view></template><scriptsetup>import { ref } from'vue'import { onLoad } from'@dcloudio/uni-app'constredirect=ref('')onLoad((options) => {if (options.redirect) {redirect.value=decodeURIComponent(options.redirect) }})constlogin= () => {uni.setStorageSync('token', 'fake-token')uni.showToast({ title: '登录成功', icon: 'success' })if (redirect.value) {// 判断原页面是否为tabBar页(简单判断)if (redirect.value.includes('/pages/user/user')) {uni.switchTab({ url: redirect.value }) } else {uni.redirectTo({ url: redirect.value }) } } else {uni.switchTab({ url: '/pages/index/index' }) }}</script>
5.7 App.vue – 初始化
<script>import { initRouteGuard } from'@/utils/route-guard'exportdefault {onLaunch() {initRouteGuard() }}</script>
六、常见错误与解决方案
❌ 错误1:H5端history模式部署后刷新404
原因:服务器未配置重定向规则。解决方案:按前文所述配置Nginx/Apache。
❌ 错误2:URL传参,参数被截断
原因:参数中包含了&、=等特殊字符。解决方案:用encodeURIComponent编码。
❌ 错误3:小程序页面栈溢出
原因:连续navigateTo超过10次。解决方案:监听页面栈深度,适时改用redirectTo。
❌ 错误4:switchTab传参无效
原因:官方不支持。解决方案:用全局存储。
❌ 错误5:微信公众号跳转小程序失败
原因:JS-SDK签名失败,或路径配置错误。解决方案:检查签名逻辑,确保路径大小写正确,考虑用hash模式。
❌ 错误6:拦截器没有拦截switchTab
原因:只拦截了navigateTo。解决方案:拦截所有四个跳转API。
❌ 错误7:事件总线监听导致内存泄漏
原因:监听了但没有在onUnload中移除。解决方案:成对使用uni.$on和uni.$off。
七、总结
今天我们深入学习了UniApp路由的方方面面:
-
路由模式:hash简单,history美观但需后端支持。
-
传参方式:URL传参最简单,注意编码;EventChannel适合双向通信;事件总线适合跨页面。
-
跨平台坑点:小程序页面栈、公众号history兼容性、tabBar传参等。
-
路由拦截:用
addInterceptor实现全局权限控制。
最后送大家一句话:路由是应用的骨架,理解了路由,就理解了应用的脉络。在跨平台开发中,多测试、多总结,才能写出健壮的代码。
如果在实际项目中遇到其他奇怪的问题,欢迎带着代码来问我。
—— 一个在路由坑里摸爬滚打多年的老前辈 !
加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!


往期相关文章推荐
夜雨聆风
