欢迎光临
我们一直在努力

开源鸿蒙平台 CMP 三方库「compose-webview-multiplatform」适配全流程

本文记录 compose-webview-multiplatform 接入 OpenHarmony 的完整过程,覆盖跨平台 WebView 库盘点、引擎/视图源码集拆分、真实协议引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkTS Web 组件真机页面、HAP 构建和真机验收。

compose-webview-multiplatform 是 JetBrains Compose Multiplatform 生态的 WebView 库:一个 WebView() composable 统一 Android(android.webkit.WebView)、iOS(WKWebView)、Desktop(KCEF/Chromium)与 WasmJS(iframe)四个平台,配套 WebViewState 状态记忆、WebViewNavigator 导航、window.kmpJsBridge JS 双向通信、CookieManager、请求拦截与 HTML 数据加载。本次适配与 KStore 的"真编译整库"路线有本质区别:上游把 API、状态与桥协议全部放在 commonMain,但其中 Compose 部分(WebView() composable、mutableStateOf 状态、协程导航)与社区鸿蒙线 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 对不齐——因此采用社区已过审的引擎复用、视图隔离路线(cmp_qrose 模式):JS 桥协议引擎(JsMessage / JsMessageDispatcher / WebViewJsBridge)、内容/状态/请求/Cookie 数据模型等纯 Kotlin 引擎原样编译成 OpenHarmony KLIB,视图层由 ArkTS Web 组件承载,页面侧 window.kmpJsBridge 协议与 Android 注入逐字节一致,真机上跑的是上游的协议,不是重写版。

项目地址: AtomGit/oh-tpc/compose-webview-multiplatform.git

上游仓库: KevinnZou/compose-webview-multiplatform

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

compose-webview-multiplatform 为 CMP 应用提供"网页视图"这一件事的完整答案:rememberWebViewState(url) 一行建状态,WebView(state, navigator, webViewJsBridge) 一步挂视图,页面里 window.kmpJsBridge.callNative("Greet", params, callback) 直接调原生、onCallback 收应答;native 侧 WebViewJsBridge.register(handler) 注册处理器,WebViewNavigator 提供 loadUrl / goBack / reload / evaluateJavaScript 全套导航,CookieManager 管 Cookie,RequestInterceptor 拦请求。CMP 项目把这些能力放在 commonMain,四个平台各出一个薄 actual 层。OpenHarmony 应用同样需要这套跨端 WebView 契约——尤其是已经写好 kmpJsBridge 页面与处理器的工程,但上游构建(Kotlin 2.1.20 / CMP 1.8.0 / AGP 8.9.3 / Gradle 8.13)不会生成 OpenHarmony 变体。

如果只在 ArkTS 里手写一个 WebView 页面,页面可以显示,JS 通信也能做,但协议就和 Android / iOS / Desktop 分叉了——同一套 H5 代码在不同端行为不一致。适配需要同时验证桥协议引擎、数据模型、Native 动态库、C ABI、N-API、ArkTS Web 组件页面和 HAP 交付链路。

障碍具体问题
目标缺失 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致 上游 Kotlin 2.1.20 / CMP 1.8.0 / AGP 8.9.3 与鸿蒙工具链(HarmonyOS Kotlin 2.2.21-1.0.0)无法共存于一个 Gradle 构建。
UI 与引擎耦合 commonMain 里 WebView() composable、WebViewState(mutableStateOf)、WebViewNavigator(协程 SharedFlow)、WebSettings(kermit/Compose Color)与 Compose 运行时深度耦合,ohos 制品线上没有对齐的 CMP 版本。
语言边界不同 ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
协议语义跨不了桥 WebViewJsBridge.dispatch → 处理器 → onCallback 回注脚本 的回调链是 Kotlin 侧概念,C ABI 只能同步一来一回。
交付链路复杂 .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:桥协议引擎与数据模型的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。视图层由 ArkTS Web 组件原生承载——这本来就是上游架构的延续(四个平台的 ActualWebView 本就是平台视图),鸿蒙只是加了第五条路线。

1.2 库提供的能力

Demo 提供一次真实的 JS 桥通信会话,对应桥接的全部入口:

  • 协议注入(script):webviewJsBridgeScript() 返回上游 IWebView.injectJsBridge 的初始化 JS 原文(Webview.jsBridgeInitScript() 逐字提取),ArkTS 在 onPageEnd 注入——与 Android/iOS 注入同一个字符串;
  • 消息分发(dispatch):页面 callNative("Greet", JSON.stringify({message}), cb) → postMessage → javaScriptProxy → WebviewDispatchMessage → JsMessage → JsMessageDispatcher → 处理器,返回引擎产出的回注脚本原文 window.kmpJsBridge.onCallback(3, '{"message":"KMP Received Hello"}'),ArkTS 原样 runJavaScript;
  • 错误通道:调用未注册的方法名返回 ok:false, no handler for 'Nope',HAP 不崩;
  • Cookie 模型:webviewCookieToString() 用上游 Cookie.toString() 拼 Set-Cookie 串;webviewFormatCookieDate() 用 ohos 纯 Kotlin 历法输出 EEE, dd MMM yyyy hh:mm:ss z(GMT),与 JVM 的 SimpleDateFormat 输出逐字节一致;
  • 版本握手:webviewVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应字段:

字段作用
ok 操作是否成功,失败时 error 带原文。
callbackId 透传页面回调 id(无回调时 -1)。
script dispatch 成功时引擎产出的 onCallback 回注脚本(这是回包的主体)。
version / upstream 版本握手返回 fork 版本与上游版本。
date / cookie Cookie 日期格式化 / Cookie 模型拼接结果。

