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.navigateTouni.redirectTouni.switchTabuni.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._syncedtrue,可用于区分变化来源:

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>

声明式导航组件,基于 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:路由基础错误,包含 codemessage
  • NavigationFailure:导航失败错误,继承 RouterError,额外包含 tofromcause

错误码

错误码 说明
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 } }
	}
}

增强后,namepath 字段在 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 应用实例,完成以下工作:

  1. 通过 provide/inject 注册路由器,使 useRouter() / useRoute() 可用
  2. 注册 $router$route 全局属性(避免与 uni-app H5 内置 vue-router 冲突)
  3. 若启用 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 环境下安全降级,不会抛出 ReferenceError
  • app.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

posted @ 2026-06-11 19:12  PedroQue99  阅读(20)  评论(0)    收藏  举报