v1.10.0 新增useUniEventChannel选项与UniEventChannel类基于uni.$emit/$on全局事件总线实现内置页面间通信使所有导航方式push/replace/relaunch均支持双向通信配合usePageChannel()组合式 API 与 Sticky 事件缓存彻底解决原生 EventChannel 的平台限制与时序竞态问题前言meng-xi/uni-router在 v1.9.0 之前已经通过uni.navigateTo原生EventChannel支持了 push 导航的页面间通信但存在以下痛点仅 push 支持通信uni.redirectTo/uni.reLaunch不支持EventChannel使用replace/relaunch导航时无法与目标页面通信时序竞态发起页在await router.push()后立即emit事件但目标页setup注册on监听器可能晚于emit执行导致事件丢失H5 刷新丢失原生EventChannel基于内存H5 刷新后通道不复存在API 不统一发起页通过events参数传递监听器目标页通过getOpenerEventChannel()获取通道两套 API 风格差异大v1.10.0 通过内置通信管理器UniEventChannel解决了这些问题基于uni.$emit/$on/$off/$once全局事件总线每次导航生成唯一navigationId隔离事件通道配合 Sticky 缓存解决时序竞态__nav_id通过 URLquery 传递使 H5 刷新后仍可重建通道。一、问题分析1. 原生 EventChannel 的平台限制uni-app 的EventChannel机制仅存在于uni.navigateTo// 仅 push 有 eventChannelconsteventChannelawaituni.navigateTo({url:/pages/detail/index,events:{receiveData:dataconsole.log(data)}})// replace / relaunch 不支持 events 参数awaituni.redirectTo({url:/pages/login/index})// 无法通信awaituni.reLaunch({url:/pages/index/index})// 无法通信实际业务中replace和relaunch同样需要与目标页面通信的场景很常见replace 登录页登录成功后需要将用户信息传回替换前的页面上下文relaunch 首页退出登录后跳转首页需要传递退出原因replace 详情页从 A 详情页跳转到 B 详情页时替换栈需要传递来源信息2. 时序竞态// 发起页constresultawaitrouter.push({path:/pages/detail/index})result.eventChannel.emit(data,{id:123})// 何时执行// 目标页 setupconstchannelgetOpenerEventChannel()channel.on(data,payload{/* 能收到吗 */})问题在于emit在发起页的微任务中执行而目标页的setup执行时机取决于平台和页面栈状态——如果emit先于on注册执行事件就丢失了。3. API 风格不统一角色API说明发起页events: { eventName: callback }通过导航参数传递监听器目标页getOpenerEventChannel()Options API 风格获取通道在组合式 API 中getOpenerEventChannel()不符合useXxx命名规范也无法在onUnmounted中自动清理。二、新增能力1. 内置通信管理器useUniEventChannel启用方式import{createRouter}frommeng-xi/uni-routerconstroutercreateRouter({routes,useUniEventChannel:true// 启用内置通信管理器})启用后所有导航方式push/replace/relaunch均返回NavigationResult包含eventChannel// pushconstpushResultawaitrouter.push({path:/pages/detail/index})pushResult.eventChannel.emit(fromSender,{msg:hello})// replaceconstreplaceResultawaitrouter.replace({path:/pages/login/index})replaceResult.eventChannel.emit(logoutReason,{reason:expired})// relaunchconstrelaunchResultawaitrouter.relaunch({path:/pages/index/index})relaunchResult.eventChannel.emit(resetInfo,{from:logout})UniEventChannel 实现UniEventChannel基于uni.$emit/$on/$off/$once全局事件总线通过navigationId隔离事件通道// 事件名格式uni-router:navId:eventName// 例如uni-router:nav-1720519200000-1:data每次导航生成唯一的navigationId格式nav-timestamp-seq确保不同导航之间的事件不会串扰。通道生命周期发起页调用 push/replace/relaunch ├─ generateNavId() 生成唯一 ID ├─ new UniEventChannel(navId) 创建通道 ├─ registerChannel(navId, channel) 注册到全局 registry ├─ enrichLocationWithNavId() 将 __nav_id 注入 query ├─ matcher.resolve() 解析路由从 query 中移除 __nav_id写入 params.__navId └─ 实际导航 URL 保留 __nav_id类似 __params_key 机制 目标页 onShow → syncCurrentRoute → 从 URL 读取 __nav_id → 重建 params.__navId 目标页 setup → usePageChannel() → 读取 params.__navId → getOrCreateChannel(navId) 目标页 onUnmounted → destroyChannel(navId) → channel.destroy() → 清理监听器与缓存2. Sticky 事件缓存UniEventChannel内置粘性事件缓存机制解决 emit/on 的时序竞态// UniEventChannel 核心逻辑简化classUniEventChannelimplementsEventChannel{privatependingEvents:Mapstring,any[]newMap()emit(event:string,...args:any[]):EventChannel{// 总是更新缓存使后续注册的 on/once 都能收到最后一次 emit 的数据this.pendingEvents.set(event,args)// 有监听器时同时通过 uni.$emit 触发if(this.listeners.get(event)?.size0){uni.$emit(wrapEventName(this.navId,event),...args)}returnthis}on(event:string,callback:Function):EventChannel{uni.$on(wrapEventName(this.navId,event),callback)// 有缓存时异步触发保留缓存使后续注册的 once 也能收到constpendingthis.pendingEvents.get(event)if(pending){Promise.resolve().then(()callback(...pending))}returnthis}}关键设计emit始终缓存无论是否有监听器都保存最后一次emit的参数on/once异步触发缓存注册时若有缓存通过Promise.resolve().then()微任务异步触发缓存不删除保留缓存确保后续注册的once也能收到直到destroy()时统一清理// 发起页导航后立即 emit不担心目标页还没注册监听器constresultawaitrouter.push({path:/pages/detail/index})result.eventChannel.emit(data,{id:123})// 目标页setup 中注册监听即使晚于 emit 也能收到缓存事件constchannelusePageChannel()channel.on(data,payload{console.log(payload)// { id: 123 } — 从缓存触发})3. usePageChannel() 组合式 API目标页面通过usePageChannel()获取通信通道替代原来的getOpenerEventChannel()import{usePageChannel,noopChannel}frommeng-xi/uni-routerexportdefault{setup(){constchannelusePageChannel()// 判断是否有可用通道consthasChannelchannel!noopChannel// 监听发起页事件channel.on(data,payload{console.log(收到数据:,payload)})// 向发起页发送事件channel.emit(ready,{status:ok})return{channel,hasChannel}}}noopChannel 安全降级当页面不是通过带__nav_id的导航打开时如直接从浏览器访问usePageChannel()返回noopChannel——所有方法均为空操作并返回自身调用方无需判空exportconstnoopChannel:EventChannel{on:()noopChannel,once:()noopChannel,off:()noopChannel,emit:()noopChannel}自动清理usePageChannel()在onUnmounted()时自动调用destroyChannel(navId)清理该通道的所有监听器和缓存防止内存泄漏exportfunctionusePageChannel():EventChannel{constrouteruseRouter()constroutegetReactiveRoute(router)constnavIdroute.value.params?.__navIdasstring|undefinedif(!navId)returnnoopChannelconstchannelgetOrCreateChannel(navId)onUnmounted(()destroyChannel(navId))// 自动清理returnchannel}4. NavigationResult 返回类型push/replace/relaunch的返回值从RouteLocation扩展为NavigationResultinterfaceNavigationResultextendsRouteLocation{eventChannel?:EventChannel}导航方式默认模式useUniEventChannel: falseuseUniEventChannel: truepusheventChannel为原生EventChanneleventChannel为UniEventChannelreplaceeventChannel为undefinedeventChannel为UniEventChannelrelauncheventChannel为undefinedeventChannel为UniEventChannel类型向后兼容NavigationResult extends RouteLocation原有的const route: RouteLocation await router.push(...)仍可用。5. RouterLink navigated 事件扩展配合NavigationResultRouterLink的navigated事件现对所有导航方式统一触发并传递eventChannel!-- 发起页模板 --RouterLinkto/pages/about/indexnavigatedonNavigated跳转到关于页/RouterLink// 无论 push / replace / relaunch都能通过 navigated 获取 channelonNavigated(eventChannel){if(eventChannel){eventChannel.on(receiveData,(data)console.log(data))eventChannel.emit(fromOpener,{msg:来自 RouterLink})}}三、架构设计通道注册表新增channel/registry.ts管理navId → UniEventChannel映射constchannelRegistry:Mapstring,UniEventChannelnewMap()exportfunctionregisterChannel(navId:string,channel:UniEventChannel):boolean{if(channelRegistry.has(navId))returnfalse// first-wins 策略channelRegistry.set(navId,channel)returntrue}exportfunctiongetOrCreateChannel(navId:string):UniEventChannel{constexistingchannelRegistry.get(navId)if(existing)returnexistingconstchannelnewUniEventChannel(navId)channelRegistry.set(navId,channel)returnchannel}exportfunctiondestroyChannel(navId:string):void{constchannelchannelRegistry.get(navId)if(channel){channel.destroy()channelRegistry.delete(navId)}}__nav_id 注入与提取类似__params_key机制__nav_id通过 URL query 在导航链路中传递enrichLocationWithNavId(location, navId) ← 注入到 query ↓ matcher.resolve(enrichedLocation) ← 从 query 移除写入 params.__navId ↓ extractNavId(enrichedLocation) ← 从 enrichedLocation 提取 ↓ 实际导航 URL 保留 __nav_id ← 目标页面可从 URL 读取 ↓ syncCurrentRoute → 读取 __nav_id → params.__navId ← 重建通道新增文件src/channel/uni-event-channel.ts—UniEventChannel类、generateNavId、wrapEventName、noopChannel、NAV_ID_KEYsrc/channel/registry.ts— 通道注册表src/channel/index.ts— 统一导出新增导出// src/index.ts 新增export{usePageChannel}from/composablesexport{UniEventChannel,noopChannel}from/channel四、完整使用示例场景登录后传递用户信息// 发起页replace 到登录页传递退出原因constresultawaitrouter.replace({path:/pages/login/index})result.eventChannel?.on(loginSuccess,user{console.log(登录成功:,user)})// 登录页登录成功后发送用户信息import{usePageChannel}frommeng-xi/uni-routerexportdefault{setup(){constchannelusePageChannel()asyncfunctionhandleLogin(){constuserawaitlogin()channel.emit(loginSuccess,user)}return{handleLogin}}}场景RouterLink navigated 事件RouterLinkto/pages/about/indexreplacenavigatedonNavigated关于页/RouterLinkonNavigated(eventChannel){if(eventChannel){eventChannel.on(replyData,(data){this.log收到回复:${JSON.stringify(data)}})eventChannel.emit(fromOpener,{msg:来自首页})}}升级指南v1.10.0 是向后兼容版本绝大多数项目无需修改代码即可升级。行为变化场景v1.9.0v1.10.0push返回类型RouteLocationNavigationResult extends RouteLocationreplace返回类型RouteLocationNavigationResult含eventChannelrelaunch返回类型RouteLocationNavigationResult含eventChannelreplace/relaunch的events参数忽略 警告useUniEventChannel: true时注册到内置通道RouterLinknavigated仅push触发所有导航方式触发原生 EventChannel默认使用useUniEventChannel: true时替代为UniEventChannel启用内置通信// 方式一createRouter 选项constroutercreateRouter({routes,useUniEventChannel:true})// 方式二渐进式——默认关闭按需开启// 不传 useUniEventChannel保持 v1.9.0 行为不变迁移建议启用useUniEventChannel后推荐将目标页的getOpenerEventChannel()替换为usePageChannel()// 目标页 import { usePageChannel, noopChannel } from meng-xi/uni-router export default { - onLoad() { - const channel getOpenerEventChannel() - channel.on(data, (payload) { ... }) - } setup() { const channel usePageChannel() channel.on(data, (payload) { ... }) return { channel } } }发起页的events参数可改为result.eventChannel.on()模式// 发起页 -const result await router.push({ - path: /pages/detail/index, - events: { receiveData: (data) console.log(data) } -}) const result await router.push({ path: /pages/detail/index }) result.eventChannel?.on(receiveData, (data) console.log(data)) result.eventChannel?.emit(fromOpener, { msg: hello })新增导出usePageChannel— 组合式 API获取当前页面通信通道UniEventChannel— 内置通信通道类noopChannel— 空操作通道常量新增类型NavigationResult— 导航结果类型extends RouteLocation新增eventChannel?: EventChannel兼容性默认模式useUniEventChannel: false行为与 v1.9.0 完全一致NavigationResult extends RouteLocation原有类型声明无需修改events参数在默认模式下仍仅 push 生效