Uniapp 鸿蒙实战:路由与导航体系从设计到落地

一、我们要做什么
1.1 路由需求清单
在 AtomGit 开源社区 App(UniApp + 鸿蒙)项目中,我们需要一套统一、可扩展的导航解决方案,核心需求如下:
| 需求编号 | 需求描述 | 优先级 |
|---|---|---|
| R-01 | 支持 navigateTo、redirectTo、reLaunch、switchTab、navigateBack 五种基本导航 |
P0 |
| R-02 | 所有导航操作必须 Promise 化,支持 async/await 语法 |
P0 |
| R-03 | 路由守卫:每次跳转前鉴权 Token,未登录时自动拦截并跳转登录页 | P0 |
| R-04 | 路由守卫支持白名单配置(登录页、首页等无需鉴权) | P0 |
| R-05 | tabBar 页面必须使用 switchTab,禁止用 navigateTo 打开 |
P0 |
| R-06 | 参数序列化与反序列化,支持对象参数和 URL 参数混合传递 | P1 |
| R-07 | 全局路由监听,可在路由变化时执行日志上报、埋点等副作用 | P1 |
| R-08 | 鸿蒙端兼容:在鸿蒙宿主环境中,拦截特殊协议跳转到原生 Ability | P2 |
| R-09 | 页面栈深度限制兼容,navigateBack 支持自动计算超出深度的回退策略 |
P2 |
1.2 预期效果
开发者调用路由时,感受到的是一套干净简洁的 API:
// 跳转到详情页(带鉴权守卫)
await Router.push('/pages/repo/detail', { repoId: 'vue', name: 'Vue Core' })
// 跳转到 tabBar 首页
await Router.switchTab('/pages/index/index')
// 带参数返回上一页
await Router.back({ selectedId: 42 })
// 全局守卫打印日志
Router.beforeEach((to, from) => console.log(`导航: ${from} -> ${to}`))
1.3 核心挑战
- 多端一致性:UniApp 在不同平台(iOS、Android、鸿蒙)上路由行为存在细微差异,特别是鸿蒙端 WebView 与原生 Ability 跳转的衔接。
- 守卫链设计:路由守卫需同时支持同步和异步鉴权,不能阻塞用户操作的即时响应体验。
- tabBar 约束:
switchTab不能携带参数(小程序平台限制),需要全局状态共享方案。 - 栈深度限制:
navigateTo在部分平台有上限(默认 10 层),超出时需给出明确错误或自动 fallback 到reLaunch。
二、数据模型设计
2.1 路由元信息定义
首先用 TypeScript interface 定义路由系统的完整数据类型:
// router/types.ts
/** 路由参数:支持字符串路径和命名的路由 */
export type RoutePath = string
/** 导航参数对象,会被 Query String 序列化 */
export interface RouteParams extends Record<string, unknown> {
// 例如 { repoId: 'vue', page: 1 }
}
/** 路由跳转方式枚举 */
export type NavigateType =
| 'navigateTo' // 推入页面栈
| 'redirectTo' // 替换当前页
| 'reLaunch' // 关闭所有页面重新打开
| 'switchTab' // 跳转 tabBar 页面
| 'navigateBack' // 返回页面栈
/** 单条路由记录 */
export interface RouteRecord {
path: RoutePath
params?: RouteParams
}
/** 路由跳转时的完整上下文 */
export interface RouteContext {
/** 目标路径(不含 Query String) */
path: RoutePath
/** 解析后的参数对象 */
params: RouteParams
/** 本次导航的 API 类型 */
navigateType: NavigateType
/** 来源页面路径 */
from: RoutePath
/** 页面栈当前深度(仅 navigateTo/navigateBack 有意义) */
stackDepth?: number
}
/** 路由守卫函数签名 */
export type NavigationGuard = (
to: RouteContext,
from: RouteContext
) => boolean | void | Promise<boolean | void>
/** 路由元信息附加标签 */
export interface RouteMeta {
/** 是否需要登录 */
requiresAuth?: boolean
/** tabBar 关联的 index */
tabBarIndex?: number
/** 页面标题(用于鸿蒙原生导航栏) */
title?: string
/** 是否禁用返回手势 */
disableBack?: boolean
/** 自定义元数据,可扩展 */
[key: string]: unknown
}
/** 全局路由配置 */
export interface RouterConfig {
/** 应用所有路由的元信息映射表 */
routes: Record<RoutePath, RouteMeta>
/** 登录页路径,白名单路由不会触发守卫 */
loginPage: RoutePath
/** 首页路径(通常也是 tabBar 首页) */
homePage: RoutePath
/** 页面栈最大深度,超出后 navigateTo 报错 */
maxStackDepth?: number
/** 是否开启调试日志 */
debug?: boolean
}
2.2 守卫拦截上下文
// router/types.ts 补充
/** 守卫执行结果 */
export interface GuardResult {
/** true = 允许跳转,false = 拦截 */
allowed: boolean
/** 拦截时的目标路径(通常为登录页) */
redirectTo?: RoutePath
/** 拦截原因描述 */
reason?: string
}
/** 全局路由监听器 */
export type RouteListener = (to: RouteContext, from: RouteContext) => void
2.3 核心设计原则
数据类型设计遵循三个原则:
- 完备性:覆盖所有导航 API 的语义,包括
navigateBack的 delta 参数场景。 - 可扩展性:
RouteMeta使用[key: string]: unknown开放字段,允许业务层随时扩展(如keepAlive、cacheMax)。 - 类型安全:所有路由跳转必须携带显式路径和参数,编译期可检查参数格式。
三、核心设计决策
3.1 声明式路由 vs 命令式路由
业界存在两种主流路由风格:
| 维度 | 声明式路由(Vue Router 风格) | 命令式路由(UniApp 原生风格) |
|---|---|---|
| 使用方式 | 配置路由表,模板中使用 <router-link> |
直接调用 uni.navigateTo() API |
| 优势 | 路由集中管理、类型推断强、导航守卫清晰 | 与 UniApp 深度集成、支持所有平台 |
| 劣势 | 需引入额外层、UniApp 原生页面(非 Vue)兼容性差 | 参数传递分散、守卫逻辑散落各处 |
| 适用场景 | 纯 Vue 页面应用 | UniApp 跨平台混合 App(本文选型) |
选型结论:采用命令式路由封装,在 UniApp 原生 API 之上构建薄封装层。原因:UniApp 的页面是原生页面而非 Vue 组件,uni.navigateTo 等 API 底层调用的页面栈管理机制不可绕过,强行套用 Vue Router 风格会带来不必要的适配复杂度。
3.2 拦截器设计方案对比
路由守卫是拦截器的核心应用场景,有三种实现路径:
| 方案 | 实现思路 | 优点 | 缺点 |
|---|---|---|---|
| 方案 A:中间件管道 | beforeEach 链式调用守卫函数 |
符合直觉、可组合、职责清晰 | 需要维护守卫队列 |
| 方案 B:事件总线 | 全局 EventBus 触发 route:before 事件 |
解耦彻底 | 事件回调无序、难以链式中断 |
| 方案 C:单例 Guard | 每次导航前同步调用 Router.guard() |
最简单 | 无法支持多个守卫、扩展性差 |
选型结论:方案 A——中间件管道模式。每个守卫函数接收 to、from 两个上下文,返回 boolean(true = 放行,false = 拦截)。支持 Promise 异步守卫,守卫队列按注册顺序线性执行,遇到拦截立即中断。
3.3 参数传递策略
UniApp 中 navigateTo 携带参数有两种方式:
| 方式 | 示例 | 限制 |
|---|---|---|
| Query String | uni.navigateTo({ url: '/pages/detail?repoId=vue' }) |
值自动 toString(),复杂对象需序列化 |
| 全局状态 | 通过 pinia / vuex 全局状态传递 |
需要状态管理器依赖,tabBar 页面均可访问 |
选型结论:优先使用 Query String 序列化(JSON.stringify 后 encodeURIComponent),在目标页通过 onLoad(query) 接收。对于需要跨多个页面的会话级数据(如临时草稿),辅助使用 pinia 持久化状态。全局状态作为 Query String 的补充而非替代。
3.4 tabBar 兼容性设计
switchTab 的核心限制:不允许在 URL 中传递参数(微信小程序平台限制,UniApp 跨平台统一遵循此约束)。解决方案:
// 维护一个跨页面共享的"导航参数槽位"(基于 Pinia)
// 当 switchTab 目标页 onLoad 时,主动从该槽位读取并清空
export const tabBarParamStore = defineStore('tabBarParams', {
state: () => ({ pending: null as RouteParams | null }),
actions: {
setPending(params: RouteParams) { this.pending = params },
consume() { const p = this.pending; this.pending = null; return p }
}
})
四、完整代码实现
4.1 路由类型与配置
// router/types.ts(精简版)
export type NavigateType = 'navigateTo' | 'redirectTo' | 'reLaunch' | 'switchTab' | 'navigateBack'
export interface RouteContext {
path: string
params: Record<string, unknown>
navigateType: NavigateType
from: string
stackDepth?: number
}
export type NavigationGuard = (
to: RouteContext,
from: RouteContext
) => boolean | void | Promise<boolean | void>
export interface RouterConfig {
routes: Record<string, { requiresAuth?: boolean; title?: string; tabBarIndex?: number }>
loginPage: string
homePage: string
maxStackDepth?: number
debug?: boolean
}
4.2 核心路由类封装
// router/index.ts
import type {
RouteParams, RouteContext, NavigationGuard,
RouterConfig, RouteListener, NavigateType
} from './types'
import { tabBarParamStore } from './tabBarStore'
class Router {
private config: RouterConfig
private guards: NavigationGuard[] = []
private listeners: RouteListener[] = []
// 缓存当前页面路径(跨端兼容)
private currentPath: string = ''
constructor(config: RouterConfig) {
this.config = {
maxStackDepth: 10,
debug: false,
...config
}
}
/** 注册前置守卫 */
beforeEach(guard: NavigationGuard) {
this.guards.push(guard)
return this
}
/** 注册路由监听器 */
afterEach(listener: RouteListener) {
this.listeners.push(listener)
return this
}
/** 构建带 Query String 的 URL */
private buildUrl(path: string, params?: RouteParams): string {
if (!params || Object.keys(params).length === 0) return path
const qs = Object.entries(params)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(JSON.stringify(v))}`)
.join('&')
return `${path}?${qs}`
}
/** 解析 Query String 为参数对象 */
parseQuery(query: Record<string, string>): RouteParams {
const params: RouteParams = {}
for (const [k, v] of Object.entries(query)) {
try { params[k] = JSON.parse(decodeURIComponent(v)) }
catch { params[k] = decodeURIComponent(v) }
}
return params
}
/** 执行守卫链 */
private async runGuards(to: RouteContext, from: RouteContext): Promise<boolean> {
for (const guard of this.guards) {
const result = await guard(to, from)
if (result === false) return false
}
return true
}
/** 推送新页面(navigateTo) */
async push(path: string, params?: RouteParams): Promise<void> {
const to: RouteContext = {
path, params: params || {},
navigateType: 'navigateTo', from: this.currentPath
}
if (!(await this.runGuards(to, { ...to, path: this.currentPath, navigateType: 'navigateBack' }))) {
return this.push(this.config.loginPage)
}
const url = this.buildUrl(path, params)
if (this.config.debug) console.log(`[Router] navigateTo: ${url}`)
await new Promise<void>((res, rej) => {
uni.navigateTo({ url, success: () => res(), fail: (e) => rej(e) })
})
this.currentPath = path
this.notifyListeners(to, { ...to, path: this.currentPath })
}
/** 切换 tabBar 页面 */
async switchTab(path: string, params?: RouteParams): Promise<void> {
if (params) tabBarParamStore.setPending(params)
await new Promise<void>((res, rej) => {
uni.switchTab({ url: path, success: () => res(), fail: (e) => rej(e) })
})
}
/** 返回页面栈 */
async back(delta: number = 1, params?: RouteParams): Promise<void> {
if (params) tabBarParamStore.setPending(params)
await new Promise<void>((res, rej) => {
uni.navigateBack({ delta, success: () => res(), fail: (e) => rej(e) })
})
}
/** 重定向 */
async replace(path: string, params?: RouteParams): Promise<void> {
await new Promise<void>((res, rej) => {
uni.redirectTo({ url: this.buildUrl(path, params), success: () => res(), fail: (e) => rej(e) })
})
}
/** 关闭所有页面重新打开 */
async relaunch(path: string, params?: RouteParams): Promise<void> {
await new Promise<void>((res, rej) => {
uni.reLaunch({ url: this.buildUrl(path, params), success: () => res(), fail: (e) => rej(e) })
})
}
private notifyListeners(to: RouteContext, from: RouteContext) {
this.listeners.forEach(fn => fn(to, from))
}
}
export default Router
4.3 守卫实现:Token 鉴权
// router/guards/authGuard.ts
import type { RouteContext, RouterConfig } from './types'
/**
* Token 鉴权守卫
* - 白名单路径直接放行
* - 需要鉴权的路径检查 Token,无 Token 则重定向到登录页
*/
export function createAuthGuard(config: Pick<RouterConfig, 'loginPage' | 'homePage' | 'routes'>) {
const whiteList = [config.loginPage, config.homePage, '/pages/webview/index']
return async function authGuard(to: RouteContext, _from: RouteContext): Promise<boolean> {
const meta = config.routes[to.path]
if (!meta?.requiresAuth) return true
if (whiteList.includes(to.path)) return true
// 读取本地存储的 Token(与后端接口层共用)
const token = uni.getStorageSync('access_token')
if (!token) {
console.warn(`[AuthGuard] 未登录,拦截: ${to.path}`)
// 将拦截路径记录,登录成功后可恢复
uni.setStorageSync('redirect_after_login', to.path)
return false
}
return true
}
}
4.4 路由初始化与全局注册
// main.ts 或 src/router/setup.ts
import Router from './router/index'
import { createAuthGuard } from './router/guards/authGuard'
import type { RouterConfig } from './router/types'
const routerConfig: RouterConfig = {
loginPage: '/pages/login/index',
homePage: '/pages/index/index',
maxStackDepth: 10,
debug: __DEV__,
routes: {
'/pages/index/index': { requiresAuth: false, title: '首页', tabBarIndex: 0 },
'/pages/repo/detail': { requiresAuth: true, title: '仓库详情' },
'/pages/repo/issues': { requiresAuth: true, title: 'Issue 列表' },
'/pages/user/profile': { requiresAuth: true, title: '个人中心', tabBarIndex: 3 },
'/pages/login/index': { requiresAuth: false },
'/pages/webview/index': { requiresAuth: false },
}
}
// 创建单例
export const $Router = new Router(routerConfig)
// 注册鉴权守卫(首位执行)
$Router.beforeEach(createAuthGuard(routerConfig))
// 注册全局监听器(路由变化埋点)
$Router.afterEach((to, from) => {
// 忽略 tabBar 切换到自身的场景
if (to.path === from.path) return
console.log(`[Analytics] page_view: ${to.path}`)
})
// 挂载到全局
uni.$Router = $Router
4.5 组件方式调用:useRouter Composable
// composables/useRouter.ts
import { getCurrentInstance } from 'vue'
/**
* 在 Vue 页面组件中使用路由的 Composable
* 内部调用全局单例 $Router
*/
export function useRouter() {
const router = (uni as any).$Router
return {
push: (path: string, params?: Record<string, unknown>) => router.push(path, params),
replace: (path: string, params?: Record<string, unknown>) => router.replace(path, params),
switchTab: (path: string, params?: Record<string, unknown>) => router.switchTab(path, params),
back: (delta?: number, params?: Record<string, unknown>) => router.back(delta, params),
relaunch: (path: string, params?: Record<string, unknown>) => router.relaunch(path, params),
parseQuery: (q: Record<string, string>) => router.parseQuery(q),
}
}
4.6 鸿蒙端 Ability 跳转衔接
// router/harmonyRouter.ts
/**
* 鸿蒙端特殊路由适配
* 当 UniApp 运行在鸿蒙宿主时,拦截特定协议跳转到原生 Ability
*/
export function registerHarmonyRouter() {
// #ifdef APP-HARMONY
// 获取鸿蒙原生的 want 对象能力(需要 @ohos.app.ability.Want 模块)
const wantAbility = (window as any).Harmony?.wantAbility
if (wantAbility) {
// 注册协议拦截:atomgit://repo/open -> 原生仓库详情 Ability
uni.addInterceptor('navigateTo', {
invoke(e: { url: string }) {
const match = e.url.match(/^atomgit:\/\/repo\/open\?(.+)$/)
if (match) {
const params = new URLSearchParams(match[1])
wantAbility({
abilityName: 'RepoDetailAbility',
parameters: {
repoId: params.get('repoId') || '',
repoName: params.get('repoName') || '',
}
})
return false // 拦截,不执行 uni.navigateTo
}
return true
}
})
}
// #endif
}
4.7 页面基类:自动注册与参数接收
// pages/mixins/pageBase.ts
import { getCurrentInstance } from 'vue'
import { tabBarParamStore } from '@/router/tabBarStore'
export const pageBase = {
onLoad(this: any, query: Record<string, string>) {
// 优先从 Query String 解析参数
const params = (uni as any).$Router.parseQuery(query)
// tabBar 页面:从全局槽位补充参数(switchTab 无法携带参数)
if (tabBarParamStore.pending) {
Object.assign(params, tabBarParamStore.consume())
}
// 触发页面自定义的 onRouteParams 生命周期
const vm = getCurrentInstance()?.proxy
if (vm && typeof (vm as any).onRouteParams === 'function') {
(vm as any).onRouteParams(params)
}
}
}
五、深度技术原理
5.1 UniApp 页面栈机制
UniApp 底层维护了一条页面历史栈(Page Stack),所有 navigateTo 系列操作都在这条栈上执行:
栈深度限制:默认限制为 10 层(在 pages.json 中通过 maxNavigateType 可配置,但实际行为因平台而异)。当栈已满时,再次 navigateTo 在 iOS/Android 上会触发错误回调,在鸿蒙端行为需要额外测试兼容。
深度计算:UniApp 未提供直接 API 获取当前栈深度,但可以通过 getCurrentPages() 函数获取当前所有页面实例数组,长度即栈深度:
function getStackDepth(): number {
const pages = getCurrentPages()
return pages.length
}
5.2 与 Vue Router 的理念对比
Vue Router 运行在单页应用(SPA)的内存中,路由变化不涉及页面刷新,导航守卫是路由系统本身的一部分。而 UniApp 的路由基于原生页面栈,每次跳转是一次真实的页面切换操作。核心差异如下:
| 维度 | Vue Router | UniApp 路由 |
|---|---|---|
| 运行载体 | 纯 JS 内存 | 原生页面栈 |
| 守卫触发时机 | 路由变化前/后 | navigateTo/navigateBack 等 API 调用前/后 |
| 栈深度管理 | 浏览器历史记录,几乎无限制 | 原生限制,通常 ≤ 20 |
| 参数传递 | 路由参数对象(解耦) | URL Query String + 全局状态 |
| 生命周期 | 完全由 Vue 控制 | onLoad/onShow 等原生页面事件 |
UniApp 官方虽然提供了 addInterceptor(拦截器)机制,但该机制在跨平台一致性上存在差异。AtomGit 项目的路由封装在 addInterceptor 之上增加了守卫队列,确保逻辑一致。
5.3 鸿蒙 UIAbility 跳转衔接原理
鸿蒙应用的页面跳转基于 UIAbility(用户交互能力)和 Want(意图对象)。UniApp 运行在鸿蒙宿主环境中时,底层 WebView 与原生 Ability 通过 NAPI 接口通信。

AtomGit 项目在 registerHarmonyRouter 中将 atomgit:// 协议注册到原生侧,通过 wantAbility 方法创建 Want 对象并启动对应 Ability。URL 协议格式为 scheme://host/path?query,与标准的 App Link 机制保持一致。
5.4 switchTab 的底层特殊性
uni.switchTab 之所以不能携带参数,是因为它底层直接操作原生 TabBar 控件,而 TabBar 的每个 Item 在 pages.json 中以独立页面注册,系统在切换 Tab 时直接加载对应页面,不走 navigateTo 的 URL 解析流程。UniApp 的 addInterceptor 对 switchTab 的拦截能力也受限,部分平台可能无法完全拦截。因此采用全局状态槽位(Pinia)作为替代方案,在目标页面 onLoad 时主动消费该槽位。
六、常见问题解答(FAQ)
Q1:为什么 switchTab 跳转后参数丢失?
A:这是 UniApp 继承自小程序平台的设计限制,switchTab 底层直接操作原生 TabBar,URL 参数解析在切换流程中被跳过。推荐使用 Pinia 全局状态槽位方案:切换 Tab 前将参数写入 tabBarParamStore,目标页面 onLoad 时读取并清空。另一备选方案是将参数持久化到 Storage,适合应用重启后仍需保留参数的场景。
Q2:navigateTo 页面栈超出上限怎么办?
A:有两种应对策略。预防策略:在封装层对 navigateTo 进行深度检查,当栈深度接近上限(建议阈值为 8)时,将最后一次跳转替换为 reLaunch 到目标页面,防止静默失败。
async push(path: string, params?: RouteParams) {
if (getStackDepth() >= (this.config.maxStackDepth! - 1)) {
console.warn('[Router] 栈深度接近上限,改用 relaunch')
return this.relaunch(path, params)
}
// ... 正常逻辑
}
补救策略:若 navigateTo 已经失败(返回错误),捕获错误后提示用户,或自动回退到首页:this.relaunch(this.config.homePage)
Q3:如何实现带参数返回上一页(navigateBack)?
A:uni.navigateBack 本身不支持携带参数,因为返回操作不重新加载目标页面。可以使用以下方式绕过:
方式一(推荐):使用 EventBus 或 pinia 全局事件总线。目标页面 B 在返回前 uni.$emit('result', data),返回后页面 A 在 onShow 生命周期中 uni.$on('result', handler) 接收。
方式二:利用 getCurrentPages() 获取上一页实例,直接调用上一页的方法:
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage) {
(prevPage as any).onResultFromNext?.(resultData)
}
uni.navigateBack({ delta: 1 })
Q4:多个路由守卫的执行顺序是怎样的?
A:守卫按注册顺序线性链式执行(类似 Koa 中间件)。返回 false 立即中断拦截导航,返回 true 或 void 继续下一个。异步守卫同样按顺序等待上一个完成后才执行下一个。执行流程:Guard1(await) → Guard2(await) → … → 执行实际导航。
Q5:鸿蒙端路由与原生 Ability 跳转如何保证一致性?
A:核心思路是统一协议层。在 registerHarmonyRouter 中将原生 Ability 跳转注册为 URL 拦截器,对业务代码屏蔽底层差异。业务层只需调用 $Router.push(url),协议路由触发了 Ability 跳转还是 WebView 内部导航,由适配层决定,业务代码无需感知。
Q6:如何在路由变化时统一注入页面标题?
A:在路由配置中为每个路径定义 title 元数据,在 afterEach 监听器中统一读取并调用 uni.setNavigationBarTitle 设置。
七、运行效果
7.1 导航流程示意
用户操作 页面栈状态 Router 行为
──────────────────────────────────────────────────────────────
[打开 App]
[0] /pages/index/index ✓
点击"Vue 仓库" ─────────────────────────→ push('/pages/repo/detail', {id:'vue'})
[1] /pages/repo/detail
点击"Issue 列表" ─────────────────────────→ push('/pages/repo/issues', {repo:'vue'})
[2] /pages/repo/issues
点击"返回" ─────────────────────────→ back(1, {selectedId:'42'})
[1] /pages/repo/detail (tabBarParamStore 槽位写入)
点击 tabBar"首页" ─────────────────────────→ switchTab('/pages/index/index')
[0] /pages/index/index (tabBarParamStore 消费清空)
无 Token 用户操作:
点击"我的仓库" ─────────────────────────→ AuthGuard 拦截 → 重定向登录页
登录成功 ─────────────────────────→ 读取 redirect_after_login → push 目标页
7.2 调试日志输出(debug 模式)
[Router] navigateTo: /pages/repo/detail?repoId=vue&name=Vue+Core
[AuthGuard] 放行: /pages/repo/detail
[Router] navigateBack: delta=1
[Router] switchTab: /pages/index/index
[Analytics] page_view: /pages/index/index
八、扩展方向
8.1 动画与转场控制
为路由配置增加 animationType 字段(如 slide-right、fade),配合 uni.preloadPage 预加载目标页面以减少白屏时间。
8.2 路由懒加载与分包
在 pages.json 中配置分包策略,减少首屏体积。路由封装层可感知分包路径前缀,自动处理分包页面解析。
8.3 深链接与 Universal Link
在鸿蒙端通过 atomgit:// 协议接入系统 Deep Link 能力,借助 UIAbility 的 onNewWant 在冷启动时接收唤起参数并完成路由导航。
更多推荐



所有评论(0)