【OpenHarmony/HarmonyOS】我把原生星图改成了 Web 渲染层:用 ArkWeb + Three.js 做一个可交互的 3D 星图

大家好,最近我在做一个 HarmonyOS 项目,名字叫 星图AI
它的核心不是传统的列表页,而是一个带空间感的 3D 星图知识网络。✨

一开始,这个星图场景是用 ArkUI 原生方式来做的,自己维护节点、连线、相机和投影关系;后来在项目推进过程中,我做了一个新的决定:

星图这块不再继续走纯原生绘制,而是改成 HarmonyOS 容器 + Web 渲染层的模式。

也就是说:

  • HarmonyOS 负责应用壳、Tab、弹层、数据状态
  • Web 负责 3D 场景渲染
  • Three.js 负责节点、连线、发光、景深和交互

这篇文章就来完整分享一下这次改造思路,适合以下同学阅读 👇

  • 想在 HarmonyOS 里接入 Web 3D 能力的开发者
  • 想做知识图谱、关系图、星图、脑图类项目的开发者
  • 想把原生与 Web 混合架构做得更顺滑的开发者

一、为什么我要把星图从原生切到 Web?🤔

先说结论:不是原生不能做,而是 Web + Three.js 在 3D 渲染表达上更灵活。

我这个项目原本已经有一套原生 3D 星图能力,包括:

  • 节点三维坐标建模
  • 相机 yaw / pitch / scale 控制
  • 点位旋转与透视投影
  • 节点缩放、透明度、层级排序
  • 节点选中、连线高亮

比如原生侧的投影核心,本质上就是先旋转点,再基于相机距离做透视换算:

export function projectNode(node: XingtuNode, camera: CameraState, viewport: ViewportSize): ProjectedNode {
  const rotated: Vec3 = rotatePoint(node.position, camera.yaw, camera.pitch);
  const depth: number = camera.distance - rotated.z;
  const perspective: number = CAMERA_FOCAL / Math.max(220, depth);
  const halfWidth: number = viewport.width / 2;
  const halfHeight: number = viewport.height / 2;

  return {
    id: node.id,
    title: node.title,
    note: node.note,
    tags: node.tags,
    screenX: halfWidth + rotated.x * perspective * camera.scale,
    screenY: halfHeight + rotated.y * perspective * camera.scale,
    scale: perspective * camera.scale,
    opacity: Math.max(0.28, Math.min(1, 0.2 + perspective * 0.35)),
    depth
  };
}

这套原生思路没有问题,而且非常适合写成“ArkTS 3D 投影”文章。
但如果目标是继续增强下面这些体验:

  • 更自然的 3D 光照
  • 更强的空间氛围
  • 更细腻的发光和粒子效果
  • 更低成本的视觉迭代
  • 后续接入更多 3D 表现能力

那么 Three.js 的表达力会更舒服。🚀

所以我最后采用了一个折中但实用的方案:

业务留在 HarmonyOS,渲染下沉到 Web。


二、这次改造后的整体架构是什么样的?🧠

改造后的架构可以理解成三层:

1. 应用壳层:HarmonyOS ArkUI

这一层继续保留:

  • 底部 Tab
  • 页面导航
  • 新增节点弹层
  • 词图生成弹层
  • 节点详情弹层
  • 数据状态管理

2. 数据层:ArkTS Store

我项目里用一个 XingtuGraphStore 来管理节点和边,结构非常清晰:

export class XingtuGraphStore {
  nodes: XingtuNode[] = [];
  edges: XingtuEdge[] = [];
  selectedNodeId: string | null = null;
  linkingSourceId: string | null = null;
  camera: CameraState = defaultCamera();
}

它不仅管理数据,还负责:

  • 新增节点
  • 连接节点
  • 删除节点
  • 更新节点
  • 词图生成
  • 相机状态维护

这意味着即使渲染层换成 Web,核心业务模型并没有被破坏。✅

3. 渲染层:ArkWeb + Three.js

这一层是这次文章的重点。

我新增了一个 Web 星图容器组件:

@Component
export struct XingtuWebScene {
  store: XingtuGraphStore = new XingtuGraphStore(false);
  revision: number = 0;
  onNodeTap: (nodeId: string) => void = () => {};
  @State pageReady: boolean = false;
  private controller: webview.WebviewController = new webview.WebviewController();
}

它的职责很明确:

  • 加载本地内置 Web 页面
  • 把原生节点数据同步给 Web
  • 接收 Web 场景中的节点点击事件
  • 保证 ArkUI 与 Three.js 的状态是同步的

三、HarmonyOS 里怎么加载本地 Web 星图?📦

很多同学做混合开发时,第一反应是加载一个线上 H5 地址。
但我这个项目没有这么做,而是直接把 Web 资源打进应用里,走本地资源加载。

对应的关键代码如下:

private webSource(): Resource {
  return $rawfile('xingtu-web/index.html');
}

Web({
  src: this.webSource(),
  controller: this.controller
})
  .width('100%')
  .height('100%')
  .javaScriptAccess(true)
  .domStorageAccess(true)
  .fileAccess(true)

