更新记录
1.0.2(2026-08-28) 下载此版本
query 缺失
1.0.1(2026-08-28) 下载此版本
修复在蒸汽模式编译安卓下无法正常使用的问题; 新增 router.push 使用 options.query 传参方式;
1.0.0(2026-08-28) 下载此版本
vue-router 风格的 uni-app x 路由封装,支持前置/后置导航守卫、返回上一页携带数据
查看更多平台兼容性
uni-app(5.21)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(5.21)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| √ | √ | √ | - | - | √ |
holo-router
vue-router 风格的 uni-app x 路由封装(uni_modules 前端插件),提供全局导航守卫与返回传参能力。
- 前置守卫
beforeEach/ 后置钩子afterEach/ 错误回调onError push / replace / reLaunch / switchTab / back五个导航方法push / replace / reLaunch支持query对象传参,自动拼接为 url 参数back可携带数据回传给上一页(配合push的onBack回调)- Android / iOS / Web / 微信小程序行为完全一致,无需任何平台适配
目录
安装
将 uni_modules/holo-router 目录复制到项目根目录的 uni_modules 下:
项目根目录/
├── uni_modules/
│ └── holo-router/
│ ├── package.json
│ └── index.uts ← 全部实现(前端插件,与页面同层编译)
└── utils/
└── router.uts ← 路由实例入口(自行创建,见下文)
环境要求:uni-app x 项目(uvue 页面),HBuilderX 4.41+(微信小程序端)。
从 1.x 升级:2.0 起改为前端插件形态,请删除旧版的整个
utssdk/目录(含index.uts与interface.uts),否则残留文件会被 HBuilderX 仍按 uts 插件编译并报错。utils/router.uts的 import 路径同步改为"@/uni_modules/holo-router/index.uts",页面代码(import router from "@/utils/router")无需任何改动。
快速开始
第 1 步:创建 utils/router.uts,实例化 Router、注册守卫:
// utils/router.uts
import { Router, RouteLocation, redirect, cancel } from "@/uni_modules/holo-router/index.uts"
const router = new Router()
router.beforeEach((to: RouteLocation, from: RouteLocation, next: () => void): any => {
next() // 放行
return null
})
router.afterEach((to: RouteLocation, from: RouteLocation): void => {
console.log("[router] " + from.fullPath + " -> " + to.fullPath)
})
export default router
第 2 步:页面中导入使用:
import router from "@/utils/router"
router.push("/pages/detail/detail", { query: { id: 1 } }) // 跳转并传参
router.back({ data: { changed: true } }) // 返回并携带数据
API
导航方法
push / replace / reLaunch 支持两种传参方式,可混用(混用时 query 对象以 & 追加到 url 已有参数之后,同名参数以 query 对象为准):
// 方式一:query 对象(推荐,写法清晰)
router.push("/pages/detail/detail", { query: { id: 1, pid: 2 } })
// 方式二:url 直接拼接(传统写法,仍完全支持)
router.push("/pages/detail/detail?id=1&pid=2")
query对象的值会统一encodeURIComponent(中文、特殊字符安全),null 值跳过;数字会转为字符串(与 url 传参一致,目标页onLoad与守卫to.query中取到的都是 string,数字请自行parseInt);复杂对象请先JSON.stringify传入,接收方解析。switchTab不支持 query(uni 规定 tabBar 页不能带参数,传入会被丢弃并打印警告)。
router.push(url, options?)
对应 uni.navigateTo,保留当前页面入栈跳转(页面栈上限 10 层)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url |
string |
是 | 目标页面路径,可带 query,如 /pages/detail/detail?id=1&from=list |
options.query |
UTSJSONObject \| null |
否 | query 参数对象,自动拼接为 url 参数({ id: 1, pid: 2 } → ?id=1&pid=2) |
options.onBack |
(data: UTSJSONObject \| null) => void |
否 | 目标页 router.back 携带数据返回时触发,data 即 back 传入的数据 |
router.push("/pages/detail/detail", {
query: { id: 1, pid: 2 },
onBack: (data: UTSJSONObject | null): void => {
const changed = data?.getBoolean("changed") ?? false
if (changed) {
refresh() // 返回后刷新列表
}
}
})
router.replace(url, options?)
对应 uni.redirectTo,关闭当前页面并跳转。适合登录后替换登录页、详情页互相切换等场景。options.query 用法与 push 相同(onBack 对 replace 无意义,传了也不会触发)。
router.reLaunch(url, options?)
对应 uni.reLaunch,关闭所有页面后跳转。适合退出登录回首页。options.query 用法与 push 相同(注:uni 规定 reLaunch 跳 tabBar 页时不能带参数)。
router.switchTab(url, options?)
对应 uni.switchTab,跳转 tabBar 页面。uni 规定 tabBar 页 url 不能携带参数:为保持签名一致仍接受 options,但 query 会被丢弃并打印警告,tab 页传参请使用全局状态(Pinia / uni.$emit 等)。
router.back(options?)
对应 uni.navigateBack,返回上一页或多级页面,可携带数据给目标页。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
options.delta |
number |
否 | 返回的页面数,默认 1 |
options.data |
UTSJSONObject \| null |
否 | 传给目标页 onBack 回调的数据 |
router.back() // 普通返回
router.back({ data: { id: 5, changed: true } }) // 携带数据返回
router.back({ delta: 2, data: { refresh: true } }) // 跨层返回
数据回传机制:back 的 data 会传给「目标页当初 push 下一页时注册的 onBack 回调」,回调在导航执行前同步触发,保证目标页 onShow 时数据已就绪。
导航守卫
router.beforeEach(guard)
全局前置守卫,在 push / replace / reLaunch / switchTab 导航前依次执行(back 不触发)。
守卫签名:
(to: RouteLocation, from: RouteLocation, next: () => void) => any
三种控制方式:
| 方式 | 用法 | 说明 |
|---|---|---|
| 放行 | next() |
可同步调用,也可在异步操作完成后调用(如校验 token 接口) |
| 重定向 | return redirect(url) |
取消本次导航,改为 push 到新地址;redirect(url, true) 则以 replace 方式跳转 |
| 取消 | return cancel() |
中止本次导航,触发 onError(1001) |
UTS 限制:UTS 闭包不支持默认参数,因此
next()无法像 vue-router 那样传参(如next('/login')),重定向/取消请使用return redirect(url)/return cancel()。这与 Vue Router 4 推荐的返回值风格一致。
守卫返回值需为 any 类型(return redirect(...) 或 return null),注册多个守卫时按注册顺序执行。
router.afterEach(hook)
全局后置钩子,导航成功后触发(back 成功也会触发):
router.afterEach((to: RouteLocation, from: RouteLocation): void => {
// 常用于埋点上报
})
router.onError(hook)
导航失败 / 被取消 / 重定向超限时触发:
router.onError((code: number, message: string): void => {
if (code >= 3000) {
uni.showToast({ title: "页面跳转失败" })
}
})
router.getCurrentRoute()
获取当前路由信息,返回 RouteLocation:
const route = router.getCurrentRoute()
console.log(route.path) // /pages/index/index
console.log(route.getString("id")) // query 取值
RouteLocation 路由信息
| 属性/方法 | 类型 | 说明 |
|---|---|---|
path |
string |
页面路径(不含 query,统一带前导 /) |
fullPath |
string |
完整路径(含 query) |
query |
Map<string, string> |
query 参数,值已 decodeURIComponent |
getString(key) |
string |
取 query 值,不存在返回 "" |
hasQuery(key) |
boolean |
是否存在该 query 参数 |
query 来源:通过 router 导航进入的页面,query 取自导航 url 解析;未经 router 进入的页面(App 启动、tabBar 点击、直接调用 uni API 跳转),query 从页面栈实例的 options 读取(同样已解码)。两种方式行为一致。
守卫指令函数
redirect(url, replace?)
创建重定向指令。replace 默认 false(用 push 跳转),传 true 时用 replace 跳转(不保留当前页)。
cancel()
创建取消指令,中止导航。
类型定义
export type NextFunction = () => void
export type NavigationGuard = (to: RouteLocation, from: RouteLocation, next: NextFunction) => any
export type PostNavigationHook = (to: RouteLocation, from: RouteLocation) => void
export type ErrorHook = (code: number, message: string) => void
export type OnBackCallback = (data: UTSJSONObject | null) => void
export type NavigationOptions = {
query?: UTSJSONObject | null
onBack?: OnBackCallback | null
}
export type BackOptions = {
delta?: number | null
data?: UTSJSONObject | null
}
场景示例
登录拦截(重定向 + 回跳)
// utils/router.uts
import { Router, RouteLocation, redirect } from "@/uni_modules/holo-router/index.uts"
import { useUserStore } from "@/stores/user" // 示例:Pinia / 全局状态
const router = new Router()
const WHITE_LIST = ["/", "/pages/login/login", "/pages/index/index"]
router.beforeEach((to: RouteLocation, from: RouteLocation, next: () => void): any => {
if (WHITE_LIST.includes(to.path)) {
next()
return null
}
if (!useUserStore().isLogin) {
// 登录后可回跳原页面
return redirect("/pages/login/login?redirect=" + encodeURIComponent(to.fullPath))
}
next()
return null
})
export default router
// 登录页:登录成功后跳回原页面
const redirect = router.getCurrentRoute().getString("redirect")
router.replace(redirect != "" ? decodeURIComponent(redirect) : "/pages/index/index")
返回上一页携带数据
// 列表页 A
import router from "@/utils/router"
function goDetail(id: number) {
router.push("/pages/detail/detail", {
query: { id: id },
onBack: (data: UTSJSONObject | null): void => {
const changed = data?.getBoolean("changed") ?? false
if (changed) loadList() // 详情页有改动,返回后刷新
}
})
}
// 详情页 B
import router from "@/utils/router"
function saveAndBack() {
router.back({ data: { id: 5, changed: true } })
}
Android 实体返回键携带数据
通过 onBackPress 拦截(App-Android),把系统返回也走 router.back:
// 详情页 B
import router from "@/utils/router"
onBackPress(options: OnBackPressOptions): boolean {
// from == "navigateBack" 说明是 router.back 触发,直接放行避免死循环
if (options.from == "navigateBack") return false
router.back({ data: { changed: true } })
return true // 拦截默认返回行为
}
异步守卫(先校验再放行)
router.beforeEach((to: RouteLocation, from: RouteLocation, next: () => void): any => {
if (to.path == "/pages/pay/pay") {
checkTokenValid().then((valid: boolean): void => {
if (valid) {
next() // 异步放行
} else {
router.replace("/pages/login/login") // 异步改跳
}
})
return null // 未同步 next(),导航挂起等待异步结果
}
next()
return null
})
错误码
| code | 含义 |
|---|---|
1001 |
导航被守卫 cancel() 取消 |
1002 |
重定向次数超过上限(默认 10 层,防死循环) |
2001 |
back 失败:页面栈深度不足 |
2002 |
uni.navigateBack 调用失败 |
3001 |
uni.navigateTo 调用失败(页面未注册 / 栈超 10 层等) |
3002 |
uni.redirectTo 调用失败 |
3003 |
uni.reLaunch 调用失败 |
3004 |
uni.switchTab 调用失败(目标页不在 tabBar 等) |
所有错误同时会 console.error("[holo-router] ..."),3001–3004 的 message 中含 uni 返回的 errCode 与 errMsg,可用于精确定位(如 errCode=4 表示页面栈超限)。
使用限制与注意事项
-
守卫拦截范围:仅拦截通过
router实例发起的导航。以下方式不会触发守卫:- tabBar 图标点击
- 页面导航栏返回按钮
<navigator>组件跳转- 直接调用
uni.navigateTo等 API
uni-app x 没有全局路由钩子(
uni.addInterceptor在 uni-app x 仅 Android 支持、iOS 不支持),如需全站拦截请统一使用router方法跳转。 -
back不走beforeEach:返回方向不执行前置守卫,但成功后触发afterEach。 -
next()不支持传参:UTS 闭包不支持默认参数的硬限制,重定向用return redirect(url),取消用return cancel()。 -
守卫必须调用
next()或返回指令:既不调用next()也不返回指令时,导航会一直挂起(用于异步校验场景)。 -
onBack回调与页面栈自动对齐:即使某处代码直接调用了uni.navigateBack/uni.redirectTo造成页面栈变化,router 每次操作前会自动与真实页面栈同步,不会错位;但直接uni.navigateBack返回时不会触发onBack(没有数据可传)。 -
switchTab不能带参数:uni 平台限制,url 与options.query均不支持(query 传入会被丢弃并打印警告),tabBar 页需要传参请使用全局状态(Pinia /uni.$emit等)。 -
守卫返回值:
beforeEach的返回类型为any,无指令时请return null(UTS 强类型要求)。 -
query 值均为 string:无论 url 拼接还是
query对象传入,数字都会转为字符串({ id: 1 }取到"1"),数字请自行parseInt/parseFloat;复杂对象请先JSON.stringify再传入,接收方解析。
FAQ
Q:为什么是「前端插件」而不是「uts 插件」(utssdk 目录)?
A:路由封装是纯逻辑组件,无需任何原生能力。而 uts 插件编译环境存在系统性限制——导航 API(uni.navigateTo 等)不在 uts 插件可调用的 uni API 白名单内(支持列表,Kotlin/Swift 编译期直接报错),页面栈实例的 options 等属性也无法访问,运行时环境与页面层还有隔离。前端插件(根目录 index.uts)与 uvue 页面同层编译,可直接调用所有 uni API,天然没有这些限制。仅当需要调用原生系统 API(蓝牙、传感器等)时才需要 uts 插件。
Q:query 对象和 url 里直接写 ?id=1 能同时用吗?
A:可以,两者会合并(query 对象以 & 追加在 url 已有参数之后,同名参数以 query 对象为准)。守卫中的 to.query 与目标页 onLoad 拿到的是合并后的完整参数。
Q:onBack 回调的参数类型写不写注解?
A:建议写:onBack: (data: UTSJSONObject | null): void => { ... }。UTS 对闭包参数类型要求严格,部分 HBuilderX 版本无法从上下文自动推导时可省略的注解会编译报错,显式注解最稳妥。
Q:为什么不用 uni.addInterceptor 实现全局拦截?
A:uni.addInterceptor 在 uni-app x 中仅 Android 端支持(iOS 不支持),不能作为跨端核心方案,因此本组件采用「统一封装导航方法」的方式实现守卫。
Q:onBack 和 uni.$emit 事件总线有什么区别?
A:onBack 是注册时绑定的定向回调,无需在目标页监听/注销,随 router 生命周期自动管理,且能严格对应「谁 push 的谁接收」;事件总线需要手动 uni.$on / uni.$off 配对,容易泄漏。
Q:多次 push 后多层返回,onBack 怎么触发?
A:back({ delta: 2 }) 时,数据传给栈中目标页(第 delta 层之上)当初注册的 onBack 回调;中间层页面的回调会被清理不再触发。
Q:redirect 会不会造成无限循环?
A:内置 10 层重定向上限(A -> B -> A -> ... 超过 10 次即停止并触发 onError(1002)),守卫逻辑本身也应避免无条件重定向。

收藏人数:
下载插件并导入HBuilderX
赞赏(0)
下载 2
赞赏 0
下载 12541443
赞赏 1947
赞赏
京公网安备:11010802035340号