跪拜 Guibai
← Back to the summary

uni-router v1.10 Ships a Global Event Bus That Gives Every Navigation Method a Bidirectional Channel

v1.10.0 adds the useUniEventChannel option and the UniEventChannel class, implementing built-in inter-page communication based on the uni.$emit/$on global event bus, enabling bidirectional communication for all navigation methods (push/replace/relaunch). Combined with the usePageChannel() 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:

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:

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:

// 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

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

New Types

Compatibility

Comments

Top 3 of 5 from juejin.cn, machine-translated. The original thread is authoritative.

似水流年146

More inclined toward the Flutter approach, const result = await navigator.push()

似水流年146

result can then receive the returned parameters

PedroQue99  → 似水流年146

There's no way around it, the uni-app official docs don't support that kind of API [facepalm]

Undefined94515

Big shot, can I add you on WeChat?

Undefined94515

[Thumbs up][Thumbs up][Thumbs up]