这里有几个好处:

  • 不依赖网络环境
  • 首屏更稳定
  • 发布时资源跟随应用一起交付
  • Web 渲染层更适合作为“内置能力模块”

四、Web 资源怎么同步到 HarmonyOS rawfile?🛠️

为了让 Web 资源真正变成应用内置页面,我给 web-demo 增加了一条自动同步脚本。

它会把构建产物中的 JS 和 CSS 同步到:

entry/src/main/resources/rawfile/xingtu-web

核心脚本如下:

const targetRoot = path.resolve(projectRoot, '../entry/src/main/resources/rawfile/xingtu-web');

await fs.mkdir(targetRoot, { recursive: true });
await fs.copyFile(sourceJs, path.join(targetRoot, 'app.js'));
await fs.copyFile(sourceCss, path.join(targetRoot, 'app.css'));
await fs.writeFile(path.join(targetRoot, 'index.html'), rawfileHtml, 'utf8');

这样做完之后,HarmonyOS 侧就能直接通过:

$rawfile('xingtu-web/index.html')

来加载页面。

我这里还顺手做了一个 npm 脚本:

"build:rawfile": "vite build && node ./scripts/sync-harmony.mjs"

也就是说,后面你每次更新 Three.js 页面,只要跑一次:

npm run build:rawfile

就能自动同步到 HarmonyOS 工程里。这个体验真的很丝滑。😄


五、原生数据怎么同步到 Web?这是这次改造的关键 🔁

如果只是把页面嵌进去,其实意义不大。
真正重要的是:

原生侧的数据变化,要能实时驱动 Web 场景刷新。

我在 XingtuWebScene 里做了一个数据桥接方法:

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

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

这个思路很直接:

  • 原生层从 store 中拿到 nodes / edges / selectedNodeId
  • 序列化成 JSON
  • 调用 Web 页面里的 window.syncXingtuGraph(...)
  • 让 Three.js 侧重建或刷新场景

这一步完成之后,原生侧的新增节点、删除节点、词图生成,都能无缝反映到 Web 星图里。


六、Three.js 页面如何接收这些数据?🌌

在 Web 侧,我专门暴露了一个全局同步方法:

window.syncXingtuGraph = (payload: XingtuGraphPayload) => {
  rebuildGraph(payload);
};

这里的 rebuildGraph 就是场景更新核心。

它做的事情包括:

  • 清理旧节点、旧连线
  • 把原生节点转换成 Three.js 运行时节点
  • 构建发光球体、光环、轨道
  • 构建弧形连线
  • 更新标签和信息面板

例如节点数据进入 Three.js 之前,我做了一次统一标准化:

function normalizeNode(node: XingtuGraphNode, index: number): RuntimeNode {
  const scaledPosition = new THREE.Vector3(
    node.position.x / 10,
    -node.position.y / 10,
    node.position.z / 10
  );
  const isCore = node.tags.includes('root') || index === 0;

  return {
    id: node.id,
    title: node.title,
    note: node.note,
    tags: node.tags,
    position: scaledPosition,
    color: isCore ? '#F2F9FF' : palette[index % palette.length],
    size: isCore ? 2.35 : 1.18 + (Math.min(node.tags.length, 4) * 0.08)
  };
}

这里我做了几件挺实用的小事:

  • 把原生坐标缩放到更适合 Three.js 的空间尺寸
  • 把 Y 轴反过来,贴合页面视觉
  • 给核心节点更亮的颜色和更大的尺寸
  • 根据标签数量给节点一些轻微的体量差异

这类“运行时标准化”特别值得保留,因为它能让你的业务数据和渲染表现解耦。✨


七、Web 场景里的交互怎么回传给原生?👆

光有数据单向同步还不够。
如果用户在 Three.js 星图里点了一个节点,HarmonyOS 原生层也必须知道。

为此,我在 ArkTS 侧做了一个桥对象:

class XingtuWebBridge {
  onNodeTapCallback: (nodeId: string) => void = () => {};

  onNodeSelect(nodeId: string): void {
    this.onNodeTapCallback(nodeId);
  }
}

再通过 javaScriptProxy 暴露给 Web:

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

对应的 Web 侧,当 Three.js 场景里点击到节点时:

function emitNodeSelection(nodeId: string): void {
  if (typeof window.xingtuBridge?.onNodeSelect === 'function') {
    window.xingtuBridge.onNodeSelect(nodeId);
  }
}

这样就打通了双向链路:

  • 原生 → Web:同步节点和边
  • Web → 原生:回传节点点击事件

这也是混合架构里最关键的一步。✅


八、Three.js 这版星图我具体做了哪些效果?✨

虽然这篇文章重点是架构,但视觉体验也值得说一说。

当前这版 Web 星图包含这些效果:

  • 🌠 背景星点粒子
  • 💡 中心节点高亮发光
  • 🪐 节点外环轨道装饰
  • 🔗 弧形空间连线
  • 🌫️ 深色宇宙雾化背景
  • 🖱️ 拖拽旋转视角
  • 🔍 滚轮缩放场景
  • 📌 点击节点切换信息卡

