《uni-app开发Harmony Next平台的App》第四篇:内置模块之WebView通讯——实现JSBridge双向交互
WebView通讯:在 uni-app 鸿蒙 App 中实现 JSBridge 双向交互

一个实际的问题:原生与 H5 如何“对话”
假设你正在开发一个 uni-app 的鸿蒙 App,页面里嵌入了一个 H5 网页。H5 需要调用一个原生能力——比如进行一次加密运算、读取本地文件、或者调用某个传感器数据。反过来,原生页面也需要通知 H5:“用户登录状态变了”、“页面即将关闭,请保存数据”。
直接使用 webview 组件的 src 参数加载网页,页面是显示出来了,但双方完全隔离。H5 不认识鸿蒙的原生 API,原生端也无法直接操作 H5 的 JavaScript 上下文。
这就是 WebView 通讯要解决的问题。
官方文档提到了 createWebviewContext 和 @message 事件,但实际项目中用起来,有相当多的细节会被忽略。不是功能本身复杂,而是 生命周期、消息时序、回调管理 这几个点容易踩坑。
它解决什么问题,以及为什么不推荐其他方案
这套机制本质上是一个简化版的 JSBridge:
- 原生→H5:通过
evalJs()直接注入并执行 JavaScript 代码 - H5→原生:通过
@message事件回调,将 H5 端的数据传递给原生
为什么直接推荐这种方案?因为鸿蒙平台的特性:
| 方案 | 优缺点 |
|---|---|
| URL Scheme 拦截 | 流程繁琐,需要 WKWebView 级别的拦截,且受限于 iframe 方案性能较差 |
| JavaScript Prompt 拦截 | 已被现代 WebView 废弃或严格限制 |
| 鸿蒙原生 API + local web server | 需要部署本地服务器,复杂度过高 |
evalJs + @message |
官方支持、接口简单、性能可接受、适合大多数业务场景 |
除非你的交互数据量极大(如视频流),否则这套 JSBridge 方案足够应对大部分需求。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机
uni-app 版本:HBuilderX 4.31 及以上(支持 Harmony Next 编译)
核心实现:从零搭建 JSBridge
第一步:uni-app 原生端
首先在页面中嵌入 web-view 标签,并通过 createWebviewContext 获取实例。
<template>
<view class="content">
<web-view ref="webview" src="/hybrid/html/index.html" @message="handleMessage"></web-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const webview = ref(null);
let webviewInstance = null;
// 从 web-view 组件中获取上下文实例
const getWebviewContext = () => {
if (!webview.value) return null;
return uni.createWebviewContext('webview');
};
// 处理来自 H5 的消息
const handleMessage = (event) => {
const data = event.detail.data || [];
console.log('收到H5消息:', data);
data.forEach((msg) => {
if (msg.type === 'callNative') {
handleNativeMethod(msg);
}
});
};
// 调用原生方法并返回结果到 H5
const handleNativeMethod = (msg) => {
// 模拟一个原生方法:加法运算
const result = msg.params.a + msg.params.b;
// 将结果通过 evalJs 传回 H5
const jsCode = `window.__JSBridge.onNativeCallback('${msg.callbackId}', ${result})`;
uni.createWebviewContext('webview').evalJs(jsCode);
};
// 原生主动调 H5
const callH5 = () => {
const jsCode = `window.__JSBridge.receiveNativeMsg({ type: 'loginStatusChanged', isLogin: true })`;
uni.createWebviewContext('webview').evalJs(jsCode);
};
onMounted(() => {
// 确保 webview 实例已经准备好
setTimeout(() => {
webviewInstance = getWebviewContext();
}, 300);
});
</script>
这段代码做了几件事:
- 通过
@message监听 H5 发来的消息 - 解析消息后调用原生方法(这里模拟加法)
- 通过
evalJs将结果注入到 H5 的执行上下文中 - 提供
callH5示例,演示原生主动通知 H5
注意点
createWebviewContext必须在onMounted之后调用,否则实例为空@message事件返回的event.detail.data是一个数组,即使只发了一条消息evalJs是异步执行的,但不需要额外回调,因为执行结果会直接注入到 H5 的 window 对象
第二步:HTML 页面端(H5)
H5 页面需要实现一个 JSBridge 对象,用于接收原生调用的结果,以及主动向原生发送消息。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>JSBridge Demo</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
button { margin: 10px 0; padding: 10px 20px; font-size: 16px; }
#result { margin: 20px 0; padding: 10px; border: 1px solid #ccc; min-height: 50px; }
</style>
</head>
<body>
<h1>JSBridge Demo</h1>
<button onclick="testAdd()">测试加法方法</button>
<button onclick="testNativeCall()">原生调 H5</button>
<div id="result">等待消息...</div>
<script>
// JSBridge 核心对象
window.__JSBridge = {
// 回调函数池
_callbackPool: {},
// 回调 ID 生成器
_callbackId: 0,
// 向原生发送消息
sendToNative: function(method, params, callback) {
const callbackId = ++this._callbackId;
if (callback) {
this._callbackPool[callbackId] = callback;
}
// 使用 uni-app 提供的 uni-webview 消息通道
const message = {
type: 'callNative',
method: method,
params: params,
callbackId: callbackId
};
// 向原生端发送消息
if (typeof uni !== 'undefined' && uni.postMessage) {
uni.postMessage({ data: message });
} else {
// 降级方案:直接通过 window.webkit.messageHandlers 或类似接口
console.error('uni.postMessage 不可用');
}
},
// 接收原生回调结果
onNativeCallback: function(callbackId, data) {
const callback = this._callbackPool[callbackId];
if (callback) {
callback(data);
delete this._callbackPool[callbackId];
}
},
// 接收原生主动发来的消息
receiveNativeMsg: function(msg) {
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = `收到原生消息: ${JSON.stringify(msg)}`;
console.log('收到原生消息:', msg);
}
};
// 测试加法方法
function testAdd() {
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = '正在请求原生计算...';
window.__JSBridge.sendToNative('add', { a: 3, b: 5 }, function(result) {
resultDiv.innerHTML = `原生计算结果: 3 + 5 = ${result}`;
});
}
// 原生调 H5 的测试(由原生端按钮触发)
function testNativeCall() {
// 向原生发送请求原生调 H5 的信号
// 实际场景中由原生端直接调用,这里只是模拟
}
</script>
</body>
</html>
这段代码在 H5 端建立了完整的 JSBridge:
- 使用
uni.postMessage发送消息到原生端(这是 uni-app 官方提供的方法) - 在
__JSBridge中维护回调池,实现原生异步回调的支持 - 通过
onNativeCallback接收并派发原生返回的结果 - 通过
receiveNativeMsg接收原生主动发来的消息
为什么这样设计,而不是直接用 postMessage 和 onmessage
如果直接使用标准的 window.postMessage,你会发现它无法触发 uni-app 的 @message 事件。这是因为 uni-app 的 web-view 组件并非标准的 WebView,它对消息通道做了自己的封装。必须使用 uni.postMessage 才能正确触发 @message 事件。
第三步:封装成可复用的 JSBridge 模块
在实际项目中,不可能在每个页面都重复写这段逻辑。封装成一个独立的模块可以大幅提升复用性。
// utils/jsbridge.ts
interface JSBridgeOptions {
webviewId: string;
timeout?: number;
}
interface Message {
type: string;
method: string;
params: any;
callbackId: number;
}
interface CallbackEntry {
resolve: (data: any) => void;
reject: (error: any) => void;
timer: number;
}
class JSBridge {
private webviewId: string;
private callbackQueue: Map<number, CallbackEntry> = new Map();
private callbackId: number = 0;
private defaultTimeout: number;
constructor(options: JSBridgeOptions) {
this.webviewId = options.webviewId;
this.defaultTimeout = options.timeout || 30000;
}
/**
* 向 H5 发送消息,并返回 Promise
*/
public callH5(method: string, params: any, timeout?: number): Promise<any> {
const callbackId = ++this.callbackId;
const jsCode = `window.__JSBridge.receiveNativeMsg({
callbackId: ${callbackId},
method: '${method}',
params: ${JSON.stringify(params)}
})`;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.callbackQueue.delete(callbackId);
reject(new Error(`JSBridge call timeout: ${method}`));
}, timeout || this.defaultTimeout);
this.callbackQueue.set(callbackId, {
resolve,
reject,
timer
});
uni.createWebviewContext(this.webviewId).evalJs(jsCode);
});
}
/**
* 处理来自 H5 的 @message 事件
*/
public handleWebviewMessage(event: any) {
const messages: Message[] = event.detail.data || [];
messages.forEach((msg) => {
switch (msg.type) {
case 'callNative':
this.handleNativeMethod(msg);
break;
case 'nativeCallbackResult':
this.handleNativeCallback(msg);
break;
default:
console.warn('Unknown message type:', msg.type);
}
});
}
/**
* 处理 H5 调用的原生方法
*/
private handleNativeMethod(msg: Message) {
// 根据 method 分发到具体的原生实现
// 这里作为抽象方法,由使用者实现
}
/**
* 处理 H5 返回的回调结果
*/
private handleNativeCallback(msg: Message) {
const entry = this.callbackQueue.get(msg.callbackId);
if (entry) {
clearTimeout(entry.timer);
entry.resolve(msg.params);
this.callbackQueue.delete(msg.callbackId);
}
}
}
export default JSBridge;
这个封装版本增加了:
- Promise 支持:让调用原生方法可以像 Promise 一样等待结果
- 超时机制:防止 H5 挂死导致原生端永久等待
- 统一的回调管理:通过 callbackId 映射到具体的 Promise
常见问题 1:createWebviewContext 获取为 null
现象:在 onMounted 中直接调用 createWebviewContext 时返回 null。
原因:web-view 组件在 vnode 挂载后并不等于 DOM 已经渲染完成,组件内部还有一个初始化的过程。createWebviewContext 需要 web-view 组件的内部状态已经就绪。
解决方案:延迟调用,或者在组件的 onReady 钩子中获取。
onMounted(() => {
// 不能直接获取,需要延迟
setTimeout(() => {
webviewInstance = uni.createWebviewContext('webview');
}, 300);
});
不推荐使用
nextTick,因为nextTick只是 Vue 的 DOM 更新结束后,不代表web-view组件内部初始化完成。300ms 是一个经验值,大多数情况下够用,如果页面复杂可以适当增大。
常见问题 2:消息时序错乱(先发的消息后到达)
现象:H5 连续发送两条消息,原生端收到的顺序与发送顺序不一致。
原因:@message 事件在 Android / iOS 上是异步批量发送的,在鸿蒙上行为类似。当 H5 多次调用 uni.postMessage 后,有可能被合并到一次 @message 回调中,但返回的 event.detail.data 数组的顺序不一定保持发送顺序。
解决方案:不要依赖消息接收顺序。在每条消息中加入一个递增的序列号,原生端收到后按序列号重新排序处理。
<!-- H5 端发送时携带序列号 -->
<script>
let seq = 0;
window.__JSBridge.sendToNative = function(method, params, callback) {
const callbackId = ++this._callbackId;
if (callback) {
this._callbackPool[callbackId] = callback;
}
const message = {
type: 'callNative',
method: method,
params: params,
callbackId: callbackId,
seq: ++seq
};
if (typeof uni !== 'undefined' && uni.postMessage) {
uni.postMessage({ data: message });
}
};
</script>
原生端收到后,如果业务需要保证顺序,可以按 seq 字段排序后再处理。
最佳实践
1. 不要在 @message 回调中执行耗时操作
@message 回调是在 JavaScript 主线程中执行的。如果回调中执行了同步的耗时运算(比如大对象的序列化、复杂计算),会阻塞 WebView 的消息处理。建议将耗时操作放在 setTimeout 或 Web Worker 中(如果支持),或者采用异步消息队列。
// 不推荐
const handleMessage = (event) => {
const data = JSON.parse(JSON.stringify(event.detail.data)); // 大对象深拷贝会卡
data.forEach(item => {
// 处理
});
};
// 推荐:通过消息队列异步处理
let messageQueue = [];
const handleMessage = (event) => {
messageQueue.push(event.detail.data);
processQueue();
};
const processQueue = () => {
requestAnimationFrame(() => {
while (messageQueue.length > 0) {
const data = messageQueue.shift();
// 处理单条消息
}
});
};
2. 使用唯一 callbackId 避免回调冲突
回调 ID 必须在全局唯一,不能只在当前页面范围内。如果你的 App 中同时存在多个 web-view 实例,或者在一个页面内多次加载不同 H5,回调 ID 的生成器不能复用同一个计数器。推荐使用 时间戳 + 随机数 的方式生成 ID。
const generateCallbackId = () => {
return `cb_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
};
3. 预置常见回调的异常处理
在 JSBridge 初始化时,注册全局错误处理逻辑。比如原生端调用一个不存在的 H5 方法,或者 H5 调用一个未注册的原生方法,应该走统一的错误上报流程,而不是静默失败。
window.__JSBridge.receiveNativeMsg = function(msg) {
if (!msg.method || !this[msg.method]) {
console.error(`Unknown method: ${msg.method}`);
// 发送错误到原生端
window.__JSBridge.sendToNative('__error', {
code: -1,
message: `Method ${msg.method} not found`
});
return;
}
this[msg.method](msg);
};
FAQ
Q:为什么真机上 evalJs 有时不生效?
A:90% 的情况是 createWebviewContext 获取不到实例,因为调用时机太早了。另外检查 H5 页面是否已经加载完成,如果加载未完成时调用 evalJs,会被忽略。建议在 H5 中的 window.onload 之后,通过 uni.postMessage 给原生端发送一个“页面已就绪”的信号,原生端收到后再调用 evalJs。
Q:@message 事件是否会触发多次?
A:会。如果 H5 端在短时间内多次调用 uni.postMessage,原生端的 @message 事件可能会被多次触发,也可能合并到一次。不要假设一次性收到所有消息,原生端应当设计为能够处理增量消息。
Q:如果在 H5 中使用了 window.addEventListener('message', handler),能收到原生消息吗?
A:不能。window.addEventListener('message', handler) 对应的是标准 WebView 的 postMessage 机制,而不是 uni-app 的 @message 事件。uni-app 对 WebView 做了封装,H5 端必须使用 uni.postMessage,原生端必须使用 @message。
Q:evalJs 传大型 JSON 时会不会有性能问题?
A:evalJs 本质上将字符串作为 JavaScript 代码执行。如果传入的 JSON 字符串很大,在 H5 端解析会有性能消耗。建议大数据量的情况下,将数据通过 URL 参数传递(适用于初始化数据),或者使用 Blob / FileReader 等机制分批传递(适用于实时通信)。
更多推荐
所有评论(0)