【OpenHarmony/HarmonyOS】ArkWeb 接入 Three.js 星图时的编译报错,我是怎么一步步修好的

最近我在做一个 HarmonyOS 项目,想把原本的原生星图场景改成:

HarmonyOS 负责应用壳,Web 负责 3D 星图渲染。

技术组合就是:

  • ArkUI / ArkTS
  • ArkWeb
  • Three.js

思路本身没有问题,但一上来就遇到了一组很典型、也很“折磨人”的编译错误。😅

比如这几个:

Function may throw exceptions. Special handling is required.

Object literals cannot be used as type declarations

Object literal must correspond to some explicitly declared class or interface

In an '@Entry' decorated component, the 'build' method can have only one root node

这篇文章我就结合我自己的项目,完整复盘一下:

  1. 这些错误为什么会出现
  2. ArkTS 在 Web 场景桥接里有哪些语法限制
  3. 我最后是怎么把这套 ArkWeb + Three.js 跑通的

如果你也在做 HarmonyOS 混合渲染、Web 场景接入、或者 3D 图谱可视化,这篇会比较有参考价值。🚀


一、项目背景:我为什么要在 HarmonyOS 里接 Web 星图?🌌

我这个项目叫 星图AI,核心目标不是做一个普通列表,而是做一个带空间感的 3D 知识星图

项目原本已经有一套原生实现,包括:

  • 节点三维坐标
  • 相机 yaw / pitch / scale 控制
  • 透视投影
  • 节点选中
  • 连线高亮
  • 词图生成

后来为了增强 3D 视觉表现,我决定把“星图渲染层”切到 Web,用 Three.js 做:

  • 发光节点
  • 星点背景
  • 空间连线
  • 轨道装饰
  • 旋转、缩放、选中交互

但是,HarmonyOS 原生层仍然保留:

  • Tab 壳结构
  • 弹层
  • Store 状态管理
  • 节点与连线数据

最终架构就变成了:

原生做业务,Web 做空间渲染。

这个思路本身很顺,但第一次把 ArkWeb 容器写出来时,编译器就开始“教育我”了。😂


二、这次踩到的报错,到底说明了什么?🧩

先看最核心的几个报错。

1. Object literals cannot be used as type declarations

我一开始在接口里直接写了内联对象类型:

interface XingtuWebNodePayload {
  id: string;
  title: string;
  note: string;
  tags: string[];
  position: {
    x: number;
    y: number;
    z: number;
  };
}

在很多 TypeScript 项目里,这么写很正常。
但是 ArkTS 对类型写法更严格,这种内联对象类型会直接报错。

2. Object literal must correspond to some explicitly declared class or interface

第二类报错出现在 map 返回对象时,比如:

const nodes: XingtuWebNodePayload[] = this.store.nodes.map((node: XingtuNode) => ({
  id: node.id,
  title: node.title,
  note: node.note,
  tags: node.tags,
  position: {
    x: node.position.x,
    y: node.position.y,
    z: node.position.z
  }
}));

这在普通 TS 里也很常见,但 ArkTS 对“匿名对象字面量”的推断更克制。
如果你没有给它一个更明确、更稳定的声明路径,它就容易报“对象字面量不够明确”的错。

3. Function may throw exceptions. Special handling is required

这个报错指向的是:

this.controller.runJavaScript(...)

意思很明确:

ArkTS 认为这个 API 可能抛异常,所以必须显式处理。

也就是说,你不能像在浏览器 JS 里那样随手直接调,需要包上异常处理。

4. build method can have only one root node

这一条其实是 ArkUI 组件结构规则。

我最开始在 build() 里是这么写的:

build() {
  if (this.pageReady && this.lastSyncedRevision !== this.revision) {
    this.syncGraphToWeb();
  }

  Web({
    src: this.webSource(),
    controller: this.controller
  })
}

问题就在于:
build() 前面做了逻辑判断,再直接返回 Web,会让 ArkUI 对“根节点结构”判断变得不稳定。

对 ArkUI 来说,**build() 必须足够“纯”**,最好就是老老实实返回一个根容器。


三、第一步修复:把内联对象类型拆出去 ✅

这个问题很好修,核心思路就是:

不要在接口里直接嵌匿名对象类型,单独声明一个接口。

所以我把它拆成了这样:

interface XingtuWebPositionPayload {
  x: number;
  y: number;
  z: number;
}

