本文基于 v1.2.0 版本,全面介绍 @meng-xi/uni-router 的设计理念与完整功能。
为什么需要 uni-router?
uni-app 原生路由系统基于 pages.json 静态配置,导航通过 uni.navigateTo / uni.redirectTo / uni.navigateBack 等 API 直接调用,缺少以下关键能力:
- 无路由守卫:无法在导航前进行权限校验、登录检查等拦截
- 无命名路由:必须硬编码路径字符串,重构时容易遗漏
- 无路由元信息:无法为路由附加标题、权限标记等结构化数据
- 无错误体系:导航失败时只能通过回调获取,缺乏统一的错误处理机制
- 无状态同步:浏览器后退、物理返回键等场景下路由状态可能不一致
@meng-xi/uni-router 的目标是在 uni-app 的静态页面模型上,提供一套 vue-router 风格的路由管理方案,让开发者在 uni-app 中也能享受现代化的路由开发体验。
核心设计理念
不替代 pages.json,而是与之配合
uni-router 不替代 pages.json。页面注册仍由 pages.json 负责,uni-router 在此基础上提供路由导航、守卫、元信息等增强能力。这种设计确保了:
- 完全兼容 uni-app 的页面管理机制
- 不影响
pages.json的条件编译等原生能力 - 可以渐进式引入,无需改造现有项目
基于 uni-app 原生 API 实现
所有导航操作最终通过 uni.navigateTo / uni.redirectTo / uni.switchTab / uni.navigateBack 执行,不绕过 uni-app 的页面管理机制,确保跨平台行为一致。
功能全景
一、路由导航
push --- 导航到新页面
typescript
// 路径字符串
await router.push('/pages/about/about')
// 路径对象 + 查询参数
await router.push({ path: '/pages/about/about', query: { id: '1' } })
// 命名路由
await router.push({ name: 'about' })
// 带动画参数(仅 App 端生效)
await router.push({ path: '/pages/about/about', animation: { type: 'slide-in-bottom', duration: 500 } })
push 自动根据 meta.isTab 选择 uni.navigateTo(普通页面)或 uni.switchTab(TabBar 页面),开发者无需手动判断。
replace --- 替换当前页面
typescript
await router.replace('/pages/login/login')
await router.replace({ name: 'home' })
对应 uni.redirectTo。替换 TabBar 页面时自动切换为 uni.switchTab(会关闭所有非 Tab 页面)。
back --- 返回上一页
typescript
await router.back() // 返回上一页
await router.back(2) // 返回两级
await router.back(1, { type: 'slide-out-left' }) // 指定动画
back() 执行完整的守卫链,守卫可以中止或重定向返回操作。若未指定动画参数,将使用目标页面的 meta.animation 作为默认动画。
重复导航检测
push 到当前页面时自动拒绝并抛出 NAVIGATION_DUPLICATED 错误,避免重复入栈。
并发导航排队
多次并发导航自动排队,前一次完成后再执行下一次,避免页面栈混乱。
二、路由守卫
路由守卫是 uni-router 最核心的能力,提供完整的导航拦截机制。
全局前置守卫 --- beforeEach
在每次导航前执行,常用于登录验证:
typescript
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
next({ name: 'login', query: { redirect: to.fullPath } })
} else {
next()
}
})
全局解析守卫 --- beforeResolve
在所有前置守卫和路由独享守卫完成后执行,适合需要确保所有守卫都已通过的场景:
typescript
router.beforeResolve((to, from, next) => {
// 所有前置守卫已通过,导航即将执行
next()
})
全局后置钩子 --- afterEach
在导航完成后执行,不影响导航结果,适合埋点、标题设置等操作:
typescript
router.afterEach((to, from) => {
if (to.meta.title) {
uni.setNavigationBarTitle({ title: to.meta.title as string })
}
})
路由独享守卫 --- beforeEnter
在路由配置中定义,仅对该路由生效:
typescript
const routes = [
{
path: 'pages/admin/admin',
name: 'admin',
meta: { requireAuth: true },
beforeEnter: (to, from, next) => {
if (isAdmin()) next()
else next({ name: 'forbidden' })
}
}
]
守卫执行顺序
beforeEach → beforeEnter → beforeResolve → 导航执行 → afterEach
守卫重定向
守卫中调用 next(location) 可重定向到其他路由,支持多级重定向(最大深度 10):
typescript
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
next({ name: 'login' }) // 重定向到登录页
} else {
next()
}
})
守卫超时保护
通过 guardTimeout 配置项(默认 10000ms),防止守卫未调用 next() 导致导航永久挂起:
typescript
const router = createRouter({
routes,
guardTimeout: 15000 // 15 秒超时
})
三、命名路由
通过 name 字段进行导航,避免硬编码路径字符串:
typescript
// 路由配置
const routes = [
{ path: 'pages/index/index', name: 'home', meta: { isTab: true } },
{ path: 'pages/about/about', name: 'about', meta: { title: '关于' } }
]
// 导航时使用名称
await router.push({ name: 'about', query: { id: '1' } })
配合 @meng-xi/vite-plugin 自动生成的类型声明,路由名称可获得 TypeScript 自动补全和类型检查。
四、路由元信息
meta 字段支持页面标题、权限标记、TabBar 标识、导航动画等自定义数据:
typescript
interface RouteMeta {
title?: string // 页面标题
isTab?: boolean // 是否为 TabBar 页面
requireAuth?: boolean // 是否需要登录认证
animation?: NavigationAnimation // 默认导航动画(仅 App 端)
[key: string]: unknown // 自定义扩展字段
}
使用示例:
typescript
const routes = [
{ path: 'pages/index/index', name: 'home', meta: { isTab: true, title: '首页' } },
{ path: 'pages/about/about', name: 'about', meta: { animation: { type: 'fade-in' } } },
{ path: 'pages/admin/admin', name: 'admin', meta: { requireAuth: true } }
]
五、导航动画
v1.2.0 新增完整的页面切换动画支持,仅 App 端生效,其他平台自动忽略。
NavigationAnimation 接口
typescript
interface NavigationAnimation {
type: UniAnimationType // 动画类型
duration?: number // 持续时间(ms),默认 300
}
UniAnimationType --- 完整动画类型
| 显示动画(navigateTo) | 关闭动画(navigateBack) |
|---|---|
slide-in-right |
slide-out-right |
slide-in-left |
slide-out-left |
slide-in-top |
slide-out-top |
slide-in-bottom |
slide-out-bottom |
fade-in |
fade-out |
zoom-out |
zoom-in |
zoom-fade-out |
zoom-fade-in |
pop-in |
pop-out |
auto / none |
auto / none |
三种使用方式
1. 导航时传入动画参数
typescript
await router.push({ path: '/pages/about/about', animation: { type: 'slide-in-bottom' } })
await router.back(1, { type: 'slide-out-left', duration: 500 })
2. 路由级默认动画(meta.animation)
typescript
const routes = [{ path: 'pages/about/about', name: 'about', meta: { animation: { type: 'fade-in' } } }]
3. RouterLink 声明式动画
vue
<RouterLink to="/pages/about/about" :animation="{ type: 'slide-in-bottom' }">
底部滑入
</RouterLink>
动画优先级
push/replace 调用时传入 > meta.animation > uni 默认值
六、uni API 拦截
通过 interceptUniApi 选项拦截原生导航 API,确保路由守卫始终生效:
typescript
const router = createRouter({
routes,
interceptUniApi: true // 拦截 uni.navigateTo 等原生 API
})
启用后,以下调用将被拦截并转由路由器处理:
typescript
// 这两种方式等价,都会经过守卫链
uni.navigateTo({ url: '/pages/about/about' })
router.push('/pages/about/about')
拦截原理
- 通过
uni.addInterceptor注册拦截器 - 路由器内部发起的 API 调用通过计数器标记放行,避免重复执行守卫
- 外部调用被拦截后,阻止原始 API 执行,转由
router.push/replace/back执行完整守卫链 - 低版本小程序基础库兼容:修改
args.url为空字符串,防止忽略返回值继续执行
拦截范围
| API | 拦截后行为 |
|---|---|
uni.navigateTo |
→ router.push |
uni.redirectTo |
→ router.replace |
uni.switchTab |
→ router.push |
uni.navigateBack |
→ router.back |
七、组合式 API
useRouter --- 获取路由器实例
typescript
import { useRouter } from '@meng-xi/uni-router'
const router = useRouter()
await router.push('/pages/about/about')
必须在 Vue 组件的 setup() 中调用,通过 Vue 的 inject 机制获取路由器实例。
useRoute --- 获取响应式路由位置
typescript
import { useRoute } from '@meng-xi/uni-router'
const route = useRoute()
// route 是 Ref<RouteLocation>,路由变化时自动更新
console.log(route.value.path)
console.log(route.value.query)
同一 router 实例共享同一个响应式 ref,通过 WeakMap 缓存避免重复创建。
八、RouterLink 组件
基于 uni-app navigator 封装的声明式导航组件:
vue
<template>
<!-- 路径跳转 -->
<RouterLink to="/pages/about/about">关于页面</RouterLink>
<!-- 命名路由 + 替换模式 -->
<RouterLink :to="{ name: 'about' }" replace>替换导航</RouterLink>
<!-- 带动画参数 -->
<RouterLink to="/pages/about/about" :animation="{ type: 'fade-in' }"> 淡入动画 </RouterLink>
<!-- 错误处理 -->
<RouterLink :to="{ name: 'admin' }" @error="onNavError">管理后台</RouterLink>
</template>
<script setup>
import { RouterLink } from '@meng-xi/uni-router/components/RouterLink.vue'
function onNavError(error) {
console.log('导航失败:', error.code)
}
</script>
Props
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
to |
RouteLocationRaw |
- | 目标路由位置 |
replace |
boolean |
false |
是否使用替换模式 |
animation |
NavigationAnimation |
undefined |
导航动画(仅 App 端) |
hoverClass |
string |
'navigator-hover' |
按下时的样式类 |
hoverStopPropagation |
boolean |
false |
阻止祖先节点点击态 |
hoverStartTime |
number |
50 |
按住后出现点击态时间 |
hoverStayTime |
number |
600 |
松开后点击态保留时间 |
Events
| 事件 | 参数 | 说明 |
|---|---|---|
error |
NavigationFailure |
导航失败时触发 |
九、路由状态同步
当页面通过浏览器后退、物理返回键等非路由器方式切换时,路由器的 currentRoute 可能与实际页面不同步。syncRoute() 方法从 uni-app 页面栈中读取当前页面信息并更新路由状态:
typescript
// 在每个页面的 onShow 生命周期中调用
import { onShow } from '@dcloudio/uni-app'
import { useRouter } from '@meng-xi/uni-router'
const router = useRouter()
onShow(() => {
router.syncRoute()
})
onRouteChange --- 路由变化监听
注册路由状态变化监听器,导航完成和状态同步时都会触发:
typescript
router.onRouteChange((to, from) => {
console.log(`路由变化: ${from.path} → ${to.path}`)
})
与 afterEach 不同,onRouteChange 也会捕获 syncRoute() 触发的状态变化。
十、错误处理
完整的错误体系
typescript
// RouterError --- 路由错误基类
class RouterError extends Error {
readonly code: RouterErrorCode
}
// NavigationFailure --- 导航失败,包含来源和目标信息
class NavigationFailure extends RouterError {
readonly to: RouteLocation
readonly from: RouteLocation
readonly cause?: unknown
}
错误码
| 错误码 | 说明 |
|---|---|
NAVIGATION_ABORTED |
导航被守卫中止或守卫超时 |
NAVIGATION_CANCELLED |
导航被取消(守卫异常或重定向超限) |
NAVIGATION_DUPLICATED |
重复导航到当前位置 |
ROUTE_NOT_FOUND |
未找到匹配的路由 |
NAVIGATION_API_ERROR |
uni 导航 API 调用失败 |
SETUP_ERROR |
路由器初始化或使用方式错误 |
全局错误捕获
typescript
router.onError((error, to, from) => {
if (error.code === 'NAVIGATION_ABORTED') {
console.log('导航被中止')
}
})
十一、TypeScript 类型提示
配合 @meng-xi/vite-plugin 自动生成的类型声明,为路由导航提供类型安全:
typescript
// 路由名称自动补全
router.push({ name: 'pagesIndexIndex' }) // ✅ 自动补全
router.push({ name: 'invalidName' }) // ❌ 类型错误
// 路径自动补全
router.push({ path: '/pages/index/index' }) // ✅ 自动补全
router.push({ path: '/invalid/path' }) // ❌ 类型错误
通过模块增强(module augmentation)填充 RouteNameMap 接口即可启用:
typescript
declare module '@meng-xi/uni-router' {
interface RouteNameMap {
pagesIndexIndex: { path: '/pages/index/index'; meta: { title: string; isTab: true } }
pagesAboutAbout: { path: '/pages/about/about'; meta: { title: string } }
}
}
API 速查
核心
| API | 说明 |
|---|---|
createRouter(options) |
创建路由器实例 |
useRouter() |
获取路由器实例(组合式 API) |
useRoute() |
获取响应式路由位置(组合式 API) |
RouterLink |
声明式导航组件 |
Router 实例方法
| 方法 | 说明 |
|---|---|
router.push(location) |
导航到新页面 |
router.replace(location) |
替换当前页面 |
router.back(delta?, animation?) |
返回上一页或多级页面 |
router.beforeEach(guard) |
注册全局前置守卫 |
router.beforeResolve(guard) |
注册全局解析守卫 |
router.afterEach(guard) |
注册全局后置钩子 |
router.onRouteChange(fn) |
注册路由变化监听器 |
router.onError(handler) |
注册错误处理回调 |
router.syncRoute() |
同步路由状态与页面栈 |
router.resolve(location) |
解析路由位置(不导航) |
router.getRoutes() |
获取所有路由配置 |
router.hasRoute(name) |
检查路由是否存在 |
RouterOptions
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
routes |
RouteConfig[] |
- | 路由配置列表 |
strict |
boolean |
true |
严格模式,未匹配路由时抛出异常 |
interceptUniApi |
boolean |
false |
拦截原生导航 API |
guardTimeout |
number |
10000 |
守卫超时时间(ms) |
与 pages.json 的关系
| 职责 | pages.json | uni-router |
|---|---|---|
| 页面注册 | 必须声明 | 不负责 |
| 路由导航 | uni.navigateTo 等 |
push / replace / back |
| 路由守卫 | 不支持 | beforeEach 等 |
| 路由元信息 | 不支持 | meta 字段 |
| 命名路由 | 不支持 | name 字段 |
| 导航动画 | 手动传参 | animation + meta.animation |
快速开始
1. 安装
bash
pnpm add @meng-xi/uni-router
2. 配置路由
typescript
// src/router.config.ts
import type { RouteConfig } from '@meng-xi/uni-router'
const routes: RouteConfig[] = [
{ path: 'pages/index/index', name: 'home', meta: { isTab: true, title: '首页' } },
{ path: 'pages/about/about', name: 'about', meta: { title: '关于' } },
{ path: 'pages/login/login', name: 'login', meta: { title: '登录' } },
{ path: 'pages/admin/admin', name: 'admin', meta: { requireAuth: true } }
]
export default routes
3. 创建路由器
typescript
// src/main.ts
import { createSSRApp } from 'vue'
import { createRouter } from '@meng-xi/uni-router'
import routes from './router.config'
import App from './App.vue'
const router = createRouter({
routes,
strict: true,
interceptUniApi: true,
guardTimeout: 15000
})
// 注册全局守卫
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
next({ name: 'login' })
} else {
next()
}
})
router.afterEach(to => {
if (to.meta.title) {
uni.setNavigationBarTitle({ title: to.meta.title as string })
}
})
export function createApp() {
const app = createSSRApp(App)
app.use(router)
return { app }
}
4. 在页面中使用
vue
<template>
<view>
<text>当前路径: {{ route.path }}</text>
<button @click="goAbout">跳转关于</button>
<button @click="goBack">返回</button>
</view>
</template>
<script setup>
import { useRouter, useRoute } from '@meng-xi/uni-router'
const router = useRouter()
const route = useRoute()
async function goAbout() {
try {
await router.push({ name: 'about', query: { from: 'home' } })
} catch (e) {
console.log('导航失败:', e.code)
}
}
async function goBack() {
await router.back()
}
</script>
平台兼容性
| 平台 | 支持情况 |
|---|---|
| uni-app (Vue 3) | ✅ 完全支持 |
| uni-app H5 (Safari / Chrome) | ✅ 完全支持 |
| uni-app App (Android / iOS) | ✅ 完全支持(动画仅 App 端生效) |
| uni-app 小程序(微信/支付宝/百度/字节等) | ✅ 完全支持 |
| uni-app x Web | ✅ 支持 |
| uni-app x 小程序 | ✅ 支持 |
| uni-app x App (Android / iOS / Harmony) | ❌ 不支持(无 JS 引擎,需 UTS 重写) |
总结
@meng-xi/uni-router 为 uni-app 开发者提供了一套完整的路由管理方案:
- vue-router 风格 API:零学习成本,上手即用
- 完整的守卫体系:beforeEach / beforeResolve / afterEach / beforeEnter,支持重定向和超时保护
- 导航动画:三种使用方式,优先级清晰,仅 App 端生效
- uni API 拦截:确保守卫始终生效,无论通过何种方式发起导航
- TypeScript 类型提示:路由名称和路径自动补全
- 错误处理体系:完整的错误码和全局错误捕获
- 组合式 API:useRouter / useRoute,响应式路由状态
- 路由状态同步:处理浏览器后退、物理返回键等场景
如果你在 uni-app 项目中需要路由守卫、命名路由、元信息管理等能力,@meng-xi/uni-router 是你的最佳选择。