乐于分享
好东西不私藏

uni-app实现vue-router开发体验

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.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:

方法
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),内置约定字段:

字段
类型
说明
titlestring
页面标题
isTabboolean
是否为 TabBar 页面
requireAuthboolean
是否需要登录认证

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._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:

属性
类型
默认值
说明
toRouteLocationRaw
目标路由位置
replacebooleanfalse
是否使用替换模式
hoverClassstring'navigator-hover'
按下时的样式类
hoverStopPropagationbooleanfalse
阻止祖先节点点击态
hoverStartTimenumber50
按住多久出现点击态(ms)
hoverStayTimenumber600
松开后点击态保留时间(ms)

Events:

事件
参数
说明
errorNavigationFailure
导航失败时触发

8. 错误处理

错误类型

uni-router 提供两种错误类:

  • RouterError
    :路由基础错误,包含 code 和 message
  • 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.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 应用实例,完成以下工作:

  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 配置项

选项
类型
默认值
说明
routesRouteConfig[]
路由配置列表
strictbooleantrue
严格模式
interceptUniApibooleanfalse
拦截 uni 原生导航 API
guardTimeoutnumber10000
守卫超时时间(ms)

RouteConfig 路由配置

字段
类型
说明
pathstring
页面路径
namestring
路由名称
metaRouteMeta
路由元信息
beforeEnterNavigationGuard | NavigationGuard[]
路由独享守卫

RouteLocation 路由位置

字段
类型
说明
pathstring
规范化后的路径
namestring
路由名称
metaRouteMeta
路由元信息
queryRecord<string, string>
查询参数
fullPathstring
完整路径(含查询参数)
_syncedboolean
是否为状态同步

兼容性说明

  • 仅支持 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.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