引擎侧完整能力(消息分发、处理器注册表、协议脚本、内容/状态/请求/Cookie 全部数据模型)都在上游 commonMain 里原样随编译进入 .so,JVM 侧保留同一份引擎编译做交叉对照,真机页面作为验收宿主。

1.3 实现适配

维度要求
代码复用 引擎 13 个 commonMain 文件原样编译,不提取、不重写;手术只有两类(剥 @Serializable / @Immutable 注解、解耦 navigator 参数);新增 1 个公开入口 Webview.kt 与 2 个 cookie 日期 actual。
平台目标 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离 根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose UI 12 文件移入未注册的 src/composeMain,sample/ 与四个平台源码集留盘不进构建。
桥接稳定 使用 5 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律 引擎零第三方依赖(不引入 coroutines / serialization / kermit ohos 制品线),工具链因此可以选社区缓存最全的 2.2.21 线。
UI 完整 三 Tab 覆盖全部桥接入口:WebView 浏览、JsBridge 全链路、Engine 版本握手与 Cookie 模型。
仓库规范 项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkTS Web 组件页面不是 Compose Multiplatform UI 实现。上游的视图层本来就是各平台原生视图(AndroidView / UIKitView / SwingPanel / DOM),鸿蒙侧由 ArkWeb 承担同一角色;适配面集中在 composable 之下的协议引擎与数据模型。

二、实现路线图

第 1 阶段:项目初始化 ── 盘点模块与源码依赖度、划定引擎/composeMain 边界、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版(零依赖线)
第 3 阶段:引擎手术 ── 剥注解、解耦 navigator、注入脚本提取为公开入口、cookie 日期双 actual
第 4 阶段:桥协议建模 ── JsMessage JSON 契约、script 回包语义、错误通道
第 5 阶段:原生桥接 ── @CName C ABI ×5、C++ N-API 双签名包装、CMake 三件套
第 6 阶段:ArkWeb 宿主 ── Web 组件 + javaScriptProxy、HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界

webview/ 库本体(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
webview/src/commonMain/ 引擎 13 文件:jsbridge 协议 + Web/请求/Cookie 模型
webview/src/ohosMain/ 新增:getCookieExpirationDate actual(纯 Kotlin 历法)
webview/src/jvmMain/ 新增:同一 expect 的 JVM actual(SimpleDateFormat,对照用)
webview/src/composeMain/ 上游 Compose UI 12 文件,留盘不参与任何目标编译
webview/src/androidMain/ 等 上游四平台视图嵌入层,留盘不进 settings
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson + Greet 处理器)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 三 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
sample/ docs/ .github/ 上游示例、文档站与 CI,留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :webview 与 :example:nativeApp,rootProject.name = "ohos_webview"。上游的 dokka / ktlint / vanniktech 发布插件、AGP classpath、git hooks 任务全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin Multiplatform HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK 21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101)
OpenHarmony 目标 ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle 8.14.1(上游 wrapper 8.13 升一格;2.2.21 线的社区经验值)
引擎依赖 零第三方依赖(commonMain 不声明任何 implementation)
DevEco product 5.1.0(18) 兼容
DevEco Studio 5.1.0 Release
ABI arm64-v8a + x86_64
Bundle org.terminator.ohos.webview

export JAVA_HOME="/path/to/jdk-21" # Windows 下 gradlew 启动器也需要 JAVA_HOME
java -version

1.3 源码依赖度盘点

上游 commonMain 共 25 个 Kotlin 文件,逐一核对 import 后一分为二:

去向文件依赖形态处置
引擎(13) JsMessage / IJsMessageHandler / JsMessageDispatcher / WebViewJsBridge serialization 注解 + Compose 注解 + IWebView 引用 剥注解、解耦 webView 引用为 scriptRunner
引擎(13) WebContent / LoadingState / WebViewError / WebViewFileReadType 纯 Kotlin 原样(WebViewError 剥 @Immutable)
引擎(13) WebRequest / WebRequestInterceptResult 纯 Kotlin 原样
引擎(13) Cookie / CookieManager / Extension 纯 Kotlin + 1 处 expect 接口保留,expect 工厂移走、日期 expect 补 actual
composeMain(12) WebView() composable / WebViewState / WebViewNavigator / IWebView / WebViewBundle Compose runtime / 协程 / Compose 资源 git mv 留盘
composeMain(12) WebSettings / PlatformWebSettings / KLogger / Platform / RequestInterceptor / ConsoleBridge kermit / Compose Color / serialization-json git mv 留盘

判断标准只有一条:不 import Compose / coroutines / serialization / kermit 的文件才留在引擎。全库 expect 声明原有 11 处(NativeWebView、WebViewBundle、ActualWebView、WebViewFactoryParam、PlatformWebViewParams、defaultWebViewFactory、WebViewCookieManager、getPlatform ×3、getCookieExpirationDate),其中 10 处随 UI 移入 composeMain,引擎只保留 getCookieExpirationDate 一处——补 ohos / jvm 两个 actual,这与 KStore 全库唯一新增源文件是 8 行 Dispatchers actual 是同款"expect 最小化"纪律。

