开源鸿蒙平台 CMP 三方库「RouteComposer」适配全流程
本文记录
RouteComposer接入 OpenHarmony 的完整过程,覆盖 iOS 路由库盘点、ArkTS 源码级移植决策、六大实体与路由引擎移植、UIKit 到 ArkUI 的平台映射、ArkUI 真机页面、HAP 构建、签名和真机验收。
RouteComposer是与具体业务无关的 iOS 导航/深链库(纯 Swift、协议导向)。它与本系列前序的 Kotlin/CMP 库不同:Swift 没有 Kotlin/Native 或 N-API 通道可走,因此本次适配采用源码级 ArkTS 移植——把库的核心抽象(Router、Step 链、Finder、Factory、Action、Interceptor、ContextTask、PostRoutingTask 与装配 DSL)逐语义移植为 ArkTS,平台层映射到 ArkUI 的Navigation/NavPathStack/Tabs,ArkUI 页面作为真机验收宿主。
项目地址: AtomGit/oh-tpc/route-composer
上游仓库: ekazaev/RouteComposer
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台适配
RouteComposer 为 iOS 提供 viewController 的组合(composition)、导航(navigation)与深链(deep linking):把一次导航拆成可复用的小步骤(Finder 找已有屏、Factory 建新屏、Action 集成进栈),声明式配置路由表,把导航代码从页面中移走,每个页面天然支持深链。上游 2.22.2 共 151 个 Swift 文件、约 9800 行,纯逻辑 + UIKit 集成层。
OpenHarmony 应用同样需要这套「协议式路由」能力(页面解耦、深链直达、登录拦截、Tab 容器编排),但上游只有 Swift 实现,不存在任何跨语言编译通道:
| 障碍 | 具体问题 |
|---|---|
| 语言不可编译 | Swift 源码无法编译进 OpenHarmony,没有 Kotlin/Native 或 N-API 的等价通道。 |
| 框架体系不同 | 上游深度绑定 UIViewController 层级遍历,ArkUI 是声明式渲染,无可内省的 VC 树。 |
| 容器语义不同 | UINavigationController/UITabBarController/模态链与 NavPathStack/Tabs/覆盖层一一错位。 |
| 类型系统差异 | Swift 协议 + associatedtype + 类型擦除体系需要映射到 ArkTS 接口(禁 any、禁重载、禁结构类型)。 |
| DSL 语法差异 | 上游 adding(...) 依赖重载、方法带参数标签,ArkTS 均不允许。 |
| 交付链路复杂 | HAR 库模块、HAP 工程、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在「核心语义逐行对照移植 + 平台映射层」:路由算法、步骤链、实体协议照搬上游语义;UIKit 依赖被映射为显式注册的「逻辑屏幕树」,ArkTS 只负责渲染这棵树。
1.2 库提供的能力
Demo 覆盖九个可观察场景(与上游 iOS Example 一一对应):
| 场景 | 装配方式 | 上游对应 |
|---|---|---|
| Welcome → 主界面 | CompleteFactoryAssembly + TabsReplaceRootAction 整体替换根 | homeScreen(TabBar replaceRoot) |
| 三 Tab 容器 | 每_tab 一个 Navigation(NavPathStack) 栈 | UITabBarController + 子导航栈 |
| Tab 内 Push | StepAssembly + PushAction | figuresAndProductScreen |
| 替换栈顶 | PushReplacingLastAction | colorScreen |
| 有栈就 Push、无栈就模态 | SwitchAssembly.addCase(NavHostTopFinder) | Product 的三分支装配 |
| 登录拦截 | LoginInterceptor + 排队导航 + 登录后续航 | LoginConfiguration |
| 动态加 Tab | AddTabAction 容器动作 | starScreen 的 addTab 装配 |
| 复用已有屏 | ClassFinder 命中不重复入栈,仅切 Tab | circleScreen(classFinder) |
| 模态叠模态 | PresentAction 作用于模态层 | secondModalScreen |
核心数据结构 RCScreen(逻辑屏幕节点)包含:
| 字段 | 作用 |
|---|---|
name | 屏幕类型标识(对应上游 UIViewController 类型)。 |
params | Record<string, Object> 上下文载荷(对应上游 Context 注入)。 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 路由引擎、步骤链、六大实体、装配 DSL 全部按上游语义移植,方法名与注释标注上游出处。 |
| 平台目标 | 纯 ArkTS HAR 库模块,无 .so、无 NAPI、无 abiFilters 依赖。 |
| 映射显式 | UIKit 容器全部映射到注册屏幕树(RootHostState/TabsHostState/NavHostState/ModalHostState)。 |
| Demo 完整 | 九个场景 + 全局埋点日志抽屉 + 防重入 + 登录拦截续航。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是对上游 UI 的像素级复刻,而是路由模型的行为验收宿主:每个按钮触发一次路由表装配的导航,日志抽屉逐条打印引擎的决策过程。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点源集、查重、确定源码级移植路线
第 2 阶段:工程打通 ── HAR 库模块 + HAP Demo 工程、依赖与构建链
第 3 阶段:核心移植 ── 六大实体、步骤链、DefaultRouter 三阶段算法
第 4 阶段:平台映射层 ── 逻辑屏幕树、宿主注册、九个 Action 的落地
第 5 阶段:ArkUI 宿主 ── 单页渲染注册屏幕树、@Builder 屏幕目录、日志抽屉
第 6 阶段:示例与验证 ── Node 复现测试、HAP、签名、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
RouteComposer/ 上游 Swift 源码(151 文件)原样保留,不参与鸿蒙构建
Example/RouteComposer/ 上游 iOS Example 原样保留(对照用)
Example/harmonyApp/ DevEco 工程:routecomposer HAR 库 + entry Demo
Example/harmonyApp/routecomposer/ ArkTS 移植本体(八个子模块)
README.md / README.upstream.md / README.OpenHarmony.md / README.OpenHarmony_CN.md
上游已有大写 Example/ 目录(iOS Demo),macOS 文件系统大小写不敏感,鸿蒙 Demo 放在 Example/harmonyApp,与已过审仓库的 example/harmonyApp 同一规则,路径大小写随上游目录。README 四件套:中文首页、上游原文(git mv 保留)、英文摘要、中文主文档(22 节、6700+ 汉字)。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| 移植语言 | ArkTS(HAR 静态共享库) |
| DevEco Studio | 26.0.0.821 |
| hvigor modelVersion | 5.1.0 |
| compatibleSdkVersion / targetSdkVersion | 5.1.0(18) / 5.1.0(18),runtimeOS HarmonyOS |
| ABI | 纯 ArkTS,无 native 依赖,无需 abiFilters |
| Bundle | org.terminator.ohos.routecomposer |
| 版本 | HAR 2.22.2(SemVer),App versionName 2.22.2-ohos.1 |
1.3 页面边界
标题区 RouteComposer · OHOS + 登录状态 + 登录/登出 + 日志开关
根内容区 welcome 单屏 或 三 Tab 容器(每 Tab 一个 Navigation 栈)
模态层 登录卡 / Wishlist 卡 / Second Modal 卡(支持叠层)
日志抽屉 底部 40% 面板,逐条显示路由埋点与拦截日志
页面启动时 bootstrap() 把 welcome 设为根屏;「进入主界面」触发 TabsReplaceRootAction,Tabs 宿主由路由动作在首次替换根时创建,不需要预注册。
第 2 阶段:工程打通
2.1 HAR 库模块
Example/harmonyApp/
├── routecomposer/ HAR 库模块(harTasks)
│ ├── Index.ets 公开 API 桶文件(约 60 个导出)
│ ├── oh-package.json5 name: routecomposer, version: 2.22.2(SemVer)
│ ├── build-profile.json5 apiType stageMode
│ ├── hvigorfile.ts harTasks
│ └── src/main/ets/routecomposer/
│ ├── core/ RCTypes / Contracts / Hosts(值类型、六实体接口与 Box、屏幕树)
│ ├── steps/ Steps(RoutingStep 链、BaseStep、DestinationStep、GeneralStep)
│ ├── router/ DefaultRouter(三阶段导航引擎)
│ ├── assemblies/ StepAssembly / ChainAssembly / SwitchAssembly / CompleteFactoryAssembly
│ ├── finders/ BaseFinders / Finders(ClassFinder 等 + 栈迭代器)
│ ├── factories/ Factories(ScreenFactory / TabsContainerFactory)
│ ├── actions/ Actions(九个 Action 的平台落地)
│ └── extra/ Extras(Inline* / Destination / 装饰器 Router)
├── entry/ HAP Demo(hapTasks,依赖 file:../routecomposer)
└── build-profile.json5 modules: [entry, routecomposer]
HAR 模块要点:src/main/module.json5 声明 "type": "har";oh-package.json5 的 version 必须是严格 SemVer(2.22.2-ohos.1 会被 ohpm 拒绝,-ohos.1 后缀保留在 App 的 versionName 里,与本系列已过审仓库一致)。
2.2 构建验证
cd Example/harmonyApp
ohpm install
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
# > hvigor BUILD SUCCESSFUL
命令行验证用 ~/ohos/command-line-tools(hvigorw + ohpm 6.0.1),真机构建在 DevEco Studio 26 内完成。两套工具链不要混用缓存:切换前删除项目内 .hvigor/、oh_modules/ 与 lock 文件,否则 DevEco 侧会报 (0, hvigor_1.WithParamReplacement) is not a function 这类内部函数缺失错误(详见 8.1 踩坑复盘)。
第 3 阶段:核心移植
3.1 UIKit → ArkUI 映射契约
上游 UIViewController 层级 本仓逻辑屏幕树(ScreenHostRegistry)
UIWindow → RootHostState(页面根内容)
UINavigationController → NavHostState(NavPathStack + 镜像数组)
UITabBarController → TabsHostState(N 个 NavHostState + 选中索引)
presented 模态链 → ModalHostState(全屏覆盖层链)
UIViewController → RCScreen(name + params,由页面 @Builder 渲染)
页面只是这棵树的渲染器:根内容 + Tabs(ForEach TabContent → Navigation(stack)) + 模态覆盖卡片。库是树的唯一写者,页面通过 RCNotifier 订阅变更刷新 @State。
3.2 六大实体与类型擦除
Swift 协议 + Any* + Box 体系天然适配 ArkTS 的「接口 + 包装类」:
export interface Finder<C> {
prepare(context: C): void;
findViewController(context: C): ScreenRef | undefined;
}
export interface Factory<C> {
prepare(context: C): void;
build(context: C): RCScreen;
}
export interface Action {
perform(screen: RCScreen, previous: ScreenRef, animated: boolean,
completion: (result: RoutingResult, newRef: ScreenRef | undefined) => void): void;
}
export interface RoutingInterceptor<C> {
perform(context: C, completion: (result: RoutingResult) => void): void;
}
步骤内以擦除形态存异构实体(对应上游 Router/Type Erasure/ 8 个 Any* + 12 个 Box):
export class FinderBox<C> implements AnyFinder {
private readonly finder: Finder<C>;
findViewController(context: RCContext): ScreenRef | undefined {
return this.finder.findViewController(context as C); // 泛型运行时擦除
}
}
Swift 的 adding(...) 重载在 ArkTS 拆为三个具名方法:addingInterceptor / addingContextTask / addingPostRoutingTask。
3.3 DefaultRouter 三阶段算法
对照上游 DefaultRouter.swift 逐段移植 navigate:
第 1 阶段 prepareFactoriesStack
toContext 装箱 → 沿步骤链(destination 最先)逐个 perform
SuccessResult finder 命中 → 导航起点(先跑本步 ContextTask)
BuildResult 未命中有工厂 → 工厂载荷 unshift 进 buildList(根方向在前)
UpdateContext ConvertingStep 更新上下文继续走
全部未命中 → RoutingError.initialControllerNotFound
第 2 阶段 startNavigation
拦截器链顺序异步执行(递归 index runner),任一失败即终止
dismiss 起点之上的模态(上游 dismissPresented 语义)
第 3 阶段 buildViewControllerStack
从容器到叶子递归:makeVisible 切 Tab → factory.build → ContextTask
→ action.perform 集成(push/present/replaceRoot/...)→ 下一工厂
工厂耗尽 → 全部 PostRoutingTask → completion(success)
每阶段都有 RCLoggerHolder 日志,真机「日志」抽屉可直接看到引擎决策。
3.4 装配 DSL 移植
| 上游 | 本仓 ArkTS |
|---|---|
StepAssembly(finder:factory:).using(action).adding(...).from(prev).assemble() | StepAssembly.finder(f, fa).using(action).addingInterceptor(...).from(prev).assemble() |
ChainAssembly.from(step) | ChainAssembly.from<RCContext>(step).using(action).assemble() |
SwitchAssembly.addCase(expecting:).assemble(default:) | new SwitchAssembly<RCContext>().addCase(finder, step).assembleUsingDefault(default) |
CompleteFactoryAssembly(factory:).with(f).using(.add())... | CompleteFactoryAssembly.with(f, addTab).with(...).usingRoot(tabsReplaceRoot).from(GeneralStep.root()).assemble() |
容器装配必须 .from(GeneralStep.root()) 锚定起点,否则路由器找不到导航起点(这是真机首验发现的问题之一,见 8.1)。
第 4 阶段:平台映射层(技术难点)
4.1 逻辑屏幕树与镜像数组
ArkUI 的 NavPathStack 没有树状遍历 API(只有 getAllPathName() / getParamByName()),而 Finder 的本职就是「在栈里找屏」。解法是库侧镜像:
export class NavHostState {
readonly stack: NavPathStack; // ArkUI 渲染驱动
screens: RCScreen[]; // 库侧镜像,Finder 的搜索对象
push(screen: RCScreen, animated: boolean): void {
this.stack.pushPathByName(screen.name, screen.params, animated);
this.screens.push(screen);
}
syncFromStack(): void { // 每次搜索前重建镜像,吸收手势返回造成的脱同步
const names: string[] = this.stack.getAllPathName() as string[];
// 名单一致则跳过;否则按 name + getParamByName 重建
}
}
DefaultStackIterator 的遍历顺序对应上游 .fullStack:模态层(顶先)→ 当前 Tab 宿主 → 其他 Tab → 独立宿主 → 根内容,起点前先对全部宿主 syncFromStack()。
4.2 九个 Action 的落地
| 本仓 Action | 上游对应 | 落地方式 |
|---|---|---|
PushAction | NavigationControllerAction.push | previous.host.push(screen) |
PushAsRootAction | .pushAsRoot(setViewControllers) | host.replaceAll(screen) |
PushReplacingLastAction | .pushReplacingLast | host.popAndPush(screen) |
PresentAction | GeneralAction.presentModally | registry.modal.present(screen) |
ReplaceRootAction | GeneralAction.replaceRoot | registry.root.setContent(screen) |
NilAction | GeneralAction.nilAction | 什么都不做,直接成功 |
DelayedAction | DispatchQueue.delay 包装 | setTimeout 延迟整个 perform |
TabsReplaceRootAction | TabBarControllerFactory + replaceRoot | 每个 TabSeed 建 NavHostState + seed → registry.setTabs(...) |
AddTabAction | UITabBarController.add() | 取 embedding.tabs[0] → currentTabs.addTab(host, true) |
4.3 两个装饰器 Router
GlobalInterceptorRouter(上游同名):全局拦截器在委托前顺序执行,全局 PostTask 在成功后执行;SingleNavigationRouter(上游同名):排队语义——导航进行中的新导航不拒绝而是 postpone,当前导航完成后补跑。这正是登录拦截器的关键依赖:拦截器在「被拦截的那次导航」内部发起navigate(loginScreen),排队机制保证它在上一次导航收尾后立刻执行。
第 5 阶段:ArkUI 宿主
5.1 页面 = 注册屏幕树的渲染器
build() {
Stack({ alignContent: Alignment.TopStart }) {
if (this.rootIsTabs) {
this.TabsRoot(this.registry.currentTabs as TabsHostState) // Tabs + N 个 Navigation
} else {
this.WelcomeScreen()
}
if (this.modalCount > 0) { this.ModalLayer() } // 最顶模态 + 半透明遮罩
this.TopBar() // 登录态 + 日志开关
if (this.logOpen) { this.LogDrawer() }
}
}
5.2 屏幕目录与状态桥接
@Builder
ScreenDestination(name: string, param?: Object) {
if (name === 'circle') { this.CirclePage() }
else if (name === 'product') { this.ProductPage(param) } // 显示 params.id
// ... square / home / star / color / 兜底
}
ArkUI 是细粒度更新,库对屏幕树的每次写入经 RCNotifier 回调搬进页面 @State(rootIsTabs / tabHosts / modalCount / topModal / logLines),再由 @State 驱动刷新——对应 UIKit「改了 VC 树系统自动排版」的隐式行为。
5.3 登录拦截续航
export class LoginInterceptor implements RoutingInterceptor<RCContext> {
perform(context: RCContext, completion: (r: RoutingResult) => void): void {
if (DemoSession.shared().loggedIn) {
completion(RoutingResult.ok()); // 放行
} else {
DemoConfiguration.router().navigate(DemoConfiguration.loginScreen(), context); // 排队弹登录
completion(RoutingResult.fail(RoutingError.intercepted('login required'))); // 本次失败
}
}
}
登录卡「登录」按钮:session.login() → modal.dismissTop() → 重新 navigate(wishlistScreen),此时拦截器放行、Wishlist 呈现——与上游 LoginConfiguration 的「拦截 → 登录 → 续航」流程一致。
第 6 阶段:示例与验证
6.1 工程结构
Example/harmonyApp/
├── routecomposer/src/main/ets/routecomposer/{core,steps,router,assemblies,finders,factories,actions,extra}/
├── entry/src/main/ets/demo/DemoState.ets 会话单例(登录态 + 日志)
├── entry/src/main/ets/demo/DemoConfiguration.ets 路由表(九个场景装配)
└── entry/src/main/ets/pages/Index.ets 单页宿主(树渲染器)
6.2 构建、签名和安装
# 命令行验证(无签名 debug 包)
cd Example/harmonyApp
ohpm install
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
# 真机:DevEco Studio 打开 Example/harmonyApp(不要打开仓库根目录)
# File → Project Structure → Signing Configs → Automatically generate signature
hdc list targets -v
hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start -a EntryAbility -b org.terminator.ohos.routecomposer -m entry
四、完整代码对照
4.1 整体架构
DemoConfiguration.ets(路由表装配)
│ StepAssembly / SwitchAssembly / CompleteFactoryAssembly
▼
DestinationStep 链 → DefaultRouter(三阶段引擎)
│ Finder 搜索逻辑屏幕树 / Factory 产出 RCScreen / Action 集成入树
▼
ScreenHostRegistry(RootHostState + TabsHostState + NavHostState + ModalHostState)
│ RCNotifier 变更通知
▼
Index.ets(@Builder 屏幕目录 + @State 桥接)
▼
ArkUI 真机页面(Tabs + Navigation + 模态覆盖层)
4.2 文件清单
| 文件 | 职责 |
|---|---|
routecomposer/src/.../core/RCTypes.ets | RCScreen / ScreenRef / RoutingResult / RoutingError / 日志与通知 |
routecomposer/src/.../core/Contracts.ets | 六实体接口 + Any*/Box 类型擦除 + ChildCoordinator |
routecomposer/src/.../core/Hosts.ets | 逻辑屏幕树四类宿主 + 注册表 + 镜像同步 |
routecomposer/src/.../steps/Steps.ets | RoutingStep 链 / BaseStep / SwitcherStep / DestinationStep / GeneralStep |
routecomposer/src/.../router/DefaultRouter.ets | 三阶段导航引擎(对照 DefaultRouter.swift) |
routecomposer/src/.../assemblies/Assemblies.ets | 四种装配 DSL(对照 Assemblies/ 目录) |
routecomposer/src/.../finders/*.ets | ClassFinder 等 + StackIterator + SearchOptions 位掩码 |
routecomposer/src/.../actions/Actions.ets | 九个 Action 平台落地 |
routecomposer/src/.../extra/Extras.ets | Inline* / Destination / 两个装饰器 Router |
entry/src/main/ets/demo/DemoConfiguration.ets | 九场景路由表 + LoginInterceptor |
entry/src/main/ets/pages/Index.ets | 单页宿主(树渲染器 + 日志抽屉) |
README.OpenHarmony_CN.md | 中文主文档(22 节:映射总表、踩坑表、故障排查、已知限制) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 库 | Router.navigate(step, context, animated, completion) | 一次导航入口(上游 Router 协议) |
| 库 | BaseStep.perform(context) | 步骤四态:命中 / 构建 / 更新上下文 / 无 |
| 库 | DefaultStackIterator.firstViewController(predicate, options) | 全树搜索(上游 StackIterator) |
| 库 | TabsContainerFactory.build(context, coordinator) | 构建 Tab 容器种子栈 |
| 库 | GlobalInterceptorRouter / SingleNavigationRouter | 全局拦截 / 防重入排队 |
| Demo | DemoConfiguration.mainScreen() | 三 Tab 容器替换根 |
| Demo | DemoConfiguration.productScreen() | SwitchAssembly 双分支 |
| Demo | LoginInterceptor.perform | 登录拦截 + 排队弹登录 + 失败返回 |
4.4 ArkTS 与上游 Swift 的边界
// ArkTS:步骤装配(上游 StepAssembly + adding 重载 → 具名方法)
StepAssembly.finder(new ClassFinder<RCContext>('wishlist'),
new ScreenFactory<RCContext>('wishlist'))
.using(new PresentAction())
.addingInterceptor(new LoginInterceptor())
.from(GeneralStep.current<RCContext>())
.assemble();
// 上游 Swift:同一语义
StepAssembly(finder: ClassFinder(), factory: ClassFactory())
.using(GeneralAction.presentModally())
.adding(LoginInterceptor())
.from(GeneralStep.current())
.assemble()
两侧之间共享的是语义(链式步骤、实体分工、三阶段引擎),不共享源码——这是 Swift 源库与本系列 Kotlin 库适配路径的根本差异。
五、关键决策说明
决策 1:源码级 ArkTS 移植,而不是桥接
Swift 没有 OpenHarmony 编译通道(无 Kotlin/Native、无 N-API 等价物),唯一可行路线就是把核心抽象移植成 ArkTS。本仓是该系列里第一个 Swift 源库案例,验证了「KMP/KN 桥接」之外的另一条标准路径。
决策 2:显式逻辑屏幕树代替 VC 层级遍历
上游靠 UIWindow/UIViewController 的运行时层级做 Finder;ArkUI 声明式渲染不可内省。本仓让库拥有显式的注册屏幕树(页面创建 NavPathStack 等句柄注册进去),树是模型、ArkUI 是渲染——Finder/Action 的语义得以原样保留。
决策 3:照搬 Any* + Box 类型擦除体系
Swift 泛型与 TS 泛型都在运行时擦除,上游 8 个 Any* + 12 个 Box 的设计在 ArkTS 里几乎原样成立,异构实体集合(一个步骤里同时存 Finder/Factory/任务)无需任何发明。
决策 4:保持 completion 回调风格
上游拦截器/Action 是异步回调语义(拦截器可以弹 UI 再取消),强行 async/await 化会改变时序。本仓保持回调风格,SingleNavigationRouter 采用上游的 postpone(排队)语义,保证「拦截中再导航」可用。
决策 5:Node 复现环境做逻辑回归
把库(纯 TS 逻辑)与 Demo 路由表放进 Node,桩掉 NavPathStack,用 esbuild 打包跑「真机点击序列」。登录流程的三处缺陷(见 8.1)全部由该环境定位并验证修复,比真机盲试快一个数量级。
六、测试与验证
6.1 测试环境
本次验证使用 macOS(Apple Silicon)、DevEco Studio 26.0.0.821、command-line-tools 6.0.1(hvigorw + ohpm)、Node 24 + esbuild 复现环境;真机通过 DevEco 自动签名安装运行。
6.2 Node 复现测试
# 把 routecomposer HAR 源码 + DemoConfiguration/DemoState 拷入临时目录(.ets → .ts)
# 桩掉 NavPathStack(pushPathByName/pop/clear/getAllPathName/getParamByName)
npx esbuild harness.ts --bundle --platform=node --outfile=out.js && node out.js
复现脚本按真机点击序列执行五步:进入主界面 → 未登录点 Wishlist → 登录卡开着再点一次 → 登录按钮 → Second Modal。修复后的断言输出:
STATE[进入主界面] modal=[(empty)] tabs[tab0:circle tab1:square tab2:home] sel=0
STATE[第一次 wishlist] modal=[login] (拦截 → 排队弹登录)
STATE[第二次 wishlist] modal=[login] (登录卡复用,不叠层)
STATE[登录后] modal=[wishlist](登录卡消失,Wishlist 呈现)
STATE[second modal] modal=[wishlist | secondmodal]
6.3 HAP 验证
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
6.4 功能用例
- Welcome「进入主界面」→ 三 Tab 容器替换根成功;
- 各 Tab 内 Push Product,
params.id正确注入显示; - 「Push Color(替换栈顶)」后栈深度不变;
- 未登录打开 Wishlist → 弹登录卡,toast 提示被拦截;
- 登录卡开着重复触发 Wishlist → 登录卡不叠层;
- 登录 → 登录卡消失、Wishlist 出现、顶栏变「已登录」;
- Wishlist 内打开 Second Modal → 模态叠模态;
- 「添加 Star Tab」→ 动态出现第四个 Tab 并选中;
- 「直达 Circle」→ ClassFinder 命中已有屏,仅切 Tab 不重复入栈;
- 连点两个导航按钮 → 第二个被 SingleNavigationRouter 排队,日志抽屉可逐条查看。
6.5 验证结论
HAR 库模块、HAP 工程、签名链路和九个路由场景已完成验证。应用在真机上成功安装并进入前台,登录拦截续航、防重入排队、容器替换根等核心语义与上游一致;适配提交 鸿蒙适配 已推送至 AtomGit,远端 Git Hooks 检查通过。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/route-composer.git
cd route-composer/Example/harmonyApp
ohpm install
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon # 命令行验证
# 真机:DevEco Studio 打开 Example/harmonyApp → 自动签名 → Run
hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start -a EntryAbility -b org.terminator.ohos.routecomposer -m entry
八、遗留问题与改进方向
8.1 踩坑复盘
- 容器装配缺锚点:
CompleteFactoryAssembly最初没有.from(...),路由器找不到起点,「进入主界面」真机必败(Initial screen not found)。修复:装配器补from(previousStep),Demo 锚定GeneralStep.root(); - 防重入语义错了:
SingleNavigationRouter初版「导航中直接拒绝」,导致拦截器内部的navigate(loginScreen)被静默吞掉。上游语义是 postpone——改为排队后登录卡才能可靠弹出; - 登录卡叠层:
loginScreen最初用NilFinder(每次新建),连续触发会叠出多张登录卡,关一张还剩一张。改ClassFinder复用后不再叠层。以上三处全部由 Node 复现环境定位; - macOS 大小写不敏感:上游已有
Example/, lowercaseexample/会并进同一目录,鸿蒙 Demo 只能放Example/harmonyApp; - ArkTS 语法硬约束:Swift 参数标签(
perform(on:with:))非法、adding重载必须拆名、类型别名桶导出要export type、NavPathStack是全局环境声明(不能从@kit.ArkUIimport)、pop重载参数个数要与签名严格对齐; - HAR 工程细节:HAR 模块需要自己的
src/main/module.json5;oh-package.json5版本必须是严格 SemVer(2.22.2-ohos.1会被警告拒绝); - 双工具链缓存互踩:command-line-tools 与 DevEco Studio 的 hvigor 版本不同,混用后报
WithParamReplacement is not a function——切换前删.hvigor/、oh_modules/与 lock 文件即可恢复; - 签名材料不进仓:
signingConfigs保持空数组,.gitignore覆盖*.p12/*.cer/*.p7b/*.hap。
8.2 已知问题
- ArkUI
Navigation无原生模态呈现,模态层是应用级覆盖卡片(无自定义转场 delegate、无 UISheetPresentation 等价物); SearchOptions六个选项做了并集简化(上游的 contained/presenting/parent 以「宿主内整栈 + 模态层」近似),逐项映射表见仓库中文主文档第 13 节;- UISplitViewController / Storyboard / Segue / SwiftUI 互操作无对应物,裁剪;
expectingContainer等编译期泛型检查不存在,以运行时约定替代;GlobalInterceptorRouter的全局 ContextTask 无命中屏可施加,当前仅保留字段并打日志说明。
8.3 未来优化方向
- 把 HAR 发布到 ohpm 中心仓,供业务工程直接依赖;
- 补充
DetailsNavigationFinder、ClassWithContextFinder更多场景与深链(Want/URL)入口示例; - 模态层接入 ArkUI 自定义转场动画,对齐上游
presentModally(transitioningDelegate:); - 为路由引擎补 ohosTest/hypium 单元测试,替代当前 Node 复现脚本;
- 在 CI 中加入 HAR 编译、HAP 构建与 codelint 检查。
九、总结
9.1 核心难点回顾
RouteComposer (Swift, 151 文件)
→ 六大实体协议 + Any*/Box 类型擦除(ArkTS 接口 + 包装类)
→ DefaultRouter 三阶段引擎(逐语义移植)
→ 逻辑屏幕树(UIKit 容器 → RootHost/TabsHost/NavHost/ModalHost)
→ 九个 Action 平台落地(NavPathStack / Tabs / 覆盖层)
→ ArkUI 单页宿主(@Builder 屏幕目录 + 日志抽屉)
→ HAP 构建、签名、真机验收
9.2 封装层次
- 库核心层:RCScreen/ScreenRef 值类型、六实体接口、Box 擦除、步骤链与装配 DSL;
- 引擎层:DefaultRouter 三阶段导航(搜索起点 → 拦截器 → 递归构建),全程可日志;
- 平台层:注册屏幕树四类宿主 + 镜像同步 + 九个 Action;
- 宿主层:单页渲染器、屏幕目录 @Builder、登录/日志会话;
- 交付层:HAR 模块 + HAP 工程、签名、安装与真机验收。
9.3 三条经验
- Swift 源库的鸿蒙适配没有跨语言捷径,先把「纯逻辑 / 平台绑定」切开,逻辑层逐语义移植、平台层显式建模,比硬做桥接稳得多;
- 声明式 UI 下做路由库,需要一个显式的屏幕树作为唯一事实源——树是模型、页面是渲染器,Finder/Action 的语义就能原样保留;
- 纯逻辑库值得配一个 Node 复现环境(桩掉 ArkUI 依赖),真机问题先在桌面上复现再修,登录流程三处缺陷就是这样定位的。
9.4 适配成果
当前适配已完成:
- 上游 2.22.2 的 ArkTS 源码级移植(HAR 库模块,八个子模块、约 60 个公开导出);
- 九个路由场景的 DevEco Stage Demo(含登录拦截续航、防重入排队、动态加 Tab);
- README 四件套(含 6700+ 汉字中文主文档:映射总表、踩坑表、故障排查、已知限制);
- Node 复现环境的逻辑回归与真机功能用例验证;
- HAP 构建、签名、设备安装和真机运行效果图;
- AtomGit 项目文档、文章与源码链接(
https://atomgit.com/oh-tpc/route-composer)。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐


所有评论(0)