interface XingtuWebNodePayload {
  id: string;
  title: string;
  note: string;
  tags: string[];
  position: XingtuWebPositionPayload;
}

这一步看起来只是“写得更啰嗦了”,但对 ArkTS 来说,它得到的是:

  • 更明确的类型边界
  • 更稳定的编译分析路径
  • 更适合后续桥接序列化的数据结构

四、第二步修复:不要在 map 里直接返回复杂匿名对象 ✅

第二个坑也非常典型。

我最后没有继续写这种匿名对象返回:

this.store.nodes.map((node) => ({
  ...
}))

而是改成拆辅助方法:

private createPositionPayload(node: XingtuNode): XingtuWebPositionPayload {
  return {
    x: node.position.x,
    y: node.position.y,
    z: node.position.z
  };
}

private createNodePayload(node: XingtuNode): XingtuWebNodePayload {
  return {
    id: node.id,
    title: node.title,
    note: node.note,
    tags: node.tags,
    position: this.createPositionPayload(node)
  };
}

然后在 graphPayload() 里这么用:

const nodes: XingtuWebNodePayload[] = this.store.nodes.map((node: XingtuNode) => this.createNodePayload(node));
const edges: XingtuWebEdgePayload[] = this.store.edges.map((edge: XingtuEdge) => this.createEdgePayload(edge));

这么写的好处有三个:

  • ArkTS 更容易识别对象属于哪个明确类型
  • 代码可读性更好
  • 以后调桥接结构时,修改点更集中

五、第三步修复:runJavaScript 必须显式处理异常 ✅

这个坑其实很容易忽略,因为很多人下意识会把它当成普通 JS 调用。

但在 ArkTS 里,runJavaScript 需要更谨慎。

我最终的处理方式是:

private syncGraphToWeb(): void {
  if (!this.pageReady) {
    return;
  }

  const payload: string = JSON.stringify(this.graphPayload());
  try {
    this.controller.runJavaScript(`window.syncXingtuGraph && window.syncXingtuGraph(${payload});`);
    this.lastSyncedRevision = this.revision;
  } catch (_error) {
    return;
  }
}

这里至少做到了两件事:

  • 满足 ArkTS 的异常处理要求
  • 避免 Web 页面还没准备好时直接把组件打崩

如果你后面还想做得更完善,可以继续加:

  • 错误日志上报
  • 同步失败重试
  • 页面状态回退

但第一版先把编译和调用链跑通,是最重要的。👌


六、第四步修复:不要在 build() 里做前置同步逻辑 ✅

ArkUI 的 build() 最好只专注于“描述 UI 树”,不要在前面塞太多运行逻辑。

我最后把结构改成了:

build() {
  Stack() {
    Web({
      src: this.webSource(),
      controller: this.controller
    })
      .width('100%')
      .height('100%')
      .backgroundColor('#060B14')
  }
  .width('100%')
  .height('100%')
}

也就是说:

  • 根节点用 Stack
  • Web 作为容器内部子节点
  • 同步逻辑不再放到 build() 前置判断里

这一点很重要,因为 ArkUI 的 build() 只要足够稳定,很多奇怪的解析错误都会少很多。✨


七、第五步修复:revision 不是普通变量,它要变成受控属性 ✅

我这里还有一个非常关键的小坑:

为了让原生数据变化时同步到 Web,我一开始给 revision 直接加了 @Watch

@Watch('onRevisionChanged')
revision: number = 0;

结果编译器直接报:

Regular variable 'revision' can not be decorated with '@Watch'

这个报错的本质是:

普通成员变量不能直接挂 @Watch,它必须先成为受装饰属性。

最后我的修复方式是:

@Prop
@Watch('onRevisionChanged')
revision: number = 0;

这样 revision 就成为了父组件传入的受控属性,可以被监听。

对应监听方法:

private onRevisionChanged(): void {
  if (this.lastSyncedRevision !== this.revision) {
    this.syncGraphToWeb();
  }
}

这样一来,当原生层节点、连线、选中状态变化并推动 revision 更新时,Web 层就能收到同步信号。


八、最终修好的核心组件长什么样?📦

修完之后,这个组件的职责就非常清晰了:

@Component
export struct XingtuWebScene {
  store: XingtuGraphStore = new XingtuGraphStore(false);

  @Prop
  @Watch('onRevisionChanged')
  revision: number = 0;

  @Watch('onPageReadyChanged')
  @State pageReady: boolean = false;