反向手术同样有先例可循:qrose 把写在 Painter 文件里的 Code128Type 抽回 commonMain(引擎引用了 UI 文件里的类型);本仓是镜像情形——引擎 WebViewJsBridge 引用了 UI 的 IWebView,解法是把 webView?.evaluateJavaScript(…) 解耦为 scriptRunner: ((String) -> Unit)?,由宿主注入执行器。注入脚本本体从 IWebView.injectJsBridge 的字符串模板逐字提取为公开入口:

// webview/src/commonMain/kotlin/com/multiplatform/webview/Webview.kt
object Webview {
const val VERSION: String = "2.0.4-ohos.1"
fun version(): String = VERSION

/** 与 IWebView.injectJsBridge 注入的初始化 JS 逐字相同 */
fun jsBridgeInitScript(jsBridgeName: String = "kmpJsBridge"): String = """
if (typeof window.
$jsBridgeName === 'undefined') {
window.
$jsBridgeName = { callbacks: {}, callbackId: 0,
callNative: function (methodName, params, callback) { … },
onCallback: function (callbackId, data) { … } };
}
"""
.trimIndent()
}

1.4 页面边界

标题区 三 Tab 布局(WebView / JsBridge / Engine)
WebView Tab 地址栏 + Go、进度条、Web 组件、Back/Forward/Reload、状态行
JsBridge Tab 状态条(bridge injected / handled / native error)
Web 组件加载 $rawfile greet.html + javaScriptProxy(kmpNativeBridge)
Native 日志区(JS> 请求 / KN< 响应,最新在前)
Engine Tab 版本握手行、注入脚本长度、Cookie 日期格式化、Cookie.toString()

页面启动时先调 webviewVersion() 做握手;JsBridge Tab 在 onPageEnd 注入协议脚本并接线 postMessage。

第 2 阶段:目标与依赖打通

2.1 制品线决策(零依赖线,本适配的路线手)

先查社区 Maven(https://maven.eazytec-cloud.com)上的鸿蒙制品分布:kotlin-stdlib 有 2.2.21-1.0.0 与 2.4.20-0.1.0-01…08 两条线;kotlinx-serialization-json 的 ohos 变体在 1.11.0-0.1.0-02/05/07(2.4.20 线);kotlinx-coroutines-core 在 1.10.2-0.5.0-07…15。

两条可选路线的取舍:走 2.4.20 线可以保留上游的 @Serializable JsMessage 与 serialization 助手,但要整体升 Kotlin 与 Gradle、与 qrose 已验证的缓存线分叉;走 2.2.21 线则引擎必须零依赖。盘点结果显示引擎唯一的 serialization 触点是 JsMessage 一个注解与两个内联助手——桥上解析的 JSON 只有一个扁平对象,MiniJson(qrose 同款)百行内解决。选 2.2.21-1.0.0 + 零依赖引擎:本机 Konan 工具链缓存(sysroot-ohos-* / cpf-llvm-*)直接复用,构建可复现;上游的 coroutines 1.10.2 / serialization 1.8.1 / kermit 2.0.3 依赖随 composeMain 留盘,接回时恢复。

2.2 根构建替换

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
// dependencyResolutionManagement 同款双仓置前

rootProject.name = "ohos_webview"

include(":webview")
include(":example:nativeApp")

根 build.gradle.kts 收敛为版本声明 + 测试纪律(group 沿用上游 io.github.kevinnzou,版本走 gradle.properties 的 version=2.0.4-ohos.1):

plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
}

allprojects {
group = "io.github.kevinnzou"
version = providers.gradleProperty("version").orElse("2.0.4-ohos.1").get()
}

subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}

gradle/libs.versions.toml 砍到只剩 HarmonyOS Kotlin 的 multiplatform 插件;wrapper 从 8.13 升到 8.14.1;gradle.properties 写死 JDK 21 路径并带上三条 kotlin.native 开关。

2.3 编译真实引擎

webview/build.gradle.kts 收敛为纯 KMP 模块:

plugins {
alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)

jvm() // 交叉验证用
ohosArm64()
ohosX64()

sourceSets {
commonMain.dependencies {} // 引擎零依赖
}
}

实测:

./gradlew :webview:compileKotlinJvm :webview:compileKotlinOhosArm64 :webview:compileKotlinOhosX64
# BUILD SUCCESSFUL

引擎文件的协议逻辑一行没动,改动只有"剥注解 / 去 navigator / 提取字符串常量"三类。

2.4 expect/actual 处置:cookie 日期的 ohos 实现

引擎保留的唯一 expect 是 getCookieExpirationDate(Long): String。上游 Android/Desktop actual 用 SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US) + GMT;JVM actual 照抄这份实现作对照,ohos actual 用纯 Kotlin 历法(KN 没有 SimpleDateFormat):

// webview/src/ohosMain/…/GetCookieExpirationDate.ohos.kt
actual fun getCookieExpirationDate(expiresDate: Long): String {
val days = floorDiv(floorDiv(expiresDate, 1000L), 86400L) // 自 1970-01-01 的天数
val (year, month, day) = civilFromDays(days) // Howard Hinnant 算法
val weekday = WEEKDAYS[floorMod(days + 4L, 7L).toInt()] // 1970-01-01 是周四
val h12 = h24 % 12; val hour12 = if (h12 == 0L) 12L else h12 // hh 12 小时制语义
return "$weekday, ${pad2(day)} ${MONTHS[month – 1]} $year ${pad2(hour12)}:${pad2(minute)}:${pad2(second)} GMT"
}

epoch 0 两边都输出 Thu, 01 Jan 1970 12:00:00 GMT——hh 是 12 小时制,00:00 输出 12,这个细节对不齐就会与 Android 逐字节比对失败。

