更新记录

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 可携带数据回传给上一页(配合 pushonBack 回调)
  • 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.utsinterface.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 } }) // 跨层返回

数据回传机制:backdata 会传给「目标页当初 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 返回的 errCodeerrMsg,可用于精确定位(如 errCode=4 表示页面栈超限)。


使用限制与注意事项

  1. 守卫拦截范围:仅拦截通过 router 实例发起的导航。以下方式不会触发守卫

    • tabBar 图标点击
    • 页面导航栏返回按钮
    • <navigator> 组件跳转
    • 直接调用 uni.navigateTo 等 API

    uni-app x 没有全局路由钩子(uni.addInterceptor 在 uni-app x 仅 Android 支持、iOS 不支持),如需全站拦截请统一使用 router 方法跳转。

  2. back 不走 beforeEach:返回方向不执行前置守卫,但成功后触发 afterEach

  3. next() 不支持传参:UTS 闭包不支持默认参数的硬限制,重定向用 return redirect(url),取消用 return cancel()

  4. 守卫必须调用 next() 或返回指令:既不调用 next() 也不返回指令时,导航会一直挂起(用于异步校验场景)。

  5. onBack 回调与页面栈自动对齐:即使某处代码直接调用了 uni.navigateBack / uni.redirectTo 造成页面栈变化,router 每次操作前会自动与真实页面栈同步,不会错位;但直接 uni.navigateBack 返回时不会触发 onBack(没有数据可传)。

  6. switchTab 不能带参数:uni 平台限制,url 与 options.query 均不支持(query 传入会被丢弃并打印警告),tabBar 页需要传参请使用全局状态(Pinia / uni.$emit 等)。

  7. 守卫返回值beforeEach 的返回类型为 any,无指令时请 return null(UTS 强类型要求)。

  8. 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:onBackuni.$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)),守卫逻辑本身也应避免无条件重定向。


隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

暂无用户评论。