uni-router v2.0.0:四大功能拆分为插件,按需加载

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
})

即使不使用 paramseventsanimation,相关代码仍被包含。随着功能增加,核心包持续膨胀。

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 错误,而非静默失效
相关推荐
Lazy_zheng1 小时前
TDD 实战:Claude Code Superpowers 保姆级教程14 个 Skills 全解析 + 实战开发
前端·claude·vibecoding
胡萝卜术1 小时前
从逐一合并到多路归并:力扣23「合并K个升序链表」的三种解法进化之路
前端·javascript·面试
名字还没想好☜1 小时前
React 受控与非受控组件:表单场景怎么选
前端·javascript·react.js·react·表单·受控组件
Damai1 小时前
Radix UI导致右侧白条
前端
weedsfly1 小时前
前端开发中的策略模式——告别 if-else 的优雅之道
前端·javascript·面试
不坑老师1 小时前
怎么在Word中制作汉字书写笔顺——不坑盒子汉字笔顺步骤在小学低段识字教学中的应用
前端·数据库·人工智能·word·excel·office
anyup1 小时前
30 分钟用 Trae Work + 魔珐星云做出会讲故事的 AI 3D 具身交互智能数字人
前端·ai编程·trae
IT_陈寒1 小时前
Vue的响应式竟然在这常见操作下失效了
前端·人工智能·后端
你怎么知道我是队长1 小时前
JavaScript 事件介绍
开发语言·前端·javascript