茶器艺科智造HarmonyOS应用实战-47-onPageEnd就把ArkWeb标成ready,Three.js异常谁来接住:建立loading、ready、error状态机
onPageEnd到达时,ArkWeb 主页面完成了一次加载;它并不等于茶杯三维引擎已经创建 Scene、WebGLRenderer、OrbitControls、Mesh,并且暴露了window.__cupSync。当前CupWeb3d.ets:489-492却在这个回调里直接执行webReady = true,随后推参数、补 STL、刷贴图、重置视角。原生侧的 ready 实际表达的是页面事件,而业务需要的是 Three.js 能力握手。
rawfilecup3d/index.html的顶层 IIFE 先访问THREE、创建渲染器和控制器,直到文件末尾才定义window.__cupSync。如果脚本缺失、WebGLRenderer 创建失败或前置初始化抛异常,后续函数可能根本没有注册;原生脚本又使用window.__cupSync && ...,函数不存在时只是短路,不会进入 catch。于是可能出现“原生认为 ready、画面空白、参数调用看似无错”的失败组合。
本文把网页加载、引擎握手、失败和重试建成显式状态机。内容来自静态源码与官方 API 语义,建议代码未写入只读项目,也未运行 ArkWeb、Three.js 或真机恢复流程。
一、当前webReady只有false和true,来源过于宽泛
组件只声明一个布尔值:
// 当前源码摘录 @State private webReady: boolean = false; .onPageEnd(() => { this.webReady = true; void this.onWebPageReadySequence(); })参数、STL、贴图与视角重置都以它为门禁。onWebPageReadySequence的顺序设计得很清楚,但前提没有被证明:它只知道主页面回调到了,不知道__cupSync、__cupEmitStlToNative、贴图函数和 Three 渲染循环是否可用。
当前 Web 组件没有附加onPageBegin、onErrorReceive或onRenderExited状态处理;JavaScriptProxy 的 methodList 只有stlBegin、stlAppend、stlFinish、cupTextureReport,没有引擎 ready/error 方法。源文件里也没有原生可观察的启动握手。
二、Three初始化异常可能发生在__cupSync定义之前
HTML 先加载本地three.min.js与OrbitControls.js,随后立即运行 IIFE。前部代码直接使用全局变量:
// 当前初始化次序摘要(function(){varcontainer=document.getElementById('c');varscene=newTHREE.Scene();varcamera=newTHREE.PerspectiveCamera(45,1,0.1,2000);varrenderer=newTHREE.WebGLRenderer({antialias:true,alpha:true});varcontrols=newTHREE.OrbitControls(camera,renderer.domElement);// 中间定义轮廓、贴图、STL、动画等大量逻辑tick();window.__cupSync=function(p){/* ... */};})();只要THREE未定义、container 异常、WebGL 上下文不可用或某个早期语句抛错,执行会在函数发布前中断。源码没有包住整个 bootstrap 的顶层 try/catch,也没有window.onerror或unhandledrejection向 ArkTS 上报。__cupSync自己的 catch 只写console.error('[cup3d]', e),原生没有据此切换状态。
三、onPageEnd应只推进加载阶段,不直接宣布ready
华为官方 ArkWeb 说明中,onPageEnd是主 frame 的页面加载完成回调;官方 FAQ 还说明收到它不能保证下一帧已经反映 DOM 状态。对本地 HTML 来说,它适合发起能力探测,不适合作为 Three 业务能力成功的替代证据。
建议状态至少包含四种:
// 建议代码 export enum CupEngineState { IDLE = 'IDLE', LOADING = 'LOADING', READY = 'READY', ERROR = 'ERROR' } export interface CupEngineFailure { readonly stage: 'RESOURCE' | 'BOOT' | 'HANDSHAKE' | 'RENDER' | 'PROCESS'; readonly code: string; readonly detail: string; readonly retryable: boolean; }状态转换应固定为:IDLE -> LOADING(开始加载),LOADING -> READY(收到匹配本轮 token 的 JS 握手),LOADING/READY -> ERROR(资源、引擎或进程错误),ERROR -> LOADING(用户重试)。onPageEnd仍保持 LOADING,只发握手请求并启动超时。
四、用带loadToken的握手证明函数已经发布
仅让 JS 页面启动时主动报 ready 仍可能遇到旧页面回调。建议每次加载生成loadToken;onPageEnd调用已经初始化完成后才会存在的window.__cupHandshake(token),Web 再把同一 token 回传。旧 token 一律忽略。
// 建议代码:放在成功完成 Three 初始化并发布业务函数之后window.__cupHandshake=function(loadToken){varhost=typeofCupNative!=='undefined'?CupNative:window.CupNative;if(!host||!host.cupEngineReady){return'NO_NATIVE_BRIDGE';}host.cupEngineReady(String(loadToken||''),'three-r128',JSON.stringify({sync:!!window.__cupSync,stl:!!window.__cupEmitStlToNative}));return'READY_REPORTED';};握手应位于__cupSync、STL 和必要贴图入口定义之后。capabilities 让原生按能力放行,不要仅靠版本字符串猜函数存在。token 不需要是秘密,只用于区分加载代次。
五、JavaScriptProxy扩展ready与error回调
现有CupNativeStlBridge已证明 Web 可以回调 ArkTS。建议将引擎事件拆成小桥,或在现有对象中增加两个经过长度限制的方法。回调只传稳定 stage/code 与截断详情,不把完整堆栈直接展示给用户。
// 建议代码:桥接层不直接操作 WebviewController class CupEngineBridge { private onReady: (token: string, version: string, caps: string) => void = (): void => {}; private onError: (token: string, stage: string, code: string, detail: string) => void = (): void => {}; attach( ready: (token: string, version: string, caps: string) => void, error: (token: string, stage: string, code: string, detail: string) => void ): void { this.onReady = ready; this.onError = error; } cupEngineReady = (token: string, version: string, caps: string): void => { this.onReady(token, version, caps); }; cupEngineError = ( token: string, stage: string, code: string, detail: string ): void => { this.onError(token, stage, code, detail.substring(0, 240)); }; }注册时把cupEngineReady和cupEngineError加入 JavaScriptProxy methodList。若另建CupEngine代理对象,要用当前 SDK 实际支持的注册方式;本文示例不声称已经编译。
六、Native侧把页面事件、握手和超时接到状态机
开始加载时清理旧超时与错误,生成新 token;onPageEnd 只注入握手脚本。超时阈值应通过低端设备启动数据确定,下面的 5000ms 只是示例配置。
// 建议代码:省略安全随机token实现细节 @State private engineState: CupEngineState = CupEngineState.IDLE; private loadToken: string = ''; private handshakeTimer: number = -1; private onWebLoadBegin(): void { this.cancelHandshakeTimer(); this.loadToken = this.nextLoadToken(); this.engineState = CupEngineState.LOADING; this.engineFailure = undefined; } private onWebPageEnd(): void { const tokenJson = JSON.stringify(this.loadToken); void this.webController.runJavaScript( `window.__cupHandshake && window.__cupHandshake(${tokenJson});` ); this.armHandshakeTimeout(5000); } private acceptEngineReady(token: string, capsJson: string): void { if (token !== this.loadToken || !this.hasRequiredCapabilities(capsJson)) { return; } this.cancelHandshakeTimer(); this.engineState = CupEngineState.READY; this.scheduleParamsToWeb(); this.flushDeferredActions(); }若脚本函数不存在,runJavaScript本身可能正常完成,但不会收到 ready;超时负责把这种静默短路变成 HANDSHAKE_ERROR。参数调度应沿用第 46 篇的 pending latest snapshot,而不是在超时前丢弃用户最新修改。
七、Web端顶层异常与运行期异常都要回传
应在危险初始化之前安装最小错误上报函数,并用顶层 try/catch 包住 bootstrap。不能等 Three 初始化完成后才定义错误桥,因为最需要接住的是启动早期异常。
// 建议代码:错误码需固定枚举,detail只用于诊断(functionbootCupEngine(){functionreportFatal(stage,code,error){try{varhost=typeofCupNative!=='undefined'?CupNative:window.CupNative;vardetail=String(error&&error.message?error.message:error||'');if(host&&host.cupEngineError){host.cupEngineError(window.__cupLoadToken||'',stage,code,detail.slice(0,240));}}catch(_){}}try{if(!window.THREE)thrownewError('THREE_UNAVAILABLE');initializeSceneRendererControls();publishCupCommands();}catch(error){reportFatal('BOOT','ENGINE_BOOT_FAILED',error);}window.addEventListener('unhandledrejection',function(event){reportFatal('RENDER','UNHANDLED_REJECTION',event.reason);});})();上例中的 token 需要由握手请求先写入,或让 error 回调不依赖 token、由当前 LOADING 代次接收。两种协议只能选一种并写清顺序。WebGL context lost、同步重建失败和动画循环异常也应调用相同上报入口;普通 console 日志可保留,但不能作为唯一状态通道。
八、所有命令只在READY消费,失败时保留可恢复意图
参数、贴图、STL、重置视角的门禁应读取engineState === READY。LOADING 时保留最新参数和未消费 nonce;ERROR 时停止自动执行,让用户看到错误状态并决定重试。只有真正提交命令后才更新last...Handled,否则重试后无法补发。
// 建议代码 private canRunCupCommand(): boolean { return this.engineState === CupEngineState.READY; } private flushDeferredActions(): void { if (!this.canRunCupCommand()) { return; } this.scheduleParamsToWeb(); this.kickCupTextureFlushIfNeeded(); this.tryFlushCupViewReset(); this.tryFlushStlExport(); } private retryEngine(): void { if (this.engineState !== CupEngineState.ERROR) { return; } this.onWebLoadBegin(); this.webController.loadUrl($rawfile('cup3d/index.html')); }loadUrl对 rawfile Resource 的具体重载形式要按当前 SDK 签名核对;也可以通过组件 key 重建 Web。重试必须有次数或用户动作门禁,避免渲染进程持续崩溃时形成自动循环。
九、验证矩阵覆盖加载、启动、运行和恢复
| 场景 | 注入条件 | 期望状态 | 延迟动作 |
|---|---|---|---|
| 正常启动 | 函数全部发布 | LOADING→READY | 只补发最新参数 |
| three脚本不可用 | THREE缺失 | LOADING→ERROR | 不发 STL/贴图 |
| renderer构造抛错 | 可控 fake | BOOT ERROR | 显示重试入口 |
| 握手函数缺失 | onPageEnd 到达 | 超时后 ERROR | pending 保留 |
| 旧 token 回调 | 重载后旧消息 | 状态不变 | 不消费新 pending |
| 资源加载错误 | onErrorReceive | RESOURCE ERROR | 记录稳定错误码 |
| 渲染进程退出 | onRenderExited | PROCESS ERROR | 需要显式 reload |
| sync重建异常 | 非法 profile | RENDER ERROR | 不伪装参数成功 |
| 加载中改参数 | 连续拖动 | 仍为 LOADING | READY 后发最终快照 |
| ERROR后重试 | 用户点击 | ERROR→LOADING→READY | nonce 不重复消费 |
单元测试可用 fake bridge 和 fake timer断言转换;HTML 可在测试页面中让initializeSceneRendererControls主动抛错;最终还需真实 ArkWeb 与 WebGL 设备测试。仅看到 onPageEnd 日志不能作为 READY 证据。
十、故障排查、官方锚点与结论范围
一直停在 LOADING 时,先运行一个只返回typeof window.__cupHandshake的脚本,区分函数未发布、JavaScriptProxy 不可见和 token 不匹配;不要直接把超时延长。进入 READY 仍空白时,检查 capability 是否只验证了函数存在却没验证首帧,再加入 renderer、scene、cupMesh 的最小状态回报。
立即进入 ERROR 时,按 stage 分路:RESOURCE 看本地脚本路径和 onErrorReceive;BOOT 看 THREE、OrbitControls 与 WebGLRenderer;PROCESS 看 onRenderExited;RENDER 看最后 revision 与杯型参数。重试后收到旧回调,则核对 loadToken 生命周期和桥对象是否残留上轮 listener。
本次源码确认:onPageEnd直接置webReady=true;业务函数发布晚于多步 Three 初始化;参数脚本对函数不存在采用短路;现有 proxy 没有引擎状态方法。官方资料确认 onPageEnd 是主 frame 加载完成回调,资源错误与渲染进程退出另有事件。本文没有复现实际 Three 异常,也没有证明建议 API 在项目目标 SDK 编译通过,状态机、握手、超时和 reload 都属于待实现方案。