v2.0.0 将 params 传递、页面间通信、导航动画、uni API 拦截四大功能拆分为独立插件,未注册的插件不增加包体积和运行时开销;同时修复了
usePageChannel()返回空通道和onShow多余状态同步事件两个核心 Bug
前言
@meng-xi/uni-router 在 v1.x 已经提供了完整的路由管理能力:导航、守卫、参数传递、页面通信、动画、API 拦截。但所有功能都内建在核心中,即使用户只需要基础导航,也会引入全部代码和运行时逻辑。
v2.0.0 引入 Swiper.js 风格的插件架构,将扩展功能拆分为四个独立插件,按需注册。同时修复了两个影响功能正确性的 Bug。
一、问题分析
1. 核心包体积膨胀
v1.x 的 createRouter 将所有功能内建:
typescript
// v1.x --- 所有功能默认可用,全部代码打包
const router = createRouter({
routes,
interceptUniApi: true,
paramsPersistent: false,
useUniEventChannel: false
})
即使不使用 params、events、animation,相关代码仍被包含。随着功能增加,核心包持续膨胀。
2. usePageChannel() 返回空通道
v1.x 中目标页调用 usePageChannel() 时,route.params.__navId 为空,返回 noopChannel(所有方法为空操作),导致页面间通信完全失效。
根因 :setCurrentRoute(to) 时 to.params 不包含 __navId(还在 query 中),目标页 <script setup> 执行时 usePageChannel() 读到空值。虽然 syncCurrentRoute() 在导航完成后会执行 runSyncHooks 提取 __navId,但此时目标页已经持有了 noopChannel。
3. onShow 每次触发多余的状态同步
syncRoute() 比较 URL query(含 __nav_id)和 currentRoute.query(不含),认为不同,重复触发 syncCurrentRoute,产生多余的 onRouteChange 事件。
根因 :URL query 中的内部 key(__nav_id、__params_key)未被移除就与 currentRoute.query 比较,导致每次 onShow 都判定为"不同"。
二、新增能力
1. 插件架构
v2.0.0 采用 Swiper.js 风格的插件系统,核心仅提供基础导航能力,扩展功能通过插件按需注册:
typescript
// v2.0 --- 按需注册
import { createRouter, ParamsPlugin, ChannelPlugin, InterceptorPlugin, AnimationPlugin } from '@meng-xi/uni-router'
const router = createRouter({
routes,
plugins: [ParamsPlugin, ChannelPlugin, InterceptorPlugin, AnimationPlugin],
interceptUniApi: true // 需要 InterceptorPlugin
})
插件列表
| 插件 | 功能 | 对应选项 |
|---|---|---|
ParamsPlugin |
params 参数传递、持久化 | paramsPersistent |
ChannelPlugin |
页面间通信、usePageChannel | useUniEventChannel |
InterceptorPlugin |
拦截 uni 原生导航 API | interceptUniApi |
AnimationPlugin |
导航动画 | meta.animation |
插件生命周期 Hook
每个插件通过 PluginContext 注册 7 种生命周期 hook:
scss
onEnrichLocation --- matcher.resolve() 前增强原始路由位置
onAfterResolve --- resolve 后提取插件数据存入 pluginData
onPrepareNavigation --- uni API 调用前修改导航 URL query 和选项
onCompleteNavigation --- uni API 调用成功后扩展 NavigationResult
onNavigationAbort --- 导航中止或失败时清理
onRouteSync --- syncCurrentRoute 期间从 URL query 提取插件数据
onAppInstall --- router.install() 时注册 app 级别清理逻辑
PLUGIN_REQUIRED 错误
使用未注册插件的功能时,抛出明确错误:
typescript
// 未注册 ParamsPlugin 但使用了 params
router.push({ path: '/detail', params: { id: 123 } })
// → RouterError: PLUGIN_REQUIRED --- ParamsPlugin is required to use params
2. applySyncHooks 导航预处理
在 setCurrentRoute 前执行 routeSyncHooks,将 __nav_id 等内部 key 从 query 提取到 params:
typescript
// 导航流程(简化)
const toWithSyncedParams = this.applySyncHooks(to) // __nav_id: query → params
this.routeState.setCurrentRoute(toWithSyncedParams) // 目标页 setup 时 params.__navId 已可用
await navigateTo(navOptions) // 触发页面跳转
这确保了目标页 <script setup> 执行时 usePageChannel() 能正确获取通道实例。
三、Bug 修复
1. usePageChannel() 返回空通道
修复前:
scss
导航发起页 emit('fromOpener', data) → 目标页 usePageChannel() 返回 noopChannel → 事件丢失
修复后:
scss
导航发起页 emit('fromOpener', data) → 目标页 usePageChannel() 返回 UniEventChannel → 事件正常接收
2. onShow 多余的状态同步事件
修复前:
erlang
页面 onShow → syncRoute() → URL query 含 __nav_id,currentRoute.query 不含 → 判定为"不同" → 多余的 onRouteChange
修复后:
scss
页面 onShow → syncRoute() → 先 runSyncHooks 移除内部 key → isSameQuery 返回 true → 跳过同步
四、架构设计
插件注册与 Hook 分发
scss
createRouter({ plugins: [A, B, C] })
└── installPlugins()
├── A.install(context, options)
│ ├── context.onEnrichLocation(hook1)
│ ├── context.onRouteSync(hook2)
│ └── context.onPrepareNavigation(hook3)
├── B.install(context, options)
│ └── context.onCompleteNavigation(hook4)
└── C.install(context, options)
└── context.onAppInstall(hook5)
导航流程:
push(location)
├── enrichLocationHooks → A.hook1
├── guard chain
├── applySyncHooks → A.hook2 (query → params)
├── setCurrentRoute
├── prepareNavigationHooks → A.hook3
├── uni.navigateTo()
├── completeNavigationHooks → B.hook4
└── afterEach
syncRoute():
├── runSyncHooks → 移除内部 key
├── isSameQuery → 跳过或继续
└── syncCurrentRoute → runSyncHooks → A.hook2
install():
└── appInstallHooks → C.hook5 (注册清理逻辑)
插件间数据共享
pluginData: Record<string, any> 在导航流程各阶段间传递,插件通过约定 key 存取数据:
typescript
// ChannelPlugin --- onAfterResolve 阶段写入
pluginData['channel'] = { navId, channel }
// ChannelPlugin --- onCompleteNavigation 阶段读取
const { channel } = pluginData['channel']
新增导出
typescript
// 插件
export { ParamsPlugin, AnimationPlugin, ChannelPlugin, InterceptorPlugin } from '@/plugins'
// 组合式 API
export { usePageChannel } from '@/plugins'
// 通道实例
export { UniEventChannel, noopChannel } from '@/plugins/channel/uni-event-channel'
// 类型
export type { RouterPlugin, PluginContext, NavigationPrepareContext, NavigationCompleteContext } from '@/types'
// 错误码
export { RouterErrorCode } from '@/types' // 新增 PLUGIN_REQUIRED
五、完整使用示例
场景:仅使用基础导航 + 守卫
typescript
import { createRouter } from '@meng-xi/uni-router'
const router = createRouter({
routes: [
{ path: 'pages/index/index', name: 'home', meta: { isTab: true } },
{ path: 'pages/about/about', name: 'about', meta: { requireAuth: true } }
]
})
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
next({ name: 'login' }, { mode: 'replace' })
} else {
next()
}
})
场景:完整功能
typescript
import { createRouter, ParamsPlugin, ChannelPlugin, InterceptorPlugin, AnimationPlugin, useRouter, useRoute, usePageChannel } from '@meng-xi/uni-router'
const router = createRouter({
routes,
plugins: [ParamsPlugin, ChannelPlugin, InterceptorPlugin, AnimationPlugin],
interceptUniApi: true, // 需要 InterceptorPlugin
useUniEventChannel: true, // 需要 ChannelPlugin
paramsPersistent: false, // 需要 ParamsPlugin
guardTimeout: 15000
})
// 页面间通信 --- 发起页
const { eventChannel } = await router.push({
path: '/pages/detail/detail',
query: { id: '1' },
events: { receiveData: data => console.log(data) }
})
eventChannel.emit('fromOpener', { msg: '你好,详情页' })
// 页面间通信 --- 目标页
const channel = usePageChannel()
channel.on('fromOpener', data => console.log(data))
channel.emit('receiveData', { result: '处理完成' })
// 参数传递
await router.push({
path: '/pages/detail/detail',
params: { userInfo: { name: 'Tom', age: 20 } },
persistent: true // H5 刷新后仍可读取
})
// 目标页读取参数
const route = useRoute()
console.log(route.value.params.userInfo) // { name: 'Tom', age: 20 }
场景:uni_modules 版本
typescript
import { createRouter, ParamsPlugin, ChannelPlugin, InterceptorPlugin, AnimationPlugin } from './uni_modules/mxuni-router-v2/js_sdk/index.js'
const router = createRouter({
routes,
plugins: [ParamsPlugin, ChannelPlugin, InterceptorPlugin, AnimationPlugin],
interceptUniApi: true
})
升级指南
v2.0.0 包含破坏性变更,需要手动迁移。
必须改动
1. 添加 plugins 数组
typescript
// v1.x --- 功能默认可用
const router = createRouter({ routes, interceptUniApi: true })
// v2.0 --- 需要显式注册插件
const router = createRouter({
routes,
plugins: [ParamsPlugin, ChannelPlugin, InterceptorPlugin, AnimationPlugin],
interceptUniApi: true
})
2. 导入插件
typescript
import { createRouter, ParamsPlugin, ChannelPlugin, InterceptorPlugin, AnimationPlugin } from '@meng-xi/uni-router'
3. usePageChannel() 导入路径
typescript
// v1.x
import { usePageChannel } from '@meng-xi/uni-router'
// v2.0 --- 同样从主入口导入(路径未变)
import { usePageChannel } from '@meng-xi/uni-router'
按需注册对照表
| 你使用的功能 | 需要注册的插件 |
|---|---|
params 参数传递 |
ParamsPlugin |
persistent 参数持久化 |
ParamsPlugin |
events / eventChannel 页面通信 |
ChannelPlugin |
usePageChannel() |
ChannelPlugin |
interceptUniApi: true |
InterceptorPlugin |
animation 导航动画 |
AnimationPlugin |
meta.animation 路由级动画 |
AnimationPlugin |
back(delta, { animation }) |
AnimationPlugin |
兼容性
- 核心 API(
push/replace/relaunch/back/beforeEach/afterEach/useRouter/useRoute)无需插件 - 路由守卫、命名路由、路由元信息无需插件
RouterLink组件无需插件TabBar/TabBarItem组件无需插件- 未注册插件的功能调用将抛出
PLUGIN_REQUIRED错误,而非静默失效