【OpenHarmony/HarmonyOS】我把原生星图改成了 Web 渲染层:用 ArkWeb + Three.js 做一个可交互的 3D 星图
【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?📝
我看了自己整个项目的实现内容,实际上可以拆出很多篇文章,但当前最有传播性的,确实是这篇:
你这个项目目前适合写的选题
【OpenHarmony/HarmonyOS】我把原生星图改成了 Web 渲染层:用 ArkWeb + Three.js 做一个可交互的 3D 星图【OpenHarmony/HarmonyOS】如何在 ArkTS 中实现 3D 节点透视投影与空间旋转【OpenHarmony/HarmonyOS】一个知识图谱应用的数据层应该怎么设计:XingtuGraphStore 实战【OpenHarmony/HarmonyOS】关键词一键生成词图网络:从输入词组到星图节点布局【OpenHarmony/HarmonyOS】ArkUI 科技感弹层怎么做?玻璃拟态 Sheet 设计实战【OpenHarmony/HarmonyOS】节点高亮、连线联动、选中反馈怎么做得更有空间感
其中我最推荐先发第 1 篇,原因很简单:
- 新鲜感强
- 关键词覆盖广
- HarmonyOS + Web + Three.js 组合更容易吸引点击
- 更适合展示项目差异化
十一、这次改造里我踩到的一个真实问题 ⚠️
在做这套方案的时候,我也遇到一个很典型的问题:
Web 页面能正常展示,但在调试环境里读取自定义全局桥接方法时,不同环境的表现可能并不完全一致。
这其实提醒我们一件事:
- “页面视觉正常”不代表“桥接一定正确”
- “调试环境能调用”也不代表“真机环境一定一样”
所以这类混合架构项目,建议把验证分成三层:
- Web 页面本身是否工作正常
- 原生到 Web 的数据同步是否生效
- Web 到原生的事件回传是否稳定
只有三层都通了,整个链路才算真的闭环。🔄
十二、最后总结一下这次方案的价值 🎯
这次把星图从原生改成 Web 渲染层之后,我得到的最大收益有三个:
1. 3D 表达力明显增强
Three.js 在空间氛围、发光、轨道、粒子这些表达上,确实更顺手。
2. 原有业务结构几乎不用推翻
因为我保留了 HarmonyOS 的外层壳和 XingtuGraphStore 数据层,所以这次改造并不是重写项目,而是替换渲染层。
3. 后续可扩展性更强
后面如果我要继续加这些能力,就会更自然:
- 更复杂的相机动画
- 节点聚合与展开
- 更丰富的图谱布局
- 关系类型差异化
- WebGL 特效增强
十三、本文涉及到的关键实现文件 📂
如果你也在做类似项目,可以重点关注这些文件:
entry/src/main/ets/components/web/XingtuWebScene.etsentry/src/main/ets/model/XingtuGraphStore.etsentry/src/main/ets/model/XingtuGraphMath.etsweb-demo/src/main.tsweb-demo/scripts/sync-harmony.mjs
结语
如果你也在做 HarmonyOS + 可视化 + 图谱 + 3D 方向的项目,我真的很建议你试试这种思路:
让 HarmonyOS 做业务骨架,让 Web/Three.js 做高表达力场景层。
它不是最“纯粹”的方案,但很可能是最实用的方案。🌟
如果这篇文章对你有帮助,欢迎留言交流。
后面我也准备继续把这个项目再拆成几篇文章,比如:
- ArkTS 3D 投影数学
- 词图自动布局
- 图谱数据层设计
- 科技感弹层与沉浸式 UI
如果你想看哪一篇,也可以告诉我,我继续整理。🚀

更多推荐



所有评论(0)