v2.2.0 新增
onBeforeRouteLeave组合式 API,支持组件内通过返回值模式拦截离开导航;同时彻底移除next()回调模式,守卫系统全面统一为返回值模式,与 Vue Router 4.x 完全一致。
前言
@meng-xi/uni-router 在 v2.1.0 引入了守卫返回值模式作为推荐方式,同时保留 next() 回调的向后兼容性。v2.2.0 彻底移除 next() 回调支持,守卫系统全面统一为返回值模式,同时新增 onBeforeRouteLeave 组件内离开守卫,实现完整的组件级守卫能力。
一、破坏性变更:彻底移除 next() 回调
变更内容
| 变更项 | v2.1.x | v2.2.0 |
|---|---|---|
NavigationGuard 签名 |
(to, from, next?) |
(to, from) |
NavigationGuardNext 类型 |
存在(已弃用) | 移除 |
NavigationGuardNextOptions 类型 |
存在(已弃用) | 移除 |
runGuardWithNext() 函数 |
存在 | 移除 |
runGuard() 模式检测 |
自动检测参数个数 | 仅返回值模式 |
迁移对照
| 旧写法 | 新写法 |
|---|---|
next() |
不写 return 或 return undefined |
next(false) |
return false |
next({ name: 'login' }) |
return { name: 'login' } |
next({ name: 'login' }, { mode: 'replace' }) |
return { name: 'login' }(mode 不再支持) |
next().catch(...) |
try { return await ... } catch { return false } |
next(new Error('msg')) |
throw new Error('msg') 或 return new Error('msg') |
二、新增能力:onBeforeRouteLeave 组件内离开守卫
1. 问题分析
在表单编辑、数据提交等场景中,用户可能误操作离开当前页面导致数据丢失。Vue Router 4.x 提供了 onBeforeRouteLeave 组合式 API 来解决这一问题,允许组件在离开前执行确认逻辑。
1.1 表单编辑场景无法阻止离开
typescript
// v2.1.x --- 需要手动注册全局守卫,手动管理移除
let removeGuard: (() => void) | null = null
onMounted(() => {
removeGuard = router.beforeEach((to, from) => {
if (from.path === '/pages/edit/edit' && hasUnsavedChanges) {
return false
}
})
})
onBeforeUnmount(() => {
removeGuard?.()
})
这种方式代码分散、容易遗漏清理,且每个组件都需要重复编写相同的逻辑。
1.2 全局守卫随着组件增多而膨胀
typescript
// v2.1.x --- 所有组件的离开逻辑集中在一个全局守卫中
router.beforeEach((to, from) => {
if (from.path === '/pages/edit/edit' && hasUnsavedChanges) return false
if (from.path === '/pages/form/form' && formDirty) return false
if (from.path === '/pages/post/post' && !postSaved) return false
// ... 随着组件增多,守卫越来越长
})
全局守卫不应关注组件内部状态,组件内守卫更符合"关注点分离"原则。
2. onBeforeRouteLeave 组合式 API
onBeforeRouteLeave 在组件 setup 中调用,注册一个仅在离开当前组件时触发的守卫。组件卸载时自动移除,无需手动清理。
typescript
import { onBeforeRouteLeave } from '@meng-xi/uni-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges) {
return false // 中止导航
}
// 不返回值或 return true 表示放行
})
返回值对照表
| 返回值 | 行为 |
|---|---|
undefined / void / true |
放行,允许离开 |
false |
中止导航(NAVIGATION_ABORTED) |
RouteLocationRaw |
重定向到其他路由 |
Error 对象 |
取消导航(NAVIGATION_CANCELLED) |
| 抛出异常 | 取消导航(NAVIGATION_CANCELLED) |
3. 异步离开确认
支持异步守卫,结合 uni.showModal 实现确认对话框:
typescript
import { onBeforeRouteLeave } from '@meng-xi/uni-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges) {
return new Promise(resolve => {
uni.showModal({
title: '提示',
content: '有未保存的修改,确认离开?',
success: res => resolve(res.confirm ? true : false)
})
})
}
})
4. RouteLeaveGuard 类型
typescript
type RouteLeaveGuard = (to: RouteLocation, from: RouteLocation) => NavigationGuardReturn | Promise<NavigationGuardReturn>
三、实现原理
onBeforeRouteLeave 内部通过 router.beforeEach 注册一个全局前置守卫,但只在 from 匹配当前组件路径时执行用户守卫:
typescript
export function onBeforeRouteLeave(guard: RouteLeaveGuard): void {
const router = useRouter()
const route = useRoute()
const fromPath = route.value.path
// 注册全局前置守卫,仅在 from 匹配当前组件路径时执行
const remove = router.beforeEach((to, from) => {
if (from.path !== fromPath) return // 不匹配时自动放行
return guard(to, from) // 执行用户守卫
})
// 组件卸载时自动移除守卫
onBeforeUnmount(remove)
}
执行流程
javascript
组件 setup
└── onBeforeRouteLeave(guard)
└── router.beforeEach((to, from) => {
├── from.path !== fromPath → 放行(不执行 guard)
└── from.path === fromPath → 执行 guard(to, from)
├── guard return undefined / true → 放行
├── guard return false → 中止导航
├── guard return RouteLocationRaw → 重定向
├── guard return Error → 取消导航
└── guard 抛出异常 → 取消导航
})
└── onBeforeUnmount(remove) // 组件卸载时自动移除
重要限制
onBeforeRouteLeave 只能拦截经过路由器 push / replace / back / relaunch 的导航,无法拦截以下场景:
- 物理返回键(Android 系统返回键)
- 侧滑返回手势(iOS 屏幕左滑)
- 浏览器后退按钮(H5)
- 小程序左上角返回按钮
- H5 平台的
uni.switchTab(因interceptUniApi对switchTab采用"放行原始调用 + success 回调同步状态"策略)
对于这些场景,需要在 onShow 中通过 syncRoute() 同步状态后做事后处理。
四、完整使用示例
场景一:表单编辑离开确认
typescript
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave } from '@meng-xi/uni-router'
const formDirty = ref(false)
function markDirty() {
formDirty.value = true
}
// 同步离开确认
onBeforeRouteLeave(() => {
if (formDirty.value) {
uni.showToast({ title: '有未保存的修改,已阻止离开', icon: 'none' })
return false
}
})
</script>
场景二:异步确认对话框
typescript
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave } from '@meng-xi/uni-router'
const hasUnsavedChanges = ref(false)
onBeforeRouteLeave(() => {
if (hasUnsavedChanges.value) {
return new Promise((resolve) => {
uni.showModal({
title: '确认离开',
content: '有未保存的修改,确定要离开吗?',
success: (res) => resolve(res.confirm)
})
})
}
})
</script>
场景三:离开时保存数据
typescript
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave } from '@meng-xi/uni-router'
const draft = ref('')
const isSaving = ref(false)
onBeforeRouteLeave(async () => {
if (draft.value) {
isSaving.value = true
try {
await saveDraft(draft.value)
// 保存成功,放行
} catch {
// 保存失败,阻止离开
return false
} finally {
isSaving.value = false
}
}
})
</script>
五、升级指南
新增导出
typescript
// 组合式 API
export { onBeforeRouteLeave } from '@meng-xi/uni-router'
// 类型
export type { RouteLeaveGuard } from '@meng-xi/uni-router'
推荐迁移
如果之前使用全局守卫模拟组件内离开逻辑,可迁移到 onBeforeRouteLeave:
typescript
// 迁移前 --- 手动管理全局守卫
const removeGuard = router.beforeEach((to, from) => {
if (from.path === '/pages/edit/edit' && hasUnsavedChanges) {
return false
}
})
onBeforeUnmount(removeGuard)
// 迁移后 --- 组件内离开守卫
onBeforeRouteLeave(() => {
if (hasUnsavedChanges) {
return false
}
})
版本兼容性
| 功能 | v2.1.x | v2.2.0 |
|---|---|---|
onBeforeRouteLeave |
不支持 | 支持 |
RouteLeaveGuard 类型 |
不存在 | 新增 |
| 守卫返回值模式 | 支持 | 支持 |
next() 回调模式 |
支持(已弃用) | 不支持 |
afterEach 接收 failure |
支持 | 支持 |