在 uni-app 的静态页面模型(pages.json)下,实现 vue-router 的开发体验——导航守卫、命名路由、类型提示、API 拦截,一个不少。
为什么需要它?
uni-app 的路由是声明式的——在 pages.json 中注册页面,通过 uni.navigateTo / uni.redirectTo / uni.navigateBack 跳转。这种模式简单直接,但在中大型项目中会遇到几个痛点:
- 没有守卫机制
:无法在导航前做权限校验、登录拦截,只能在每个页面的 onShow里重复判断 - 没有命名路由
:硬编码路径字符串,页面路径调整时需要全局搜索替换 - 没有路由元信息
:页面标题、权限要求等散落在各处,无法统一管理 - 没有类型提示
:路径拼错、参数遗漏只能在运行时发现 - 原生 API 绕过守卫
:即使写了守卫逻辑,直接调用 uni.navigateTo就绕过了
@meng-xi/uni-router 在不改变 uni-app 页面模型的前提下,为上述问题提供了一整套解决方案。
快速上手
安装
# npm 包方式pnpm add @meng-xi/uni-router# 或使用 uni_modules# 将 mxuni-router 目录复制到项目的 uni_modules 目录下
创建路由器
// src/router/index.tsimport { createRouter } from '@meng-xi/uni-router'const router = createRouter({routes: [{ path: 'pages/index/index', name: 'home', meta: { title: '首页', isTab: true } },{ path: 'pages/about/about', name: 'about', meta: { title: '关于' } },{ path: 'pages/user/user', name: 'user', meta: { title: '我的', isTab: true } },{ path: 'pages/detail/detail', name: 'detail', meta: { title: '详情', requireAuth: true } },{ path: 'pages/login/login', name: 'login', meta: { title: '登录' } }],strict: true, // 严格模式,未匹配的命名路由将抛出异常interceptUniApi: true, // 拦截 uni 原生导航 API,确保守卫始终生效guardTimeout: 10000 // 守卫超时保护(毫秒),防止导航永久挂起})export default router
注册到 Vue 应用
// main.tsimport { createSSRApp } from 'vue'import App from './App.vue'import router from './router'export function createApp() {const app = createSSRApp(App)app.use(router)return { app }}
在组件中使用
import { useRouter, useRoute } from '@meng-xi/uni-router'const router = useRouter()const route = useRoute()// 路径字符串导航await router.push('/pages/about/about')// 命名路由导航await router.push({ name: 'detail', query: { id: '1' } })// 替换当前页面await router.replace({ name: 'login' })// 返回上一页await router.back()// 读取当前路由信息console.log(route.value.path) // '/pages/detail/detail'console.log(route.value.query) // { id: '1' }console.log(route.value.meta) // { title: '详情', requireAuth: true }
核心功能详解
1. 路由导航:push / replace / back
三个核心导航方法,分别对应 uni-app 的原生导航 API:
push(location) | uni.navigateTouni.switchTab | |
replace(location) | uni.redirectTouni.switchTab | |
back(delta?) | uni.navigateBack |
智能 API 选择:根据 meta.isTab 自动选择 navigateTo 还是 switchTab,无需手动判断:
// 自动使用 uni.switchTab(因为 meta.isTab = true)await router.push({ name: 'home' })// 自动使用 uni.navigateTo(普通页面)await router.push({ name: 'detail', query: { id: '1' } })
重复导航检测:push 到当前已处于的页面时,会拒绝导航并抛出 NAVIGATION_DUPLICATED 错误,避免页面栈中出现重复页面。
并发导航排队:如果前一次导航尚未完成,新的导航会自动排队等待,避免并发导航导致页面栈混乱。
导航位置支持三种写法:
// 路径字符串router.push('/pages/about/about?id=1')// 路径对象router.push({ path: '/pages/about/about', query: { id: '1' } })// 命名对象router.push({ name: 'about', query: { id: '1' } })
2. 导航守卫:完整的守卫链
这是 uni-router 最重要的能力——在 uni-app 中实现 vue-router 风格的导航守卫。
守卫执行顺序
导航触发↓全局前置守卫 beforeEach↓路由独享守卫 beforeEnter↓全局解析守卫 beforeResolve↓执行 uni 导航 API↓全局后置钩子 afterEach
全局前置守卫 beforeEach
在每次导航前执行,常用于权限校验、登录拦截:
router.beforeEach(async (to, from, next) => {if (to.meta.requireAuth && !isLoggedIn()) {next({ name: 'login' }) // 重定向到登录页} else {next() // 放行}})
next 函数的三种调用方式:
next() | |
next(false) | |
next(location) |
路由独享守卫 beforeEnter
定义在路由配置上,只在进入该路由时触发:
const router = createRouter({routes: [{path: 'pages/admin/admin',name: 'admin',meta: { title: '管理后台' },beforeEnter: (to, from, next) => {if (isAdmin()) next()else next({ name: 'home' })}}]})
支持单个守卫函数或守卫数组:
beforeEnter: [checkAuth, checkPermission]全局解析守卫 beforeResolve
在所有前置守卫和路由独享守卫完成后执行,适合做最终的数据预取确认:
router.beforeResolve(async (to, from, next) => {// 所有守卫已通过,可以安全地做数据预取await prefetchData(to)next()})
全局后置钩子 afterEach
导航完成后执行,不影响导航结果,适合做页面标题设置、埋点等:
router.afterEach((to, from) => {if (to.meta.title) {uni.setNavigationBarTitle({ title: to.meta.title as string })}})
守卫超时保护
守卫中写了异步请求但忘记调用 next(),会导致导航永久挂起。guardTimeout 配置项为守卫设置超时时间,超时后自动中止导航:
const router = createRouter({routes: [...],guardTimeout: 10000, // 10 秒超时(默认值),设为 0 可禁用})
超时时控制台输出警告:
[uni-router] Navigation guard "checkPermission" did not resolve within 10s. Make sure to call next() in your guard function.守卫重定向防循环
守卫中 next(location) 会触发新的导航,如果重定向目标又触发重定向,可能形成无限循环。uni-router 设置了最大重定向深度(10 次),超过后自动取消导航。
守卫移除
所有守卫注册方法都返回移除函数,支持动态注册和移除:
const removeGuard = router.beforeEach((to, from, next) => {// 一次性守卫next()removeGuard() // 执行后立即移除})
3. 命名路由与路由元信息
命名路由
为路由定义 name,通过名称而非路径进行导航,降低路径耦合:
// 定义{ path: 'pages/detail/detail', name: 'detail' }// 使用router.push({ name: 'detail', query: { id: '1' } })
路由元信息 meta
为路由附加自定义数据,统一管理页面属性:
{ path: 'pages/detail/detail', name: 'detail', meta: { title: '详情', requireAuth: true } }在守卫中读取:
router.beforeEach((to, from, next) => {if (to.meta.requireAuth && !isLoggedIn()) {next({ name: 'login' })} else {next()}})
meta 支持自定义扩展字段([key: string]: unknown),内置约定字段:
title | string | |
isTab | boolean | |
requireAuth | boolean |
4. uni 原生 API 拦截
这是 uni-router 的一个关键能力——拦截 uni.navigateTo、uni.redirectTo、uni.switchTab、uni.navigateBack 四个原生导航 API,确保路由守卫始终生效。
为什么需要拦截?
即使注册了 beforeEach 守卫,代码中直接调用 uni.navigateTo 仍会绕过守卫。第三方库、历史代码、同事的提交都可能直接调用原生 API。启用拦截后,所有导航调用都会被路由器接管:
const router = createRouter({routes: [...],interceptUniApi: true, // 启用拦截})
拦截原理
拦截器通过 uni.addInterceptor 注册 invoke 回调,区分两种调用来源:
- 路由器内部发起
:通过 markRouterCall()标记,拦截器放行 - 外部直接调用
:阻止原始 API 调用,转由 router.push/router.replace/router.back执行完整守卫链
// 这两种写法效果相同,都会经过守卫链uni.navigateTo({ url: '/pages/about/about' })router.push('/pages/about/about')
低版本基础库兼容
部分低版本小程序基础库可能忽略 invoke 回调的返回值,导致拦截失效。uni-router 采用双重保险策略——除了返回 false 阻止原始调用外,还会将 args.url 置为空字符串,确保即使返回值被忽略,导航也不会到达非预期页面。
5. 路由状态同步
问题场景
uni-app 中,物理返回键、浏览器后退、手势滑动返回等操作不经过路由器,导致 currentRoute 与实际页面不同步。
syncRoute()
在每个页面的 onShow 生命周期中调用,从 uni-app 页面栈读取当前页面信息并更新路由状态:
// 每个页面的 onShow 中调用onShow(() => {router.syncRoute()})
syncRoute 会同时比较路径和查询参数,只有两者都发生变化时才更新状态,避免不必要的通知。
onRouteChange()
统一监听所有路由变化,包括导航完成和状态同步:
router.onRouteChange((to, from) => {// 导航完成和 syncRoute 同步都会触发analytics.track('page_view', { from: from.path, to: to.path })})
与 afterEach 的区别:
_synced 标记
通过 syncRoute 同步的路由变化,to._synced 为 true,可用于区分变化来源:
router.onRouteChange((to, from) => {if (!to._synced) {// 真正的导航,记录页面浏览analytics.track('page_view', { path: to.path })}// 状态同步(如物理返回键),仅更新 UI 状态updateActiveTab(to.path)})
6. 组合式 API:useRouter / useRoute
useRouter()
获取路由器实例,必须在 setup() 中调用:
import { useRouter } from '@meng-xi/uni-router'const router = useRouter()await router.push({ name: 'home' })
useRoute()
获取当前路由位置的响应式引用,路由变化时组件自动更新:
import { useRoute } from '@meng-xi/uni-router'const route = useRoute()// 在模板中直接使用// {{ route.path }}// {{ route.query.id }}// {{ route.meta.title }}
useRoute 内部通过 WeakMap 缓存响应式 ref,同一 router 实例共享同一个 ref,避免重复创建。
模板中使用全局属性
安装路由器后,模板中可直接访问 $router 和 $route:
<view>当前路径:{{ router.back()">返回</button>7. RouterLink 组件
声明式导航组件,基于 uni-app 的 <navigator> 封装:
<mxuni-routerto="/pages/about/about"><view>关于我们</view></mxuni-router><!-- 命名路由 --><mxuni-router:to="{ name: 'detail', query: { id: '1' } }"><view>查看详情</view></mxuni-router><!-- 替换模式 --><mxuni-routerto="/pages/login/login"replace><view>登录</view></mxuni-router><!-- 导航失败处理 --><mxuni-routerto="/pages/protected/index"@error="onNavError"><view>需要登录的页面</view></mxuni-router>
Props:
to | RouteLocationRaw | ||
replace | boolean | false | |
hoverClass | string | 'navigator-hover' | |
hoverStopPropagation | boolean | false | |
hoverStartTime | number | 50 | |
hoverStayTime | number | 600 |
Events:
error | NavigationFailure |
8. 错误处理
错误类型
uni-router 提供两种错误类:
- RouterError
:路由基础错误,包含 code和message - NavigationFailure
:导航失败错误,继承 RouterError,额外包含to、from、cause
错误码
NAVIGATION_ABORTED | |
NAVIGATION_CANCELLED | |
NAVIGATION_DUPLICATED | |
ROUTE_NOT_FOUND | |
NAVIGATION_API_ERROR | |
SETUP_ERROR |
onError()
注册全局错误处理器,所有导航错误都会经过这里:
router.onError((error, to, from) => {if (error.code === 'NAVIGATION_ABORTED') {uni.showToast({ title: '导航被拦截', icon: 'none' })}if (error.code === 'NAVIGATION_API_ERROR') {uni.showToast({ title: '页面跳转失败', icon: 'none' })}// 上报错误trackError(error, to, from)})
try/catch 处理
也可以在调用处直接捕获:
try {await router.push({ name: 'detail', query: { id: '1' } })} catch (error) {if (error.code === 'NAVIGATION_DUPLICATED') {// 已在详情页,忽略}}
9. 路由匹配与解析
resolve()
将原始路由位置解析为完整的 RouteLocation 对象,不执行导航:
const location = router.resolve({ name: 'detail', query: { id: '1' } })// { path: '/pages/detail/detail', name: 'detail', meta: { ... }, query: { id: '1' }, fullPath: '/pages/detail/detail?id=1' }
hasRoute()
检查是否存在指定名称的路由:
if (router.hasRoute('admin')) {// 路由存在}
getRoutes()
获取所有已注册的路由配置列表:
const routes = router.getRoutes()严格模式
启用 strict: true 后,通过名称解析不存在的路由将抛出 ROUTE_NOT_FOUND 错误;关闭后仅输出警告并回退到默认路径。
10. TypeScript 类型提示
RouteNameMap 类型增强
通过模块增强(module augmentation)为路由名称和路径提供类型提示:
// env.d.tsdeclare module '@meng-xi/uni-router' {interface RouteNameMap {home: { path: '/pages/index/index'; meta: { title: string; isTab: true } }about: { path: '/pages/about/about'; meta: { title: string } }detail: { path: '/pages/detail/detail'; meta: { title: string; requireAuth: boolean } }}}
增强后,name 和 path 字段在 IDE 中获得自动补全和类型检查:
router.push({ name: 'detail' }) // ✅ 自动补全router.push({ name: 'detial' }) // ❌ 类型错误
完整类型导出
export type {RouteNameMap,RouteName,RoutePath,RouteMeta,RouteConfig,RouteLocation,RouteLocationPathRaw,RouteLocationNamedRaw,RouteLocationRaw,NavigationGuardNext,NavigationGuard,PostNavigationGuard,RouterOnError,RouterOptions,Router}export { RouterErrorCode }export { RouterError, NavigationFailure }
11. 路由器生命周期
isReady()
等待路由器初始化完成。路由器在首次设置 currentRoute 后标记为就绪:
await router.isReady()// 路由器已就绪,可以安全访问 currentRoute
install()
安装路由器到 Vue 应用实例,完成以下工作:
通过 provide/inject注册路由器,使useRouter()/useRoute()可用注册 $router和$route全局属性(避免与 uni-app H5 内置 vue-router 冲突)若启用 interceptUniApi,注册 uni API 拦截器
API 速查
Router 实例方法
push(location) | |
replace(location) | |
back(delta?) | |
beforeEach(guard) | |
beforeResolve(guard) | |
afterEach(guard) | |
onRouteChange(listener) | |
onError(handler) | |
resolve(location) | |
hasRoute(name) | |
getRoutes() | |
syncRoute() | |
isReady() |
Router 实例属性
currentRoute |
组合式 API
useRouter() | |
useRoute() |
RouterOptions 配置项
routes | RouteConfig[] | ||
strict | boolean | true | |
interceptUniApi | boolean | false | |
guardTimeout | number | 10000 |
RouteConfig 路由配置
path | string | |
name | string | |
meta | RouteMeta | |
beforeEnter | NavigationGuard | NavigationGuard[] |
RouteLocation 路由位置
path | string | |
name | string | |
meta | RouteMeta | |
query | Record<string, string> | |
fullPath | string | |
_synced | boolean |
兼容性说明
- 仅支持 uni-app Vue 3 版本
,不兼容 Vue 2 支持全平台:H5、微信小程序、支付宝小程序、百度小程序、抖音小程序、App 等 getCurrentPages()在 SSR / Node 环境下安全降级,不会抛出 ReferenceErrorapp.onUnmount在不支持的环境中自动跳过,不影响功能 拦截器在低版本小程序基础库下采用双重保险策略,确保导航不会被意外放行
设计理念
不改变 uni-app 的页面模型
uni-router 不引入动态路由,不改变 pages.json 的声明方式,不替换 uni-app 的页面栈管理。它是在 uni-app 原生导航 API 之上的一层抽象,通过守卫、拦截和状态同步来增强路由能力。
守卫优先
所有导航(包括被拦截的原生 API 调用)都会经过完整的守卫链。守卫是路由器的核心能力,不是可选的附加功能。
防御性编程
守卫超时保护,防止导航永久挂起 并发导航排队,防止页面栈混乱 重定向深度限制,防止无限循环 环境检测降级,确保非 uni-app 环境不崩溃 重复安装警告,帮助快速定位多实例问题
类型安全
通过 TypeScript 类型增强,让路由名称、路径和元信息在 IDE 中获得自动补全和类型检查,将运行时错误提前到编译时发现。
完整示例
// src/router/index.tsimport { createRouter } from '@meng-xi/uni-router'const router = createRouter({routes: [{ path: 'pages/index/index', name: 'home', meta: { title: '首页', isTab: true } },{ path: 'pages/about/about', name: 'about', meta: { title: '关于' } },{ path: 'pages/user/user', name: 'user', meta: { title: '我的', isTab: true } },{ path: 'pages/detail/detail', name: 'detail', meta: { title: '详情', requireAuth: true } },{ path: 'pages/login/login', name: 'login', meta: { title: '登录' } },{path: 'pages/admin/admin',name: 'admin',meta: { title: '管理后台', requireAuth: true, requireAdmin: true },beforeEnter: (to, from, next) => {if (isAdmin()) next()else next({ name: 'home' })}}],strict: true,interceptUniApi: true,guardTimeout: 10000})// 全局前置守卫:登录拦截router.beforeEach(async (to, from, next) => {if (to.meta.requireAuth && !isLoggedIn()) {next({ name: 'login', query: { redirect: to.fullPath } })} else {next()}})// 全局解析守卫:数据预取router.beforeResolve(async (to, from, next) => {if (to.name === 'detail' && to.query.id) {await prefetchDetail(to.query.id)}next()})// 全局后置钩子:页面标题router.afterEach(to => {if (to.meta.title) {uni.setNavigationBarTitle({ title: to.meta.title as string })}})// 路由变化监听:埋点router.onRouteChange((to, from) => {if (!to._synced) {analytics.track('page_view', { from: from.path, to: to.path })}})// 全局错误处理router.onError(error => {console.error('[Router Error]', error.code, error.message)})export default router
GitHub: MengXi-Studio/uni-router | License: MIT
夜雨聆风