🌙 JSBridge 协议设计方案
🌙 一、核心设计目标
- 双向通信:支持 H5 ↔ Android、H5 ↔ iOS 双向调用
- 安全性:通过白名单限制调用权限,防止恶意调用
- 可靠性:包含超时处理、错误码机制、唯一标识关联请求响应
- 易用性:API 设计贴近原生调用习惯,隐藏通信细节
🌙 二、协议规范
🌙 1. 数据交换格式
采用 JSON 作为序列化格式,所有消息需包含基础字段,扩展字段按需添加。
🌙 2. 消息结构(BaseMessage)
| 字段名 | 类型 | 必选 | 说明 |
|---|---|---|---|
id | String | 否 | 唯一标识(UUID),用于关联请求与响应(request/response 类型必传) |
type | String | 是 | 消息类型:request(请求)、response(响应)、event(事件) |
method | String | 是 | 方法名/事件名(如 getDeviceInfo、onNetworkChange) |
params | Object | 否 | 调用参数(request/event 类型可用) |
data | Object | 否 | 响应数据(response 类型可用) |
code | Number | 否 | 响应状态码(response 类型必传,200 为成功) |
error | String | 否 | 错误信息(code 非 200 时必传) |
🌙 3. 状态码规范
| code | 说明 |
|---|---|
| 200 | 成功 |
| 400 | 参数错误(格式/必填项) |
| 403 | 方法未在白名单(无权限) |
| 404 | 方法不存在 |
| 500 | 执行异常(原生/JS 错误) |
| 504 | 超时未响应 |
🌙 4. 通信流程
H5 调用原生(JS → Native):
H5 生成request消息 → 原生接收并验证 → 执行方法 → 原生返回response消息 → H5 处理响应。原生调用 H5(Native → JS):
原生生成request消息 → H5 接收并验证 → 执行方法 → H5 返回response消息 → 原生处理响应。事件通知(单向):
发送方生成event消息(无id)→ 接收方监听并处理(无需响应)。
🌙 5. 安全性措施
- 白名单机制:原生端维护允许调用的
method列表,非白名单方法直接返回 403。 - 参数校验:原生/H5 接收消息时校验
params格式(如必填字段、类型),无效则返回 400。
🌙 三、三方实现 Demo
🌙 1. H5 端(Next.js)
封装 JSBridge 工具类,提供调用原生、监听事件、注册原生可调用方法的能力。
// utils/jsBridge.ts
class JSBridge {
private callbackMap: Record<string, (data: any, error: string) => void> = {};
private eventListeners: Record<string, ((params: any) => void)[]> = {};
private isInitialized = false;
constructor() {
this.init();
}
// 初始化:注册原生消息接收入口
private init() {
if (this.isInitialized) return;
// 暴露给原生调用的消息接收方法
window.JSBridge = {
receiveMessage: (message: string) => this.handleNativeMessage(message),
};
this.isInitialized = true;
}
// 生成唯一 ID(用于关联请求响应)
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}
// 处理原生发送的消息(request/event)
private handleNativeMessage(messageStr: string) {
try {
const message = JSON.parse(messageStr);
switch (message.type) {
case 'request':
this.handleNativeRequest(message);
break;
case 'event':
this.handleNativeEvent(message);
break;
default:
console.error('未知消息类型', message.type);
}
} catch (e) {
console.error('解析原生消息失败', e);
}
}
// 处理原生对 H5 的调用(request)
private async handleNativeRequest(message: any) {
const { id, method, params } = message;
// 模拟 H5 注册的方法(实际项目中可通过 registerMethod 动态注册)
const methodMap = {
showToast: (text: string) => { alert(text); return { success: true }; },
getPageInfo: () => ({ url: window.location.href, title: document.title }),
};
let response = { id, type: 'response' as const, code: 200, data: null, error: '' };
try {
if (!methodMap.hasOwnProperty(method)) {
response.code = 404;
response.error = `方法 ${method} 不存在`;
} else {
response.data = await methodMap[method](params);
}
} catch (e) {
response.code = 500;
response.error = (e as Error).message;
}
// 向原生发送响应
this.sendToNative(response);
}
// 处理原生发送的事件(event)
private handleNativeEvent(message: any) {
const { method, params } = message;
if (this.eventListeners[method]) {
this.eventListeners[method].forEach(listener => listener(params));
}
}
// 发送消息到原生
private sendToNative(message: any) {
const messageStr = JSON.stringify(message);
// Android 原生通过 JSBridgeInterface 接收
if (window.AndroidJSBridge) {
window.AndroidJSBridge.receiveMessage(messageStr);
}
// iOS 原生通过 WKScriptMessageHandler 接收(注册名称为 JSBridge)
else if (window.webkit?.messageHandlers?.JSBridge) {
window.webkit.messageHandlers.JSBridge.postMessage(messageStr);
} else {
console.error('未检测到原生 JSBridge 环境');
}
}
// H5 调用原生方法(异步)
callNative(method: string, params?: any, timeout = 5000): Promise<any> {
return new Promise((resolve, reject) => {
const id = this.generateId();
// 注册回调
this.callbackMap[id] = (data, error) => {
if (error) reject(new Error(error));
else resolve(data);
delete this.callbackMap[id]; // 清理回调
};
// 超时处理
const timer = setTimeout(() => {
reject(new Error(`调用 ${method} 超时`));
delete this.callbackMap[id];
}, timeout);
// 发送请求
this.sendToNative({
id,
type: 'request',
method,
params,
});
});
}
// 监听原生事件
on(eventName: string, listener: (params: any) => void) {
if (!this.eventListeners[eventName]) {
this.eventListeners[eventName] = [];
}
this.eventListeners[eventName].push(listener);
}
// 取消监听原生事件
off(eventName: string, listener?: (params: any) => void) {
if (!this.eventListeners[eventName]) return;
if (listener) {
this.eventListeners[eventName] = this.eventListeners[eventName].filter(l => l !== listener);
} else {
delete this.eventListeners[eventName];
}
}
}
// 实例化并导出
export const jsBridge = new JSBridge();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
🌙 2. Android 端(Kotlin)
通过 WebView 注入接口,处理 H5 消息并响应,支持调用 H5 方法。
// JsBridgeInterface.kt(暴露给 H5 的接口)
class JsBridgeInterface(private val webView: WebView) {
// 白名单:允许 H5 调用的方法
private val methodWhitelist = setOf("getDeviceInfo", "openCamera")
// H5 调用此方法发送消息(需添加 @JavascriptInterface 注解)
@JavascriptInterface
fun receiveMessage(messageStr: String) {
try {
val message = JSONObject(messageStr)
when (message.getString("type")) {
"request" -> handleJsRequest(message)
"event" -> handleJsEvent(message) // 可选:处理 H5 发送的事件
else -> sendErrorResponse(message, 400, "未知消息类型")
}
} catch (e: Exception) {
// 解析失败,无法获取 id,只能日志打印
Log.e("JsBridge", "解析 H5 消息失败: ${e.message}")
}
}
// 处理 H5 的请求(request)
private fun handleJsRequest(message: JSONObject) {
val id = message.getString("id")
val method = message.getString("method")
val params = message.optJSONObject("params")
// 校验白名单
if (!methodWhitelist.contains(method)) {
sendErrorResponse(message, 403, "方法 $method 无权限")
return
}
// 执行对应方法
try {
val result = when (method) {
"getDeviceInfo" -> getDeviceInfo()
"openCamera" -> openCamera(params?.optString("type"))
else -> {
sendErrorResponse(message, 404, "方法 $method 不存在")
return
}
}
// 发送成功响应
sendSuccessResponse(id, result)
} catch (e: Exception) {
sendErrorResponse(message, 500, e.message ?: "执行失败")
}
}
// 发送成功响应给 H5
private fun sendSuccessResponse(id: String, data: JSONObject) {
val response = JSONObject().apply {
put("id", id)
put("type", "response")
put("code", 200)
put("data", data)
}
postMessageToJs(response.toString())
}
// 发送错误响应给 H5
private fun sendErrorResponse(message: JSONObject, code: Int, error: String) {
val response = JSONObject().apply {
put("id", message.getString("id"))
put("type", "response")
put("code", code)
put("error", error)
}
postMessageToJs(response.toString())
}
// 向 H5 发送消息(通过 WebView 注入 JS)
private fun postMessageToJs(messageStr: String) {
webView.post {
val jsCode = "window.JSBridge.receiveMessage('${escapeJson(messageStr)}');"
webView.evaluateJavascript(jsCode, null)
}
}
// 工具方法:转义 JSON 中的单引号,避免 JS 语法错误
private fun escapeJson(json: String): String {
return json.replace("'", "\\'")
}
// 原生实现的方法示例:获取设备信息
private fun getDeviceInfo(): JSONObject {
return JSONObject().apply {
put("model", Build.MODEL)
put("os", "Android ${Build.VERSION.RELEASE}")
put("appVersion", "1.0.0")
}
}
// 原生实现的方法示例:打开相机
private fun openCamera(type: String?): JSONObject {
val cameraType = type ?: "photo"
// 实际项目中调用相机 API
return JSONObject().apply {
put("result", "相机已打开(类型:$cameraType)")
}
}
// 原生主动调用 H5 方法示例
fun callJsMethod(method: String, params: JSONObject) {
val id = "native_${System.currentTimeMillis()}"
val request = JSONObject().apply {
put("id", id)
put("type", "request")
put("method", method)
put("params", params)
}
postMessageToJs(request.toString())
}
// 原生发送事件给 H5 示例(如网络变化)
fun sendEventToJs(eventName: String, params: JSONObject) {
val event = JSONObject().apply {
put("type", "event")
put("method", eventName)
put("params", params)
}
postMessageToJs(event.toString())
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// WebView 初始化(Activity/Fragment 中)
class BridgeActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_bridge)
webView = findViewById(R.id.webView)
// 配置 WebView
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
// 暴露 JSBridge 接口(H5 可通过 window.AndroidJSBridge 调用)
val jsBridge = JsBridgeInterface(webView)
webView.addJavascriptInterface(jsBridge, "AndroidJSBridge")
// 加载 H5 页面
webView.loadUrl("https://your-h5-domain.com")
// 示例:原生主动调用 H5 方法
findViewById<Button>(R.id.btnCallJs).setOnClickListener {
val params = JSONObject().put("text", "来自 Android 的通知")
jsBridge.callJsMethod("showToast", params)
}
// 示例:原生发送事件给 H5(如网络变化)
findViewById<Button>(R.id.btnSendEvent).setOnClickListener {
val params = JSONObject().put("isConnected", true).put("type", "wifi")
jsBridge.sendEventToJs("onNetworkChange", params)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
🌙 3. iOS 端(Swift)
使用 WKWebView 的 WKScriptMessageHandler 接收 H5 消息,通过注入 JS 调用 H5 方法。
// JsBridgeHandler.swift(处理 H5 消息)
import WebKit
class JsBridgeHandler: NSObject, WKScriptMessageHandler {
private weak var webView: WKWebView?
// 白名单:允许 H5 调用的方法
private let methodWhitelist = Set(["getDeviceInfo", "openCamera"])
init(webView: WKWebView) {
self.webView = webView
super.init()
}
// 接收 H5 发送的消息(通过 WKScriptMessageHandler)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let messageStr = message.body as? String else {
print("H5 消息格式错误(非字符串)")
return
}
handleJsMessage(messageStr)
}
private func handleJsMessage(_ messageStr: String) {
guard let data = messageStr.data(using: .utf8),
let message = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
print("解析 H5 消息失败")
return
}
guard let type = message["type"] as? String else {
sendErrorResponse(message: message, code: 400, error: "缺少 type 字段")
return
}
switch type {
case "request":
handleJsRequest(message)
case "event":
handleJsEvent(message) // 可选:处理 H5 事件
default:
sendErrorResponse(message: message, code: 400, error: "未知消息类型:\(type)")
}
}
// 处理 H5 的请求(request)
private func handleJsRequest(_ message: [String: Any]) {
guard let id = message["id"] as? String,
let method = message["method"] as? String else {
print("H5 请求缺少 id 或 method")
return
}
// 校验白名单
if !methodWhitelist.contains(method) {
sendErrorResponse(message: message, code: 403, error: "方法 \(method) 无权限")
return
}
// 执行对应方法
do {
let params = message["params"] as? [String: Any]
let result: [String: Any]
switch method {
case "getDeviceInfo":
result = getDeviceInfo()
case "openCamera":
let type = params?["type"] as? String ?? "photo"
result = openCamera(type: type)
default:
sendErrorResponse(message: message, code: 404, error: "方法 \(method) 不存在")
return
}
sendSuccessResponse(id: id, data: result)
} catch {
sendErrorResponse(message: message, code: 500, error: error.localizedDescription)
}
}
// 发送成功响应给 H5
private func sendSuccessResponse(id: String, data: [String: Any]) {
let response: [String: Any] = [
"id": id,
"type": "response",
"code": 200,
"data": data
]
postMessageToJs(response)
}
// 发送错误响应给 H5
private func sendErrorResponse(message: [String: Any], code: Int, error: String) {
guard let id = message["id"] as? String else {
print("无法发送错误响应:缺少 id")
return
}
let response: [String: Any] = [
"id": id,
"type": "response",
"code": code,
"error": error
]
postMessageToJs(response)
}
// 向 H5 发送消息(通过注入 JS)
private func postMessageToJs(_ message: [String: Any]) {
guard let webView = webView,
let jsonData = try? JSONSerialization.data(withJSONObject: message),
let jsonStr = String(data: jsonData, encoding: .utf8) else {
print("构造消息失败")
return
}
// 转义单引号,避免 JS 语法错误
let escapedStr = jsonStr.replacingOccurrences(of: "'", with: "\\'")
let jsCode = "window.JSBridge.receiveMessage(' \(escapedStr) ');"
webView.evaluateJavaScript(jsCode, completionHandler: { _, error in
if let error = error {
print("发送消息到 H5 失败:\(error)")
}
})
}
// 原生实现的方法示例:获取设备信息
private func getDeviceInfo() -> [String: Any] {
return [
"model": UIDevice.current.model,
"os": "iOS \(UIDevice.current.systemVersion)",
"appVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
]
}
// 原生实现的方法示例:打开相机
private func openCamera(type: String) -> [String: Any] {
// 实际项目中调用相机 API
return ["result": "相机已打开(类型:\(type))"]
}
// 原生主动调用 H5 方法示例
func callJsMethod(method: String, params: [String: Any]) {
let id = "native_\(Date().timeIntervalSince1970)"
let request: [String: Any] = [
"id": id,
"type": "request",
"method": method,
"params": params
]
postMessageToJs(request)
}
// 原生发送事件给 H5 示例(如网络变化)
func sendEventToJs(eventName: String, params: [String: Any]) {
let event: [String: Any] = [
"type": "event",
"method": eventName,
"params": params
]
postMessageToJs(event)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// ViewController 中初始化 WKWebView
import UIKit
import WebKit
class BridgeViewController: UIViewController {
private var webView: WKWebView!
private var jsBridge: JsBridgeHandler!
override func viewDidLoad() {
super.viewDidLoad()
setupWebView()
loadH5Page()
setupButtons()
}
private func setupWebView() {
let config = WKWebViewConfiguration()
// 注册 JSBridge 消息处理器(H5 通过 window.webkit.messageHandlers.JSBridge 发送消息)
jsBridge = JsBridgeHandler(webView: webView)
config.userContentController.add(jsBridge, name: "JSBridge")
webView = WKWebView(frame: view.bounds, configuration: config)
webView.uiDelegate = self
view.addSubview(webView)
}
private func loadH5Page() {
if let url = URL(string: "https://your-h5-domain.com") {
webView.load(URLRequest(url: url))
}
}
private func setupButtons() {
// 示例:原生主动调用 H5 方法
let callJsBtn = UIButton(type: .system)
callJsBtn.setTitle("调用 H5 显示通知", for: .normal)
callJsBtn.addTarget(self, action: #selector(callJsMethod), for: .touchUpInside)
callJsBtn.frame = CGRect(x: 50, y: 100, width: 200, height: 40)
view.addSubview(callJsBtn)
// 示例:原生发送事件给 H5
let sendEventBtn = UIButton(type: .system)
sendEventBtn.setTitle("发送网络变化事件", for: .normal)
sendEventBtn.addTarget(self, action: #selector(sendEventToJs), for: .touchUpInside)
sendEventBtn.frame = CGRect(x: 50, y: 160, width: 200, height: 40)
view.addSubview(sendEventBtn)
}
@objc private func callJsMethod() {
let params: [String: Any] = ["text": "来自 iOS 的通知"]
jsBridge.callJsMethod(method: "showToast", params: params)
}
@objc private func sendEventToJs() {
let params: [String: Any] = ["isConnected": true, "type": "wifi"]
jsBridge.sendEventToJs(eventName: "onNetworkChange", params: params)
}
}
extension BridgeViewController: WKUIDelegate {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
🌙 四、使用示例
🌙 1. H5 调用原生
// 调用原生获取设备信息
const getDeviceInfo = async () => {
try {
const data = await jsBridge.callNative('getDeviceInfo');
console.log('设备信息', data); // { model: "xxx", os: "Android 13", appVersion: "1.0.0" }
} catch (e) {
console.error('调用失败', e);
}
};
// 调用原生打开相机
const openCamera = async () => {
try {
const data = await jsBridge.callNative('openCamera', { type: 'video' });
console.log('相机状态', data); // { result: "相机已打开(类型:video)" }
} catch (e) {
console.error('调用失败', e);
}
};
// 监听原生事件(如网络变化)
useEffect(() => {
const handleNetworkChange = (params) => {
console.log('网络变化', params); // { isConnected: true, type: "wifi" }
};
jsBridge.on('onNetworkChange', handleNetworkChange);
return () => jsBridge.off('onNetworkChange', handleNetworkChange);
}, []);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
🌙 2. 原生调用 H5(Android/iOS 按钮点击事件)
- 原生调用 H5 的
showToast方法,H5 弹出提示。 - 原生发送
onNetworkChange事件,H5 监听到后更新 UI。
🌙 五、协议扩展建议
- 批量调用:支持一次发送多个请求,减少通信次数。
- 加密传输:敏感场景下对消息内容加密(如 AES)。
- 版本兼容:消息中添加
version字段,支持多版本协议共存。