  private controller: webview.WebviewController = new webview.WebviewController();
  private bridge: XingtuWebBridge = new XingtuWebBridge();
}

它完成的事情包括:

  • 加载本地 rawfile 页面
  • 向 Web 注入节点与连线数据
  • 把 Web 中的点击事件回传给原生
  • 监听原生 revision 更新触发同步

说白了,它就是一个:

HarmonyOS 与 Three.js 星图之间的桥接容器。


九、原生和 Web 是怎么互通的?🔁

这部分也是整个方案里最有价值的地方。

原生 → Web

原生层会序列化图数据,然后调用:

this.controller.runJavaScript(`window.syncXingtuGraph && window.syncXingtuGraph(${payload});`);

Web → 原生

通过 javaScriptProxy 暴露桥对象:

.javaScriptProxy({
  object: this.bridge,
  name: 'xingtuBridge',
  methodList: ['onNodeSelect'],
  controller: this.controller
})

Web 场景点击节点时,再调用:

window.xingtuBridge.onNodeSelect(nodeId);

这样整个数据链路就闭环了:

  • 原生管理业务状态
  • Web 负责渲染展示
  • 双向桥接传递用户行为和数据更新

这也是我认为 HarmonyOS 做复杂可视化时很值得采用的模式。🌟


十、这次修复后,构建结果怎么样?🧪

我修完之后,重新跑了:

/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw -p module=entry@default -p product=default assembleHap

结果是:

  • CompileArkTS 已经成功通过 ✅
  • 最终卡在 PackageHap 阶段

但这个卡点不是代码问题,而是当前机器缺少 Java Runtime:

Unable to locate a Java Runtime.

也就是说:

这次 ArkTS 编译错误已经解决了,剩下的是本机打包环境问题。

这个区分非常重要,不然很容易误以为自己代码还没修好。


十一、我对 ArkTS + ArkWeb 混合开发的一点经验总结 💡

这次踩完坑之后,我有几个非常明确的感受:

1. ArkTS 比普通 TypeScript 更严格

特别是在这些方面:

  • 对匿名对象更敏感
  • 对内联对象类型更敏感
  • 对异常处理要求更高
  • 对组件 build() 结构要求更严

2. 复杂桥接结构一定要显式建模

不要偷懒写一堆匿名对象。
显式的 interface / helper method 在 ArkTS 里真的更稳。

3. build() 要尽量“纯”

如果你把运行逻辑、状态同步、条件分支全塞在 build() 前面,很容易把问题复杂化。

4. 混合架构里,状态同步比渲染本身更重要

Three.js 页面能显示只是第一步。
真正麻烦也真正关键的,是:

  • 原生怎么推数据过去
  • Web 怎么回事件回来
  • 两边如何避免状态不同步

十二、这类项目还可以继续写哪些文章?📝

如果你和我一样,正在做 HarmonyOS 可视化项目,那这个方向其实还能继续拆很多篇:

  1. 【OpenHarmony/HarmonyOS】ArkWeb 接入 Three.js 的完整实战:从本地资源加载到双向桥接
  2. 【OpenHarmony/HarmonyOS】ArkTS 为什么总对对象字面量报错?一次讲清楚常见坑位
  3. 【OpenHarmony/HarmonyOS】如何把 HarmonyOS 原生数据层同步到 Web 3D 场景
  4. 【OpenHarmony/HarmonyOS】知识图谱项目里,原生壳 + Web 渲染层为什么更实用
  5. 【OpenHarmony/HarmonyOS】Three.js 做 3D 星图界面,如何嵌入 HarmonyOS 应用

结语

这次我做的不是单纯“修一个编译错误”,而是顺手把整个 ArkWeb + Three.js + ArkTS 的边界理清楚了。

如果你也在 HarmonyOS 里做:

  • Web 混合开发
  • 图谱可视化
  • 3D 场景渲染
  • 原生与 Web 双向通信

那我很建议你重点注意这四件事:

  • 类型显式化
  • 对象构造显式化
  • 异常处理显式化
  • build() 结构容器化

很多 ArkTS 报错看起来“离谱”,但你顺着这几个方向去整理,通常就能很快把问题压下去。💪

如果你愿意,我后面也可以继续把我这个星图项目拆成系列文章,把:

  • 3D 星图架构
  • ArkTS 数据层
  • Web 渲染层
  • Three.js 星图视觉实现

完整整理出来。🌠

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