在这里插入图片描述

一、我们要做什么

1.1 路由需求清单

在 AtomGit 开源社区 App(UniApp + 鸿蒙)项目中,我们需要一套统一、可扩展的导航解决方案,核心需求如下:

需求编号 需求描述 优先级
R-01 支持 navigateToredirectToreLaunchswitchTabnavigateBack 五种基本导航 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 核心挑战

  1. 多端一致性:UniApp 在不同平台(iOS、Android、鸿蒙)上路由行为存在细微差异,特别是鸿蒙端 WebView 与原生 Ability 跳转的衔接。
  2. 守卫链设计:路由守卫需同时支持同步和异步鉴权,不能阻塞用户操作的即时响应体验。
  3. tabBar 约束switchTab 不能携带参数(小程序平台限制),需要全局状态共享方案。
  4. 栈深度限制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 开放字段,允许业务层随时扩展(如 keepAlivecacheMax)。
  • 类型安全:所有路由跳转必须携带显式路径和参数,编译期可检查参数格式。

三、核心设计决策

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——中间件管道模式。每个守卫函数接收 tofrom 两个上下文,返回 booleantrue = 放行,false = 拦截)。支持 Promise 异步守卫,守卫队列按注册顺序线性执行,遇到拦截立即中断。

3.3 参数传递策略

UniApp 中 navigateTo 携带参数有两种方式:

方式 示例 限制
Query String uni.navigateTo({ url: '/pages/detail?repoId=vue' }) 值自动 toString(),复杂对象需序列化
全局状态 通过 pinia / vuex 全局状态传递 需要状态管理器依赖,tabBar 页面均可访问

选型结论:优先使用 Query String 序列化(JSON.stringifyencodeURIComponent),在目标页通过 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 的 addInterceptorswitchTab 的拦截能力也受限,部分平台可能无法完全拦截。因此采用全局状态槽位(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)?

Auni.navigateBack 本身不支持携带参数,因为返回操作不重新加载目标页面。可以使用以下方式绕过:

方式一(推荐):使用 EventBuspinia 全局事件总线。目标页面 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 立即中断拦截导航,返回 truevoid 继续下一个。异步守卫同样按顺序等待上一个完成后才执行下一个。执行流程: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-rightfade),配合 uni.preloadPage 预加载目标页面以减少白屏时间。

8.2 路由懒加载与分包

pages.json 中配置分包策略,减少首屏体积。路由封装层可感知分包路径前缀,自动处理分包页面解析。

8.3 深链接与 Universal Link

在鸿蒙端通过 atomgit:// 协议接入系统 Deep Link 能力,借助 UIAbility 的 onNewWant 在冷启动时接收唤起参数并完成路由导航。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