runJavaScript返回成功,H5那边一行都没收到
一个活动页,用 Web 组件加载运营配置的 H5 页面。原生侧需要在页面加载完成后把当前用户的信息(昵称、头像、会员等级)推给 H5,H5 拿到之后渲染个性化内容。你写了 controller.runJavaScript("window.__setUserInfo(" + JSON.stringify(user) + ")"),在 onPageEnd 里调,返回值是一个 Promise,没有报错。你在 H5 侧加了 console.log,DevTools 里一行都没有。你以为是 H5 的接收函数没定义,打开源码一看,window.__setUserInfo 明明写在 <head> 里。你又把调用挪到 onPageBegin、挪到 aboutToAppear、挪到 onControllerAttached,有时候报一个“WebviewController not associated with a Web component”的错误,有时候安静地什么都不发生。
问题不在 H5 有没有写接收函数,也不在你拼的 JS 字符串有没有语法错误。runJavaScript 的执行依赖 Web 组件的控制器已经完成关联、页面已经进入可执行 JS 的阶段,而这两个条件分别有各自的就绪信号,不是“我调了 controller 的方法,它就该执行”。你在页面还没开始加载时调,控制器可能已经关联但渲染引擎还没启动;你在 onPageBegin 里调,页面正在加载,JS 上下文可能还没创建完成;你拿到一个成功返回的 Promise,只代表指令被投递到了 Web 内核的队列,不代表 H5 的 JS 引擎已经执行了它。
onControllerAttached 之后才能安全地碰 controller
先把控制器的关联时机钉死。Web 组件的 controller 在构造时传入,但控制器与组件真正完成关联是在 onControllerAttached 回调触发的时候。在这个回调之前,controller 对象存在,但调用它的方法会得到 “WebviewController not associated with a Web component” 的错误。这个错误在 aboutToAppear 里很常见,因为 aboutToAppear 触发时组件还没完成挂载。
typescript
Web({ src: this.url, controller: this.controller })
.onControllerAttached(() => {
// 此时 controller 已与组件关联,可以安全调用 controller 的方法
// 但页面还没开始加载,runJavaScript 的执行时机仍然要看页面状态
this.controllerAttached = true;
})
.onPageEnd(() => {
// 页面加载完成,DOM 和 JS 上下文都已就绪
// 适合注入依赖页面结构的脚本
})
onControllerAttached 解决的是“能不能调 controller”,它不解决“调用的 JS 能不能执行”。在 onControllerAttached 里调 runJavaScript,如果页面还没开始加载,指令会被投递到一个空白的初始上下文里,页面加载完成之后,新的 JS 上下文会替换掉旧的,之前投递的脚本执行结果随着旧上下文一起消失。
onPageEnd 是页面加载完成的信号,但不是所有子资源都就绪
onPageEnd 的语义是“页面加载完成”,它在主文档加载完毕、DOM 解析完成时触发。但一个典型的 H5 页面会引用外部的 JS 文件、CSS 文件、图片资源,onPageEnd 触发时,这些子资源未必全部加载完毕。如果你的注入脚本依赖某个外部 JS 库(比如 window.$ 或者 window.__setUserInfo 定义在外部文件里),在 onPageEnd 里调用 runJavaScript,有可能那个库还没执行完,注入脚本里引用的对象还是 undefined。
更稳的做法是把注入逻辑放在 H5 侧的一个确定时机,由 H5 主动通知原生“我准备好了”,原生收到通知之后再执行注入。H5 侧在 DOMContentLoaded 或者所有依赖脚本加载完成之后,调一个原生注册的轻量桥接方法,比如 nativeBridge.notifyReady(),原生在回调里再执行真正的 runJavaScript。这样注入的时机由 H5 自己决定,不依赖原生对“页面加载完成”的猜测。
typescript
// 原生侧:注册一个轻量的就绪通知
this.controller.registerJavaScriptProxy(
{
notifyReady: (): void => {
// H5 侧依赖已全部就绪,此时注入用户信息
this.injectUserInfo();
}
},
'nativeBridge',
['notifyReady']
);
// H5 侧:依赖加载完成后主动通知
window.addEventListener('load', () => {
window.nativeBridge.notifyReady();
});
registerJavaScriptProxy 的注册也有一个时序问题,和 runJavaScript 是同一类。注册动作发生在页面加载完成之前时,注册的对象会出现在下一个加载的页面里,当前已经加载完成的页面看不到这个对象。如果 H5 在 load 事件里调 window.nativeBridge.notifyReady(),而 registerJavaScriptProxy 是在 onPageEnd 里才调的,H5 的 load 可能已经执行过了,nativeBridge 在当时还不存在,调用会抛 TypeError。
registerJavaScriptProxy 之后要 refresh,否则当前页面看不到新对象
registerJavaScriptProxy 的官方行为是:注册的对象的函数会被注入到 Web 组件的 JS 上下文中,但当前已经加载完成的页面不会自动刷新。你需要调用 controller.refresh() 来重新加载页面,新的 JS 上下文才会包含刚注册的对象。如果页面已经加载完成,你注册了对象但没有 refresh,H5 侧访问 window.nativeBridge 会得到 undefined,然后在调用 nativeBridge.xxx() 时抛异常。
typescript
Web({ src: this.url, controller: this.controller })
.onControllerAttached(() => {
// 在页面加载之前注册,注册对象会随页面首次加载一起注入
this.controller.registerJavaScriptProxy(
this.bridge,
'nativeBridge',
['notifyReady', 'getUserInfo'],
['getUserInfo'] // asyncMethodList,异步方法
);
})
.onPageEnd(() => {
// 如果需要注入额外的对象,注册后必须 refresh 才能让当前页面看到
// 更好的做法是在 onControllerAttached 里一次性注册完
})
registerJavaScriptProxy 的最佳时机是 onControllerAttached 里、页面开始加载之前。这个时机注册的对象会随页面的首次加载一起进入 JS 上下文,不需要额外的 refresh。在页面加载完成之后才注册,要么 refresh 重新加载页面(用户会看到一次闪烁),要么等下一次页面加载(本次会话看不到)。
注入的脚本要包装成 IIFE,返回值不能直接依赖表达式
runJavaScript 接收的字符串会被当作脚本执行,它不是一个函数调用,没有自动的隔离作用域。如果你拼接的脚本里有多个语句,或者依赖了页面上的全局变量,这些语句在页面上下文里的执行顺序和变量可见性需要你自己保证。
一个常见的错误是拼接了一个裸表达式,比如 "window.__userInfo",期望 runJavaScript 的返回值就是那个对象。实际上 runJavaScript 的返回值和脚本的最后一个表达式的值有关,但不同版本的行为不完全一致,依赖它拿返回值不如在脚本里显式调用一个回调。更稳的写法是把注入逻辑包成一个 IIFE,把结果通过一个约定的全局函数交回来:
typescript
const script = `(function() {
try {
if (typeof window.__setUserInfo === 'function') {
window.__setUserInfo(${JSON.stringify(user)});
return 'ok';
}
return 'not_ready';
} catch (e) {
return 'error:' + e.message;
}
})()`;
const result = await this.controller.runJavaScript(script);
typeof window.__setUserInfo === 'function' 这一行是防御。H5 的脚本可能还没加载完,或者被内容安全策略拦截了,直接调用会抛异常,包在 IIFE 里至少能把错误信息通过返回值带回来,而不是让 runJavaScript 的 Promise 静默兑现成一个 undefined。JSON.stringify 的结果直接嵌在脚本里,如果用户信息里包含引号、换行或者 </script> 这类字符,拼接出来的脚本会语法错误。对 JSON.stringify 的结果做一次转义,或者用 JSON.stringify(JSON.stringify(user)) 再拼接,确保字符串在 JS 源码里是合法的字面量。
javaScriptAccess 默认值和内容安全策略,不是所有页面都允许执行 JS
Web 组件的 javaScriptAccess 属性控制是否允许执行 JavaScript,默认值是 true。如果你或者某个安全配置把它设成了 false,runJavaScript 不会执行,H5 页面里的内联脚本也不会执行。这个属性的关闭是静默的,runJavaScript 的 Promise 仍然会兑现,但页面里没有任何 JS 被执行。
另一个更隐蔽的拦截来自 H5 页面自身的内容安全策略。如果页面的 CSP 头里 script-src 没有包含 'unsafe-inline',通过 runJavaScript 注入的内联脚本会被浏览器引擎拒绝执行,控制台里会有一条 CSP 违规日志,但原生侧的 runJavaScript 返回值不会反映这个拒绝。排查“注入了但没效果”的时候,打开 DevTools 看控制台有没有 CSP 报错,比反复检查拼接的字符串快。
排查顺序:先看关联,再看页面阶段,最后看注册时机和 CSP
下次 runJavaScript 返回成功但 H5 没反应的时候,按这个顺序走。
第一步,确认调用发生在 onControllerAttached 之后。之前调会得到“not associated”错误,但如果你把错误吞了,表现就是“没反应”。
第二步,确认页面处于可执行 JS 的阶段。onPageBegin 太早,JS 上下文可能还没创建;onPageEnd 相对安全,但如果注入依赖外部脚本,等 H5 主动通知更稳。在 aboutToAppear 里调 runJavaScript,控制器可能还没关联,页面更没开始加载。
第三步,如果是 registerJavaScriptProxy 注册的对象 H5 访问不到,检查注册时机是不是在页面加载完成之后。页面加载完成后注册的对象需要 refresh 才对当前页面生效,最佳实践是在 onControllerAttached 里一次性注册完。
第四步,检查 javaScriptAccess 是否为 true,DevTools 控制台有没有 CSP 违规日志。这两个拦截都不会让 runJavaScript 的 Promise 失败。
第五步,检查拼接的脚本有没有语法错误,用户信息里的特殊字符有没有转义,注入逻辑有没有包在 IIFE 里做防御。
runJavaScript 的 Promise 兑现只说明指令被投递到了 Web 内核的队列,它不承诺页面处于可执行状态、不承诺 H5 的接收函数已经定义、不承诺 CSP 允许这次执行。控制器关联看 onControllerAttached,页面就绪看 onPageEnd 或 H5 的主动通知,代理注册要在页面加载之前完成,注入脚本要防御、要转义、要能带回错误信息。下一次注入的脚本石沉大海的时候,先别怀疑 H5 的代码写错了——打开原生侧看一眼,那个 runJavaScript,到底是在页面生命周期的哪一步被调用的。

更多推荐

所有评论(0)