例如连线不是简单的直线,而是用了二次贝塞尔曲线:

function createConnection(from: THREE.Vector3, to: THREE.Vector3): THREE.Line {
  const middle = from.clone().add(to).multiplyScalar(0.5);
  middle.y += 4.5;
  middle.z += 1.5;

  const curve = new THREE.QuadraticBezierCurve3(from, middle, to);
  const points = curve.getPoints(48);
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial({
    color: '#7CC8FF',
    transparent: true,
    opacity: 0.38
  });

  return new THREE.Line(geometry, material);
}

这一点非常关键,因为“星图感”很多时候不是靠节点本身,而是靠 空间中的关系线条 来建立氛围。


九、为什么这个方案比“纯 Web 页面”更适合 App?📱

有同学可能会说:

既然都用 Three.js 了,那为什么不直接做成纯前端 App?

我的答案是:因为这个项目本质上还是一个 HarmonyOS 应用,而不是单纯网页。

HarmonyOS 这边仍然有很多不可替代的价值:

  • 原生页面组织和导航
  • 更自然的系统级交互承接
  • 弹层、表单、设置页等业务 UI 更好维护
  • 本地资源组织更清晰
  • 后续更方便接设备能力、存储能力和系统能力

所以最适合我的方案,不是“全原生”也不是“全 Web”,而是:

业务原生化,3D 场景 Web 化。

这个边界一旦划清楚,开发体验会非常舒服。


十、从项目角度看,这个选题为什么适合发 CSDN?📝

我看了自己整个项目的实现内容,实际上可以拆出很多篇文章,但当前最有传播性的,确实是这篇:

你这个项目目前适合写的选题

  1. 【OpenHarmony/HarmonyOS】我把原生星图改成了 Web 渲染层:用 ArkWeb + Three.js 做一个可交互的 3D 星图
  2. 【OpenHarmony/HarmonyOS】如何在 ArkTS 中实现 3D 节点透视投影与空间旋转
  3. 【OpenHarmony/HarmonyOS】一个知识图谱应用的数据层应该怎么设计:XingtuGraphStore 实战
  4. 【OpenHarmony/HarmonyOS】关键词一键生成词图网络:从输入词组到星图节点布局
  5. 【OpenHarmony/HarmonyOS】ArkUI 科技感弹层怎么做?玻璃拟态 Sheet 设计实战
  6. 【OpenHarmony/HarmonyOS】节点高亮、连线联动、选中反馈怎么做得更有空间感

其中我最推荐先发第 1 篇,原因很简单:

  • 新鲜感强
  • 关键词覆盖广
  • HarmonyOS + Web + Three.js 组合更容易吸引点击
  • 更适合展示项目差异化

十一、这次改造里我踩到的一个真实问题 ⚠️

在做这套方案的时候,我也遇到一个很典型的问题:

Web 页面能正常展示,但在调试环境里读取自定义全局桥接方法时,不同环境的表现可能并不完全一致。

这其实提醒我们一件事:

  • “页面视觉正常”不代表“桥接一定正确”
  • “调试环境能调用”也不代表“真机环境一定一样”

所以这类混合架构项目,建议把验证分成三层:

  1. Web 页面本身是否工作正常
  2. 原生到 Web 的数据同步是否生效
  3. Web 到原生的事件回传是否稳定

只有三层都通了,整个链路才算真的闭环。🔄


十二、最后总结一下这次方案的价值 🎯

这次把星图从原生改成 Web 渲染层之后,我得到的最大收益有三个:

1. 3D 表达力明显增强

Three.js 在空间氛围、发光、轨道、粒子这些表达上,确实更顺手。

2. 原有业务结构几乎不用推翻

因为我保留了 HarmonyOS 的外层壳和 XingtuGraphStore 数据层,所以这次改造并不是重写项目,而是替换渲染层。

3. 后续可扩展性更强

后面如果我要继续加这些能力,就会更自然:

  • 更复杂的相机动画
  • 节点聚合与展开
  • 更丰富的图谱布局
  • 关系类型差异化
  • WebGL 特效增强

十三、本文涉及到的关键实现文件 📂

如果你也在做类似项目,可以重点关注这些文件:

  • entry/src/main/ets/components/web/XingtuWebScene.ets
  • entry/src/main/ets/model/XingtuGraphStore.ets
  • entry/src/main/ets/model/XingtuGraphMath.ets
  • web-demo/src/main.ts
  • web-demo/scripts/sync-harmony.mjs

结语

如果你也在做 HarmonyOS + 可视化 + 图谱 + 3D 方向的项目,我真的很建议你试试这种思路:

让 HarmonyOS 做业务骨架,让 Web/Three.js 做高表达力场景层。

它不是最“纯粹”的方案,但很可能是最实用的方案。🌟

如果这篇文章对你有帮助,欢迎留言交流。
后面我也准备继续把这个项目再拆成几篇文章,比如:

  • ArkTS 3D 投影数学
  • 词图自动布局
  • 图谱数据层设计
  • 科技感弹层与沉浸式 UI

如果你想看哪一篇,也可以告诉我,我继续整理。🚀

img

Logo

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

更多推荐