uni-router v1.10 Ships a Global Event Bus That Gives Every Navigation Method a Bidirectional Channel
v1.10.0 adds the
useUniEventChanneloption and theUniEventChannelclass, implementing built-in inter-page communication based on theuni.$emit/$onglobal event bus, enabling bidirectional communication for all navigation methods (push/replace/relaunch). Combined with theusePageChannel()composable API and Sticky event caching, it completely resolves the platform limitations and timing race conditions of the native EventChannel.
Foreword
Before v1.9.0, @meng-xi/uni-router already supported inter-page communication for push navigation via the native uni.navigateTo EventChannel, but had the following pain points:
- Only push supports communication:
uni.redirectTo/uni.reLaunchdo not supportEventChannel, so communication with the target page is impossible when usingreplace/relaunchnavigation. - Timing race condition: The initiating page calls
emitimmediately afterawait router.push(), but the target page'ssetupregistration of theonlistener may execute later thanemit, causing event loss. - H5 refresh loss: The native
EventChannelis memory-based; the channel ceases to exist after an H5 refresh. - Inconsistent API: The initiating page passes listeners via the
eventsparameter, while the target page obtains the channel viagetOpenerEventChannel(). The two API styles differ significantly.
v1.10.0 solves these problems through a built-in communication manager (UniEventChannel): based on the uni.$emit/$on/$off/$once global event bus, a unique navigationId is generated for each navigation to isolate event channels. Combined with Sticky caching to resolve timing race conditions, __nav_id is passed via URL query so the channel can be rebuilt even after an H5 refresh.
1. Problem Analysis
1. Platform Limitations of Native EventChannel
uni-app's EventChannel mechanism only exists in uni.navigateTo:
// Only push has eventChannel
const eventChannel = await uni.navigateTo({
url: '/pages/detail/index',
events: { receiveData: data => console.log(data) }
})
// replace / relaunch do not support the events parameter
await uni.redirectTo({ url: '/pages/login/index' }) // Cannot communicate
await uni.reLaunch({ url: '/pages/index/index' }) // Cannot communicate
In actual business, scenarios where replace and relaunch also need to communicate with the target page are common:
- replace login page: After a successful login, user information needs to be passed back to the context of the page before replacement.
- relaunch homepage: After logging out and jumping to the homepage, the logout reason needs to be passed.
- replace detail page: When jumping from detail page A to detail page B and replacing the stack, source information needs to be passed.
2. Timing Race Condition
// Initiating page
const result = await router.push({ path: '/pages/detail/index' })
result.eventChannel.emit('data', { id: 123 }) // When does this execute?
// Target page setup
const channel = getOpenerEventChannel()
channel.on('data', payload => {
/* Can it be received? */
})
The problem is: emit executes in the microtask of the initiating page, while the execution timing of the target page's setup depends on the platform and page stack state—if emit executes before the on registration, the event is lost.
3. Inconsistent API Style
| Role | API | Description |
|---|---|---|
| Initiating Page | events: { eventName: callback } |
Pass listeners via navigation parameters |
| Target Page | getOpenerEventChannel() |
Obtain channel in Options API style |
In the Composition API, getOpenerEventChannel() does not conform to the useXxx naming convention and cannot be automatically cleaned up in onUnmounted.
2. New Capabilities
1. Built-in Communication Manager (useUniEventChannel)
How to Enable
import { createRouter } from '@meng-xi/uni-router'
const router = createRouter({
routes,
useUniEventChannel: true // Enable built-in communication manager
})
Once enabled, all navigation methods (push/replace/relaunch) return a NavigationResult containing eventChannel:
// push
const pushResult = await router.push({ path: '/pages/detail/index' })
pushResult.eventChannel.emit('fromSender', { msg: 'hello' })
// replace
const replaceResult = await router.replace({ path: '/pages/login/index' })
replaceResult.eventChannel.emit('logoutReason', { reason: 'expired' })
// relaunch
const relaunchResult = await router.relaunch({ path: '/pages/index/index' })
relaunchResult.eventChannel.emit('resetInfo', { from: 'logout' })
UniEventChannel Implementation
UniEventChannel is based on the uni.$emit/$on/$off/$once global event bus, isolating event channels via navigationId:
// Event name format: uni-router:<navId>:<eventName>
// Example: uni-router:nav-1720519200000-1:data
A unique navigationId (format nav-<timestamp>-<seq>) is generated for each navigation, ensuring events between different navigations do not interfere.
Channel Lifecycle
Initiating page calls push/replace/relaunch
├─ generateNavId() generates unique ID
├─ new UniEventChannel(navId) creates channel
├─ registerChannel(navId, channel) registers to global registry
├─ enrichLocationWithNavId() injects __nav_id into query
├─ matcher.resolve() resolves route (removes __nav_id from query, writes to params.__navId)
└─ Actual navigation URL retains __nav_id (similar to __params_key mechanism)
Target page onShow → syncCurrentRoute → reads __nav_id from URL → rebuilds params.__navId
Target page setup → usePageChannel() → reads params.__navId → getOrCreateChannel(navId)
Target page onUnmounted → destroyChannel(navId) → channel.destroy() → cleans up listeners and cache
2. Sticky Event Caching
UniEventChannel has a built-in sticky event caching mechanism to resolve the emit/on timing race condition:
// UniEventChannel core logic (simplified)
class UniEventChannel implements EventChannel {
private pendingEvents: Map<string, any[]> = new Map()
emit(event: string, ...args: any[]): EventChannel {
// Always update cache so subsequent on/once registrations receive the last emitted data
this.pendingEvents.set(event, args)
// If listeners exist, also trigger via uni.$emit
if (this.listeners.get(event)?.size > 0) {
uni.$emit(wrapEventName(this.navId, event), ...args)
}
return this
}
on(event: string, callback: Function): EventChannel {
uni.$on(wrapEventName(this.navId, event), callback)
// If cache exists, trigger asynchronously (retain cache so subsequent once registrations also receive it)
const pending = this.pendingEvents.get(event)
if (pending) {
Promise.resolve().then(() => callback(...pending))
}
return this
}
}
Key design points:
emitalways caches: Regardless of whether listeners exist, the lastemitparameters are saved.on/oncetriggers cache asynchronously: If a cache exists upon registration, it triggers asynchronously via aPromise.resolve().then()microtask.- Cache is not deleted: Retaining the cache ensures subsequent
onceregistrations also receive it, untildestroy()cleans up uniformly.
// Initiating page: emit immediately after navigation, no worry about target page not yet registering listener
const result = await router.push({ path: '/pages/detail/index' })
result.eventChannel.emit('data', { id: 123 })
// Target page: register listener in setup, receives cached event even if later than emit
const channel = usePageChannel()
channel.on('data', payload => {
console.log(payload) // { id: 123 } — triggered from cache
})
3. usePageChannel() Composable API
The target page obtains the communication channel via usePageChannel(), replacing the original getOpenerEventChannel():
import { usePageChannel, noopChannel } from '@meng-xi/uni-router'
export default {
setup() {
const channel = usePageChannel()
// Check if a usable channel exists
const hasChannel = channel !== noopChannel
// Listen for events from the initiating page
channel.on('data', payload => {
console.log('Received data:', payload)
})
// Send events to the initiating page
channel.emit('ready', { status: 'ok' })
return { channel, hasChannel }
}
}
noopChannel Safe Degradation
When a page is not opened via navigation with __nav_id (e.g., accessed directly from a browser), usePageChannel() returns noopChannel—all methods are no-ops and return itself, so the caller does not need null checks:
export const noopChannel: EventChannel = {
on: () => noopChannel,
once: () => noopChannel,
off: () => noopChannel,
emit: () => noopChannel
}
Automatic Cleanup
usePageChannel() automatically calls destroyChannel(navId) in onUnmounted(), cleaning up all listeners and caches for that channel to prevent memory leaks:
export function usePageChannel(): EventChannel {
const router = useRouter()
const route = getReactiveRoute(router)
const navId = route.value.params?.__navId as string | undefined
if (!navId) return noopChannel
const channel = getOrCreateChannel(navId)
onUnmounted(() => destroyChannel(navId)) // Automatic cleanup
return channel
}
4. NavigationResult Return Type
The return value of push / replace / relaunch is extended from RouteLocation to NavigationResult:
interface NavigationResult extends RouteLocation {
eventChannel?: EventChannel
}
| Navigation Method | Default Mode (useUniEventChannel: false) | useUniEventChannel: true |
|---|---|---|
push |
eventChannel is native EventChannel |
eventChannel is UniEventChannel |
replace |
eventChannel is undefined |
eventChannel is UniEventChannel |
relaunch |
eventChannel is undefined |
eventChannel is UniEventChannel |
The type is backward compatible: NavigationResult extends RouteLocation, so the original const route: RouteLocation = await router.push(...) still works.
5. RouterLink navigated Event Extension
In conjunction with NavigationResult, the RouterLink navigated event now triggers uniformly for all navigation methods and passes eventChannel:
<!-- Initiating page template -->
<RouterLink to="/pages/about/index" @navigated="onNavigated"> Go to About Page </RouterLink>
// Whether push / replace / relaunch, channel can be obtained via navigated
onNavigated(eventChannel) {
if (eventChannel) {
eventChannel.on('receiveData', (data) => console.log(data))
eventChannel.emit('fromOpener', { msg: 'From RouterLink' })
}
}
3. Architecture Design
Channel Registry
Added channel/registry.ts, managing the navId → UniEventChannel mapping:
const channelRegistry: Map<string, UniEventChannel> = new Map()
export function registerChannel(navId: string, channel: UniEventChannel): boolean {
if (channelRegistry.has(navId)) return false // first-wins strategy
channelRegistry.set(navId, channel)
return true
}
export function getOrCreateChannel(navId: string): UniEventChannel {
const existing = channelRegistry.get(navId)
if (existing) return existing
const channel = new UniEventChannel(navId)
channelRegistry.set(navId, channel)
return channel
}
export function destroyChannel(navId: string): void {
const channel = channelRegistry.get(navId)
if (channel) {
channel.destroy()
channelRegistry.delete(navId)
}
}
__nav_id Injection and Extraction
Similar to the __params_key mechanism, __nav_id is passed through the navigation chain via URL query:
enrichLocationWithNavId(location, navId) ← Injects into query
↓
matcher.resolve(enrichedLocation) ← Removes from query, writes to params.__navId
↓
extractNavId(enrichedLocation) ← Extracts from enrichedLocation
↓
Actual navigation URL retains __nav_id ← Target page can read from URL
↓
syncCurrentRoute → reads __nav_id → params.__navId ← Rebuilds channel
New Files
src/channel/uni-event-channel.ts—UniEventChannelclass,generateNavId,wrapEventName,noopChannel,NAV_ID_KEYsrc/channel/registry.ts— Channel registrysrc/channel/index.ts— Unified exports
New Exports
// Added in src/index.ts
export { usePageChannel } from '@/composables'
export { UniEventChannel, noopChannel } from '@/channel'
4. Complete Usage Examples
Scenario: Passing User Information After Login
// Initiating page: replace to login page, pass logout reason
const result = await router.replace({ path: '/pages/login/index' })
result.eventChannel?.on('loginSuccess', user => {
console.log('Login successful:', user)
})
// Login page: send user information after successful login
import { usePageChannel } from '@meng-xi/uni-router'
export default {
setup() {
const channel = usePageChannel()
async function handleLogin() {
const user = await login()
channel.emit('loginSuccess', user)
}
return { handleLogin }
}
}
Scenario: RouterLink + navigated Event
<RouterLink to="/pages/about/index" replace @navigated="onNavigated"> About Page </RouterLink>
onNavigated(eventChannel) {
if (eventChannel) {
eventChannel.on('replyData', (data) => {
this.log = `Received reply: ${JSON.stringify(data)}`
})
eventChannel.emit('fromOpener', { msg: 'From homepage' })
}
}
Upgrade Guide
v1.10.0 is a backward compatible version; most projects can upgrade without code changes.
Behavioral Changes
| Scenario | v1.9.0 | v1.10.0 |
|---|---|---|
push return type |
RouteLocation |
NavigationResult extends RouteLocation |
replace return type |
RouteLocation |
NavigationResult (includes eventChannel) |
relaunch return type |
RouteLocation |
NavigationResult (includes eventChannel) |
events parameter for replace/relaunch |
Ignored + warning | Registered to built-in channel when useUniEventChannel: true |
RouterLink @navigated |
Only push triggers |
Triggers for all navigation methods |
| Native EventChannel | Used by default | Replaced by UniEventChannel when useUniEventChannel: true |
Enabling Built-in Communication
// Method 1: createRouter option
const router = createRouter({
routes,
useUniEventChannel: true
})
// Method 2: Gradual — disabled by default, enable as needed
// Do not pass useUniEventChannel to keep v1.9.0 behavior unchanged
Migration Suggestions
After enabling useUniEventChannel, it is recommended to replace the target page's getOpenerEventChannel() with usePageChannel():
// Target page
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 }
+ }
}
The initiating page's events parameter can be changed to the result.eventChannel.on() pattern:
// Initiating page
-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' })
New Exports
usePageChannel— Composable API, obtains the current page's communication channelUniEventChannel— Built-in communication channel classnoopChannel— No-operation channel constant
New Types
NavigationResult— Navigation result type (extends RouteLocation, addseventChannel?: EventChannel)
Compatibility
- Default mode (
useUniEventChannel: false) behavior is completely consistent with v1.9.0 NavigationResult extends RouteLocation, existing type declarations require no modification- The
eventsparameter still only works for push in default mode
Top 3 of 5 from juejin.cn, machine-translated. The original thread is authoritative.
More inclined toward the Flutter approach, const result = await navigator.push()
result can then receive the returned parameters
There's no way around it, the uni-app official docs don't support that kind of API [facepalm]
Big shot, can I add you on WeChat?
[Thumbs up][Thumbs up][Thumbs up]