第 3 阶段:桥协议建模

3.1 JSON 契约

com.multiplatform.webview.jsbridge/**(上游引擎,原样编译)
↓ JsMessage / JsMessageDispatcher / WebViewJsBridge
Bridge.kt(@CName × 5,扁平 JSON 进出)
↓ UTF-8 JSON
N-API webviewDispatchMessage() 等
↓
ArkTS Web 组件(javaScriptProxy → runJavaScript)

请求与响应:

请求 : {"methodName":"Greet","params":"{\\"message\\":\\"Hello\\"}","callbackId":3}
响应 : {"ok":true,"callbackId":3,"script":"window.kmpJsBridge.onCallback(3, '{\\"message\\":\\"KMP Received Hello\\"}')"}
{"ok":false,"error":"no handler for 'Nope'","callbackId":4}

params 是 JSON 文本的字符串转义形式(页面 JSON.stringify 一行可得,与上游 JsMessage.params 语义一致),内层结构对桥不透明、原样透传给处理器。

3.2 script 回包语义:回包不是数据,是引擎产出的脚本

这是本次桥协议的核心设计。WebViewJsBridge 的上游回调链是 处理器 callback(data) → onCallback(data, id) → webView.evaluateJavaScript("window.kmpJsBridge.onCallback(id, 'data')")。桥把最后一步的脚本原文作为回包返回,宿主在 scriptRunner 里捕获:

// example/nativeApp/src/ohosMain/kotlin/Bridge.kt
private val bridge = WebViewJsBridge().apply {
register(GreetJsMessageHandler()) // 与 sample/shared 同名同应答
scriptRunner = { script -> lastScript = script }
}

@CName("WebviewDispatchMessage")
fun webviewDispatchMessage(request: String): String = try {
val json = MiniJson.parse(request)
val message = JsMessage(json.int("callbackId", –1), json.str("methodName"), json.str("params"))
bridge.dispatch(message)
val script = lastScript
if (script == null) """{"ok":false,"error":"no handler …","callbackId":${message.callbackId}}"""
else """{"ok":true,"callbackId":${message.callbackId},"script":${jsonString(script)}}"""
} catch (t: Throwable) { """{"ok":false,"error":…}""" }

ArkTS 拿到 script 直接 controller.runJavaScript(script)——与 Android/iOS 的 evaluateJavaScript 完全同源。这样整条 dispatch → 处理器 → onCallback 链路全部在上游引擎内部走完,桥只搬运结果。

页面接线与 Android 逐字对照:

Android(AndroidWebView.kt)HarmonyOS Demo
addJavascriptInterface(this, "androidJsBridge") .javaScriptProxy({ object, name: 'kmpNativeBridge', methodList: ['call'] })
injectJsBridge() 注入初始化 JS webviewJsBridgeScript() 返回同一份字符串,onPageEnd 注入
window.kmpJsBridge.postMessage = function (m) { window.androidJsBridge.call(m); } window.kmpJsBridge.postMessage=function(m){window.kmpNativeBridge.call(m);}
Json.decodeFromString<JsMessage> + dispatch WebviewDispatchMessage(MiniJson → JsMessage → dispatch)
evaluateJavaScript(onCallback 脚本) scriptRunner 捕获同一句脚本,ArkTS runJavaScript

Demo 的 GreetJsMessageHandler 与上游 sample/shared 的同名处理器协议一致:方法名 Greet、应答 {"message":"KMP Received <name>"}——greet.html 不做任何改动就能在两端跑同一协议。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI;未注册方法名走 no handler 显式通道;非法 JSON、类型错误同样收敛到 ok:false,ArkTS 侧统一渲染。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接

ArkTS
│ JSON string
▼
C++ N-API entry(libwebview_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoswebview.so)
│ WebviewVersion / WebviewJsBridgeScript / WebviewDispatchMessage
│ WebviewFormatCookieDate / WebviewCookieToString
▼
UTF-8 JSON + DisposeString

4.2 Kotlin/Native 导出函数
C ABI 符号ArkTS 方法覆盖内核
WebviewVersion webviewVersion() 版本握手(真机版本行数据来源)
WebviewJsBridgeScript webviewJsBridgeScript() Webview.jsBridgeInitScript() 协议注入原文
WebviewDispatchMessage webviewDispatchMessage(req) JsMessage 构造 + WebViewJsBridge.dispatch 全链路
WebviewFormatCookieDate webviewFormatCookieDate(req) ohos actual 纯历法日期格式化
WebviewCookieToString webviewCookieToString(req) 上游 Cookie.toString() 模型拼接
4.3 C++ N-API 方法分发:两个签名的教训

五个 KN 入口分两类:三个带参(const char* f(const char*))、两个无参(const char* f(void))。C++ 函数指针不做隐式转换,最初把无参入口塞进带参包装直接编译失败(no known conversion from 'const char *()' to 'const char *(*)(const char *)')——这正是本适配真机联调时踩过并修掉的坑。最终形态是双入口包装 + 公共尾部:

// 公共尾部:KN 字符串 → JS 字符串,随后必须 DisposeString 归还 Kotlin 运行时
static napi_value KnStringToNapi(napi_env env, const char *raw, const char *name);

// 带参入口:napi_typeof 校验 string → OH_LOG_INFO 打字节数 → entry(request.c_str())
static napi_value CallStringEntry(napi_env, napi_callback_info,
const char *(*entry)(const char *), const char *name, bool requireArg);

// 无参入口:version / jsBridgeScript
static napi_value CallVoidStringEntry(napi_env, napi_callback_info,
const char *(*entry)(void), const char *name);

libohoswebview_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与五个业务符号),不拖整份生成头;nm_modname 为 webview_napi,与 ArkTS import 的 libwebview_napi.so、index.d.ts 导出名、napi_define_properties 四处严格一致。

4.4 CMake 和类型声明

add_library(ohoswebview SHARED IMPORTED)
set_target_properties(ohoswebview PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)

add_library(webview_napi SHARED napi_init.cpp)
target_link_options(webview_napi PRIVATE "-Wl,–no-undefined" "-Wl,-rpath,\\$ORIGIN")
target_link_libraries(webview_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoswebview)

OHOS_ARCH 为空或 libohoswebview.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const webviewVersion: () => string;
export const webviewJsBridgeScript: () => string;
export const webviewDispatchMessage: (request: string) => string;
export const webviewFormatCookieDate: (request: string) => string;
export const webviewCookieToString: (request: string) => string;

第 5 阶段:ArkWeb 宿主

5.1 javaScriptProxy 注入与消息触发

aboutToAppear(): void {
this.proxy.setHandler((msg: string) => this.onNativeMessage(msg));
this.refreshVersion(); // 版本握手:真实 Native 调用
}

private onNativeMessage(msg: string): void {
setTimeout(() => { // proxy 回调不在 UI 线程,先转回主线程
const res = JSON.parse(webviewDispatchMessage(msg)) as NativeResult;
this.bridgeLog = 'JS> ' + msg + '\\nKN< ' + res + '\\n\\n' + this.bridgeLog;
if (res.ok && res.script !== undefined) {
this.bridgeStatus = 'handled, callbackId=' + res.callbackId;
this.bridgeController.runJavaScript(res.script); // 引擎产物的 onCallback 脚本
} else {
this.bridgeStatus = 'native error: ' + res.error;
}
}, 0);
}

// onPageEnd 时注入协议 + postMessage 接线(与 Android 注入顺序一致)
private injectBridge(): void {
const parsed = JSON.parse(webviewJsBridgeScript()) as NativeResult;
const wiring = 'window.kmpJsBridge.postMessage=function(m){window.kmpNativeBridge.call(m);};';
this.bridgeController.runJavaScript(parsed.script + '\\n' + wiring);
this.bridgeStatus = 'bridge injected (kmpJsBridge ready)';
}

Web 组件挂载 javaScriptProxy({ object: this.proxy, name: 'kmpNativeBridge', methodList: ['call'] }),加载本地 $rawfile('greet.html')——页面提供名字输入框、Greet 按钮、未知方法按钮和滚动日志。

5.2 页面交互

WebView Tab:地址栏 + Go、onProgressChange 驱动进度条、Back/Forward/Reload(accessBackward 先检查)、主帧错误进状态行。JsBridge Tab:状态条实时显示 bridge injected → handled, callbackId=N,Native 日志区按 JS> 请求 / KN< 响应 成对留痕。Engine Tab:版本握手行、注入脚本长度、Cookie 日期格式化与 Cookie.toString() 两个演示按钮。

5.3 错误边界与 ArkTS 严格模式

ok:false 时错误原文进状态条与日志;Native 调用抛错(理论不该发生)同样被 try/catch 收进日志区。ArkTS 严格模式在联调时贡献了三个真实编译错:Tabs 底部栏是 BarPosition.End(没有 Bottom);Web 事件是 onPageBegin(不是 onPageStart);事件回调参数必须显式类型(OnProgressChangeEvent / OnErrorReceiveEvent),否则 arkts-no-any-unknown 拒绝编译。

第 6 阶段:示例与验证

6.1 工程结构

example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoswebview)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/Bridge.kt @CName × 5 + MiniJson + Greet 处理器
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.webview、versionName 2.0.4-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 双签名导出、参数检查、DisposeString
├── src/main/cpp/include/libohoswebview_api.h
├── src/main/cpp/types/libwebview_napi/{index.d.ts, oh-package.json5}
├── src/main/resources/rawfile/greet.html 本地协议验收页
└── src/main/ets/pages/Index.ets ArkTS 三 Tab 验收页面

6.2 构建、签名和安装

./gradlew :webview:compileKotlinJvm :webview:compileKotlinOhosArm64 :webview:compileKotlinOhosX64
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug –no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机配置,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。

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.webview -m entry

四、完整代码对照

4.1 整体架构

com.multiplatform.webview.jsbridge/**(上游协议引擎原样编译)
│ JsMessage / JsMessageDispatcher / WebViewJsBridge / IJsMessageHandler
com.multiplatform.webview.{web,request,cookie}/**(上游数据模型原样编译)
│ WebContent / LoadingState / WebViewError / WebRequest / Cookie
Webview.kt(新增公开入口:版本 + 协议脚本逐字提取)
Bridge.kt + MiniJson
│ @CName × 5
libwebview_napi.so
│ webviewVersion / webviewJsBridgeScript / webviewDispatchMessage / …
Index.ets + Web 组件
│ javaScriptProxy + runJavaScript
▼
ArkTS 真机页面(三 Tab)

4.2 文件清单

文件职责
settings.gradle.kts 极简 ohos 构建、eazytec Maven 置前、ohos_webview、sample 移出
gradle/libs.versions.toml 只留 HarmonyOS Kotlin 2.2.21-1.0.0 multiplatform 插件
build.gradle.kts(根) 版本 2.0.4-ohos.1、全局禁用 Ohos*Test* 任务
webview/build.gradle.kts 纯 KMP:jvm() + ohosArm64 + ohosX64,零依赖
webview/src/commonMain/(13 文件) 引擎:剥注解 / 解耦 navigator 后原样编译
webview/src/composeMain/(12 文件) 上游 Compose UI 留盘,接回上游工具链时恢复
webview/src/{ohosMain,jvmMain}/…/GetCookieExpirationDate.*.kt 唯一 expect 的双 actual(纯历法 / SimpleDateFormat)
webview/src/commonMain/…/Webview.kt 公开入口(版本 + 注入脚本逐字提取)
example/nativeApp/build.gradle.kts 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/…/Bridge.kt 5 个 C ABI、scriptRunner 捕获、MiniJson、Greet 处理器
harmonyApp/…/cpp/napi_init.cpp N-API 双签名导出、参数检查、DisposeString
harmonyApp/…/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/…/ets/pages/Index.ets ArkTS 三 Tab 验收页面
README.OpenHarmony_CN.md 鸿蒙适配主文档(27 节)

4.3 关键 API 对照

层次API作用
引擎 JsMessage(callbackId, methodName, params) 页面 → native 的消息模型(桥请求体同名同义)
引擎 JsMessageDispatcher.dispatch 方法名 → 处理器路由
引擎 WebViewJsBridge.dispatch(message) 全链路入口,内部走 onCallback
引擎 IJsMessageHandler.handle(message, callback) 处理器接口(Demo 的 Greet 实现)
引擎 Cookie.toString() / getCookieExpirationDate Set-Cookie 拼接 / GMT 日期格式化
入口 Webview.jsBridgeInitScript() 与 IWebView.injectJsBridge 逐字相同的注入脚本
Native WebviewDispatchMessage 等 × 5 C ABI JSON 导出
N-API webviewDispatchMessage(req) 等 向 ArkTS 暴露能力
ArkTS onNativeMessage(msg) proxy 回调 → 转主线程 → dispatch → runJavaScript(script)

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(webviewDispatchMessage(JSON.stringify({
methodName: 'Greet',
params: JSON.stringify({ message: 'Hello' }),
callbackId: 1
})));
// res.ok === true
// res.script === "window.kmpJsBridge.onCallback(1, '{"message":"KMP Received Hello"}')"
this.controller.runJavaScript(res.script);

val bridge = WebViewJsBridge().apply {
register(GreetJsMessageHandler())
scriptRunner = { script -> lastScript = script } // 宿主注入的执行器
}
bridge.dispatch(JsMessage(1, "Greet", """{"message":"Hello"}"""))
// lastScript == "window.kmpJsBridge.onCallback(1, '{"message":"KMP Received Hello"}')"

两者之间只传输扁平 JSON 字符串与脚本原文,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:拆引擎而不是真编译整库

KStore 能整库编译,是因为它的 commonMain 零 UI 耦合;本库的公共层一半是 Compose(composable、mutableStateOf 状态、协程导航、Compose 资源),ohos 制品线上没有与之对齐的 CMP。把引擎从 UI 里剥出来——协议、模型是纯 Kotlin,视图本来就是平台原生——既守住了"页面协议逐字一致"的跨端价值,又不必等 CMP 工具链对齐。

决策 2:工具链选 2.2.21-1.0.0 零依赖线,Gradle 8.14.1

引擎剥到零依赖后,kotlinx ohos 制品线(serialization 1.11.0-0.1.0-xx、coroutines 1.10.2-0.5.0-xx,都在 2.4.20 线)就没有存在必要——为 JsMessage 一个注解升整条工具链不划算。2.2.21 线与 qrose 完全同款,本机 Konan 缓存(sysroot / LLVM)直接复用;Gradle 按 2.2.21 线经验值从 8.13 升到 8.14.1。桥上 JSON 用 MiniJson 解决,与 qrose 的取舍一致。

决策 3:根构建替换(cmp_qrose 模式),Compose UI 与 sample 留盘

上游根构建(dokka / ktlint / vanniktech / AGP / git hooks)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。12 个 Compose 文件 git mv 到未注册的 src/composeMain(保留 git 重命名历史),sample/ 四端示例与平台源码集留盘不进 settings.gradle.kts——它们的存在方式只有这一种,接回上游工具链时原样恢复。

决策 4:script 回包语义 + navigator 解耦

桥的回包不是处理器的数据,而是引擎 onCallback 产出的回注脚本原文——宿主 runJavaScript 执行的字符串与 Android evaluateJavaScript 的完全相同,"引擎复用"从签名级提升到字节级。配套解耦是 navigator:导航属于宿主页面(ArkTS 路由 / Compose Navigator),引擎只管消息路由,WebViewJsBridge 的 webView: IWebView 引用换成 scriptRunner: ((String) -> Unit)?。

决策 5:cookie 日期 ohos 纯历法 + JVM 对照

引擎唯一保留的 expect 是 getCookieExpirationDate。ohos actual 不引任何日期库,纯 Kotlin 历法(civil-from-days + 周四锚点 + 12 小时制)拼出与 SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z") + GMT 逐字节一致的输出;JVM actual 照抄上游实现作对照。这是"expect 最小化 + 可对账"纪律在本库的落点,对应 KStore 的 8 行 Dispatchers actual。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Eclipse Adoptium 21.0.12.101)、DevEco Studio 5.1.0(18)、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 编译与符号验证

:webview:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: WebviewVersion / WebviewJsBridgeScript / WebviewDispatchMessage /
WebviewFormatCookieDate / WebviewCookieToString 均为全局 T 符号(双 ABI)

实测产物:

产物大小
arm64-v8a/libohoswebview.so 2,379,632 B(约 2.3 MB)
arm64-v8a/libc++_shared.so 1,850,184 B
x86_64/libohoswebview.so 2,014,016 B(约 2.0 MB)
x86_64/libc++_shared.so 1,731,432 B

链接期 CAdapter 对 @CName 字符串入口有 5 条 non-void function does not return a value in all control paths 告警(每个入口一条),与 qrose 相同,属无害告警。JVM 侧同一份引擎编译通过,构成"无 Compose、无三方库前提下自洽"的交叉对照;Webview.jsBridgeInitScript() 与 composeMain 里 IWebView.injectJsBridge 的字符串常量逐字比对一致。

6.3 HAP 验证

BuildNativeWithNinja: finished(双 ABI 编出 libwebview_napi.so,三件套同目录)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

联调过程中真实踩掉两轮编译错(N-API 函数指针签名、ArkTS 严格模式三处),均已修复入库——细节见 8.1 踩坑复盘。

6.4 功能用例

  • 启动 Engine Tab 即出现 engine 2.0.4-ohos.1 (upstream 2.0.4)(真实 Native 握手,非静态文字);
  • WebView Tab 默认加载 example.com,进度条随 onProgressChange 走到 100,Back/Forward/Reload 可用;
  • JsBridge Tab 加载本地 greet.html,onPageEnd 后状态条显示 bridge injected (kmpJsBridge ready);
  • 输入名字点 Call Native Greet,页面日志出现 JS <- onCallback: {"message":"KMP Received Hello"},Native 日志出现 KN< {"ok":true,"callbackId":N,"script":"window.kmpJsBridge.onCallback(N, '…')"},状态条变 handled, callbackId=N;
  • 点 Call Unknown Method,页面无回调(预期),Native 侧 ok:false, no handler for 'Nope',HAP 不崩;
  • Engine Tab 点 Format cookie expiration,输出形如 Wed, 21 Oct 2026 xx:xx:xx GMT(12 小时制,与 JVM 对照);点 Build Cookie.toString(),输出 session=kmp; Path=/; Domain=example.com; Expires=…; Max-Age=3600; SameSite=LAX;。
  • 6.5 验证结论

    协议引擎、数据模型、Native 动态库、C ABI、N-API、ArkTS Web 组件页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的桥通信会话:协议注入、Greet 全链路应答、错误通道与 Cookie 模型闭环(见下节效果图)。

    七、运行效果

    7.1 真机截图

    在这里插入图片描述

    7.2 命令速查

    git clone https://atomgit.com/oh-tpc/compose-webview-multiplatform.git
    cd compose-webview-multiplatform
    export JAVA_HOME="/path/to/jdk-21"
    ./gradlew :webview:compileKotlinJvm :webview:compileKotlinOhosArm64 :webview:compileKotlinOhosX64
    ./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
    cd example/harmonyApp
    hvigorw assembleHap -p product=default -p buildMode=debug –no-daemon
    hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
    hdc shell aa start -a EntryAbility -b org.terminator.ohos.webview -m entry
    hdc shell hilog | grep Webview

    八、遗留问题与改进方向

    8.1 踩坑复盘

  • 只改 ArkTS 页面不算 KMP 适配,必须让 :webview 真正编译成 ohosArm64 / ohosX64 动态库;
  • settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  • Windows 下 gradlew 启动器读不到 gradle.properties 里的 org.gradle.java.home,必须另设 JAVA_HOME 环境变量,否则报 JAVA_HOME is not set;
  • @CName 无参与带参入口的 C 函数指针签名不同(const char*(void) vs const char*(*)(const char*)),C++ 包装必须两个重载,硬塞一个签名直接 no matching function for call 编译失败;
  • ArkTS 严格模式三连:Tabs 底部栏是 BarPosition.End(没有 Bottom);Web 事件名是 onPageBegin(不是 onPageStart);事件回调参数必须显式类型((event: OnProgressChangeEvent)),否则 arkts-no-any-unknown 拒绝编译;
  • javaScriptProxy 的回调不在 UI 线程,runJavaScript 与 @State 更新都要先 setTimeout(…, 0) 转回主线程;
  • 页面就绪日志早于 onPageEnd 注入属预期时序,别把 kmpJsBridge = undefined 误判为注入失败——以状态条与按钮回路为准;
  • entry/build-profile.json5 的 abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可,缺 so 安装直接报 9568347;
  • KN 返回字符串必须经 libohoswebview_symbols()->DisposeString 归还 Kotlin 运行时,N-API 层不能 free();
  • build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组,勾选 DevEco 自动签名后由 IDE 回填,提交前 git update-index –skip-worktree 保护,证书绝不入库;
  • 根锚定 gitignore 盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/.idea/、entry/build/、entry/libs/ 要单独追加 gitignore;
  • 改了 Kotlin 源码要重跑 linkDebugShared* 再打 HAP,CMake 只认 entry/libs 里的文件时间戳。
  • 8.2 已知问题

    • WebView() composable、WebViewState / WebViewNavigator(Compose 状态与协程)、WebSettings / PlatformWebSettings 未进引擎,设置项在 ArkTS Web 组件属性上直接配置;
    • RequestInterceptor 接口(带 navigator 参数)随 UI 留盘,WebRequest / WebRequestInterceptResult 模型在引擎里但拦截执行依赖 ArkTS onInterceptorRequest,Demo 未覆盖;
    • ConsoleBridge(依赖 serialization-json)未进引擎,控制台转发未覆盖;
    • 引擎内 JsMessage 剥除了 @Serializable,序列化助手留在 composeMain——接回上游工具链时随 UI 一起恢复;
    • 桥回调数据沿用上游「单引号包裹」约定(onCallback(id, '…')),数据含单引号时需先转义——与 Android 行为一致,不是鸿蒙退化;
    • Demo 的 bridge 是进程级单例(scriptRunner 捕获最后一次脚本),多 WebView 实例场景需为每个控制器建独立 bridge;
    • Android / iOS / Desktop / WasmJS 目标与 sample/ 剥离(源文件留在树里),发布、dokka、ktlint 一并剥离,本 fork 不发 Maven Central;
    • HAP 安装必须使用与 bundle name org.terminator.ohos.webview 匹配的签名。

    8.3 未来优化方向

    • 把 WebSettings 的纯数据字段下沉引擎,供 ArkTS 侧生成配置(当前依赖 KLogger / Color 的部分留 UI);
    • 桥支持多 bridge 实例与脚本队列语义,对齐上游"每 WebView 一个 bridge"的持有方式;
    • 补充请求拦截入口(ArkTS onInterceptorRequest ↔ 引擎 WebRequestInterceptResult 模型);
    • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
    • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
    • 跟踪社区 CMP ohos 线:compose.ui ohos 制品与上游 CMP 对齐后接回 composeMain,引擎与 ArkTS 承载降级为过渡方案。

    九、总结

    9.1 核心难点回顾

    上游 webview commonMain(25 文件)
    → 源码依赖度盘点:13 引擎 + 12 Compose(标准:不 import Compose/协程/serialization/kermit)
    → 工具链对齐 Kotlin 2.2.21-1.0.0(零依赖线,Gradle 8.14.1)
    → 根构建替换(cmp_qrose 模式)
    → 引擎手术:剥注解 / 解耦 navigator / 注入脚本逐字提取为公开入口
    → ohosArm64 / ohosX64 KLIB + cookie 日期双 actual
    → C ABI(@CName × 5)+ script 回包语义
    → C++ N-API(双签名包装 + DisposeString 纪律)
    → ArkTS Web 组件 + javaScriptProxy(kmpNativeBridge ↔ androidJsBridge)
    → 真机三 Tab 验收(协议注入 / Greet 全链路 / Cookie 模型)

    9.2 封装层次

    • 引擎层:上游原版 jsbridge 协议(JsMessage / 分发器 / WebViewJsBridge)与 Web/请求/Cookie 数据模型;
    • 入口层:新增 Webview 公开入口(版本 + 与 injectJsBridge 逐字相同的协议脚本);
    • Native 层:生成双 ABI 动态库并通过 5 个 C ABI 输出 JSON 与脚本原文;
    • N-API 层:完成参数检查、字符串转换和 Native 内存释放(双签名包装);
    • ArkTS 层:管理页面状态、协议注入、proxy 回调转主线程、runJavaScript 回注与日志;
    • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

    9.3 三条经验

  • 适配路线按"库的耦合形态"选:KStore 的 commonMain 零 UI 耦合,正确答案是真编译整库;本库 UI 与协议混在 commonMain,正确答案是拆引擎——跨端库的复用主体是契约(这里是 window.kmpJsBridge 协议与消息模型),路线选择围绕"契约能不能原样过桥"展开;
  • 零依赖引擎让工具链选择自由:引擎剥干净后不需要 kotlinx ohos 制品线,就能留在缓存最全、社区验证最多的 2.2.21 线上——依赖越少,适配的自由度越大,这条优先级应该排在"保留注解便利性"之前;
  • “页面能感知的行为逐字一致、宿主感知的行为原生承载"是可执行的验收口径:注入脚本逐字提取、回包是引擎产出的 onCallback 脚本原文、Greet 应答与 sample 同名同格式——三者叠加,才能宣称"真机跑的是上游协议,不是重写版”。
  • 9.4 适配成果

    当前适配已完成:

    • compose-webview-multiplatform 2.0.4 协议引擎的双 ABI Kotlin/Native 编译(引擎 13 文件零重写,手术仅剥注解与解耦两类,新增 1 个公开入口 + 2 个 cookie 日期 actual);
    • 5 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证双 ABI 全局 T 符号,arm64 约 2.3 MB / x86_64 约 2.0 MB);
    • DevEco Stage ArkTS 三 Tab 验收页面(WebView 浏览、JS 桥全链路、版本握手与 Cookie 模型);
    • greet.html 与 Android 示例页协议级兼容(方法名、应答格式、注入脚本逐字一致);
    • HAP 构建(双 ABI 三件套)、签名、设备安装和真机验证;
    • AtomGit 项目文档、OpenHarmony README(中英双份,中文主文档 27 节)和本地效果图。

    适配过程 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。

    参考文档

    • AtomGit/oh-tpc/compose-webview-multiplatform
    • KMP&CMP 鸿蒙社区
    • Kotlin Multiplatform
    • OpenHarmony ArkWeb(Web 组件)
    • OpenHarmony N-API
    • DevEco Studio
    • OpenHarmony ArkUI
    赞(0)
    未经允许不得转载:171主机测评 » 开源鸿蒙平台 CMP 三方库「compose-webview-multiplatform」适配全流程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址