系列文章《鸿蒙开发从入门到专家》第 55 篇 · 高级
第 41 篇讲了 Web 组件怎么用、JS Bridge 怎么注册。第 49 篇讲了怎么把 Web 组件封装成容器框架。这篇聚焦 Bridge 本身:协议怎么设计才安全、跨端通信怎么做错误处理、调试方案怎么搭。
Hybrid 应用里 Bridge 是原生和 H5 之间的唯一通道。通道设计得好,H5 调原生能力像调本地函数一样简单;设计得差,每次新增一个 Bridge 接口都要改三处代码,还得担心注入劫持。
一、Bridge 的通信模型
H5 页面 ArkWeb Web 组件 原生层
│ │ │
│ 调用 bridge.method() │ │
│ ─────────────────────────>│ JavaProxyProxyObj │
│ │ .onMethodCalled() │
│ │ ─────────────────────────>│
│ │ │ 执行原生逻辑
│ │ │
│ │ 返回结果/Promise │
│ │ <─────────────────────────│
│ resolve(result) │ │
│ <─────────────────────────│ │
核心机制:ArkWeb 提供的 javaScriptProxy 将一个原生对象注入到 H5 的 window 上,H5 调用对象方法时,原生侧的 onMethodCalled 回调被触发。
二、协议设计
2.1 统一消息格式
Bridge 通信采用统一的 JSON 消息格式,而不是每个接口设计不同的参数签名:
// 协议定义
interface BridgeMessage {
/** 消息 ID,用于异步回调匹配 */
id: string;
/** 方法名 */
method: string;
/** 参数 */
params: Record<string, any>;
/** 调用时间戳 */
timestamp: number;
}
H5 端封装一个统一的调用接口:
// H5 端 Bridge SDK
window.__HarmonyBridge__ = (function() {
const callbacks = new Map();
let seq = 0;
// 监听原生回传的消息
window.addEventListener('bridgeCallback', function(event) {
const data = event.detail;
const callback = callbacks.get(data.id);
if (callback) {
if (data.error) {
callback.reject(new Error(data.error));
} else {
callback.resolve(data.result);
}
callbacks.delete(data.id);
}
});
return {
call: function(method, params) {
return new Promise(function(resolve, reject) {
const id = 'msg_' + (++seq) + '_' + Date.now();
callbacks.set(id, { resolve: resolve, reject: reject });
// 调用注入的原生对象
window.nativeBridge.postMessage(JSON.stringify({
id: id,
method: method,
params: params || {},
timestamp: Date.now()
}));
// 超时处理
setTimeout(function() {
if (callbacks.has(id)) {
callbacks.delete(id);
reject(new Error('Bridge call timeout: ' + method));
}
}, 10000);
});
}
};
})();
2.2 原生端处理
// bridge/BridgeHandler.ets
import { webview } from '@kit.ArkWeb';
interface BridgeMessage {
id: string;
method: string;
params: Record<string, any>;
timestamp: number;
}
interface BridgeHandler {
method: string;
handler: (params: Record<string, any>) => Promise<any>;
}
export class BridgeManager {
private controller: webview.WebviewController;
private handlers: Map<string, BridgeHandler['handler']> = new Map();
// 注册 Bridge 方法
register(method: string, handler: (params: Record<string, any>) => Promise<any>): void {
this.handlers.set(method, handler);
}
// 注入到 H5
inject(webView: WebController): void {
webView.javaScriptProxy({
object: this,
name: 'nativeBridge',
methodList: ['postMessage'],
asyncMethodList: [],
permission: '__BridgeInternal__'
});
}
// H5 调用入口
postMessage(rawMessage: string): void {
let msg: BridgeMessage;
try {
msg = JSON.parse(rawMessage) as BridgeMessage;
} catch (e) {
console.error('Bridge: invalid message format');
return;
}
// 安全检查
if (!this.handlers.has(msg.method)) {
this.sendCallback(msg.id, null, `Unknown method: ${msg.method}`);
return;
}
const handler = this.handlers.get(msg.method)!;
handler(msg.params)
.then(result => {
this.sendCallback(msg.id, result, null);
})
.catch(error => {
this.sendCallback(msg.id, null, error.message || 'Unknown error');
});
}
// 回调 H5
private sendCallback(id: string, result: any, error: string | null): void {
const callbackData = JSON.stringify({ id, result, error });
// 通过 evaluateJS 回调 H5
this.controller.runJavaScript(
`window.dispatchEvent(new CustomEvent('bridgeCallback', { detail: ${callbackData} }));`
);
}
}
三、安全设计
Bridge 是 H5 调原生能力的入口,安全管控不到位等于给恶意页面开后门。
3.1 URL 白名单
只有可信域名的页面才能使用 Bridge:
export class SecurityGuard {
private whitelist: string[] = [
'https://app.example.com',
'https://*.example.com',
'file:///android_asset/'
];
isUrlAllowed(url: string): boolean {
return this.whitelist.some(pattern => {
if (pattern.includes('*')) {
// 通配符匹配
const regex = new RegExp(
'^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'
);
return regex.test(url);
}
return url.startsWith(pattern);
});
}
}
在 Bridge 调用入口做检查:
postMessage(rawMessage: string): void {
const currentUrl = this.controller.getUrl();
if (!this.securityGuard.isUrlAllowed(currentUrl)) {
console.error(`Bridge: blocked call from ${currentUrl}`);
return;
}
// ... 后续处理
}
3.2 方法权限分级
不是所有 Bridge 方法对所有 H5 页面开放。按敏感度分级:
enum BridgePermission {
PUBLIC = 'public',
PROTECTED = 'protected',
PRIVATE = 'private'
}
interface BridgeHandlerConfig {
method: string;
handler: (params: Record<string, any>) => Promise<any>;
permission: BridgePermission;
}
// 注册时声明权限
bridgeManager.register({
method: 'getDeviceInfo',
handler: getDeviceInfo,
permission: BridgePermission.PUBLIC
});
bridgeManager.register({
method: 'openPayment',
handler: openPayment,
permission: BridgePermission.PROTECTED
});
bridgeManager.register({
method: 'readContacts',
handler: readContacts,
permission: BridgePermission.PRIVATE
});
调用时根据当前页面域名和方法的权限级别做匹配:
private checkPermission(method: string, url: string): boolean {
const config = this.handlers.get(method);
if (!config) return false;
switch (config.permission) {
case BridgePermission.PUBLIC:
return this.securityGuard.isUrlAllowed(url);
case BridgePermission.PROTECTED:
return url.includes('app.example.com');
case BridgePermission.PRIVATE:
return url.startsWith('file:///') || url.startsWith('resource://');
default:
return false;
}
}
四、错误处理
Bridge 调用的错误来源有三类:协议错误(消息格式不对)、方法错误(方法不存在或参数校验失败)、执行错误(原生逻辑执行失败)。
统一错误码体系:
enum BridgeErrorCode {
// 协议级
INVALID_MESSAGE = 1001,
UNKNOWN_METHOD = 1002,
INVALID_PARAMS = 1003,
// 权限级
URL_BLOCKED = 2001,
PERMISSION_DENIED = 2002,
// 执行级
EXECUTION_FAILED = 3001,
TIMEOUT = 3002
}
class BridgeError extends Error {
constructor(public code: BridgeErrorCode, message: string) {
super(message);
}
toJSON(): string {
return JSON.stringify({
code: this.code,
message: this.message
});
}
}
H5 端可以根据错误码做不同处理:
window.__HarmonyBridge__.call('openPayment', { orderId: '123' })
.then(function(result) {
// 成功
})
.catch(function(error) {
if (error.message.includes('2002')) {
// 权限不足,提示用户
alert('当前页面无权调用支付');
} else if (error.message.includes('3002')) {
// 超时,重试
retry();
}
});
五、调试方案
Bridge 的调试难点在于 H5 和原生分属两个调试环境。H5 用 Chrome DevTools,原生用 DevEco Profiler。
5.1 日志桥
在 Bridge 调用入口和出口打日志,统一格式:
private logBridgeCall(msg: BridgeMessage, url: string): void {
console.info(
`[Bridge] CALL | id=${msg.id} method=${msg.method} url=${url} params=${JSON.stringify(msg.params)}`
);
}
private logBridgeResult(id: string, result: any, error: string | null): void {
if (error) {
console.error(
`[Bridge] ERROR | id=${id} error=${error}`
);
} else {
console.info(
`[Bridge] RESULT | id=${id} result=${JSON.stringify(result)}`
);
}
}
通过 id 可以在日志中串联一次完整的调用链。
5.2 开发模式注入 Mock
本地开发时,H5 可能跑在本地 dev server 上,原生侧的功能还没实现。可以在开发模式下注入 Mock:
// 开发模式注入 Mock Bridge
if (BuildConfig.DEBUG) {
this.controller.runJavaScript(`
window.nativeBridge = {
postMessage: function(msg) {
var parsed = JSON.parse(msg);
setTimeout(function() {
// Mock 返回
window.dispatchEvent(new CustomEvent('bridgeCallback', {
detail: JSON.stringify({
id: parsed.id,
result: { mock: true, method: parsed.method },
error: null
})
}));
}, 100);
}
};
`);
// 不注入真实的 javaScriptProxy
return;
}
H5 端完全无感知,可以独立开发和联调。
六、常用 Bridge 接口清单
一个完整的 Hybrid 应用通常需要的 Bridge 接口:
小结
Bridge 设计的三个核心:协议统一(JSON 消息格式)、安全分级(URL 白名单 + 方法权限)、调试可观测(日志桥 + Mock 模式)。不要让每个 Bridge 接口自己定义参数签名和回调方式,统一走消息格式 + id 匹配的方式,扩展时只加方法不改编译协议。
下篇讲 IPC 跨进程通信。DataShareExtension 和跨应用数据流转怎么实现。
参考阅读:
第 41 篇:鸿蒙 ArkWeb 实战(Web 组件嵌入)
第 49 篇:鸿蒙 H5 容器化(容器框架设计)
官方文档:javaScriptProxy、WebviewController