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>

这段代码做了几件事:

  1. 通过 @message 监听 H5 发来的消息
  2. 解析消息后调用原生方法(这里模拟加法)
  3. 通过 evalJs 将结果注入到 H5 的执行上下文中
  4. 提供 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:

  1. 使用 uni.postMessage 发送消息到原生端(这是 uni-app 官方提供的方法)
  2. __JSBridge 中维护回调池,实现原生异步回调的支持
  3. 通过 onNativeCallback 接收并派发原生返回的结果
  4. 通过 receiveNativeMsg 接收原生主动发来的消息
为什么这样设计,而不是直接用 postMessageonmessage

如果直接使用标准的 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 等机制分批传递(适用于实时通信)。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