uni-app实现vue-router开发体验
在 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.ts
import { 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.ts
import { 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:
| 方法 | uni API | 说明 |
|---|---|---|
push(location) |
uni.navigateTo / uni.switchTab |
导航到新页面 |
replace(location) |
uni.redirectTo / uni.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 |
是否为 TabBar 页面 |
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 的区别:
| 特性 | afterEach | onRouteChange |
|---|---|---|
| 路由器导航完成 | 触发 | 触发 |
| syncRoute 同步 | 不触发 | 触发 |
| 可移除 | 返回移除函数 | 返回移除函数 |
| 用途 | 导航后置逻辑 | 路由状态变化订阅 |
_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>当前路径:{{ $route.path }}</view> <button @click="$router.back()">返回</button>
7. RouterLink 组件
声明式导航组件,基于 uni-app 的 <navigator> 封装:
<mxuni-router to="/pages/about/about">
<view>关于我们</view>
</mxuni-router>
<!-- 命名路由 -->
<mxuni-router :to="{ name: 'detail', query: { id: '1' } }">
<view>查看详情</view>
</mxuni-router>
<!-- 替换模式 -->
<mxuni-router to="/pages/login/login" replace>
<view>登录</view>
</mxuni-router>
<!-- 导航失败处理 -->
<mxuni-router to="/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 |
按住多久出现点击态(ms) |
hoverStayTime |
number |
600 |
松开后点击态保留时间(ms) |
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 |
uni 导航 API 调用失败 |
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.ts
declare 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 |
拦截 uni 原生导航 API |
guardTimeout |
number |
10000 |
守卫超时时间(ms) |
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.ts
import { 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

浙公网安备 33010602011771号