前言
如果在此之前你从事过H5/Hybrid开发,那么你一定听说过JSBridge这个概念,简单理解就是:一个提供给H5页面去调用原生能力的工具,同时还能从原生应用中传递数据到H5页面。那么在实际应用中这层能力究竟是如何实现的呢?下面我将以IOS应用中的H5开发为例,构建一个简单的JSBridge.
搭建一个简易JSBridge
定义规范
明确在H5中JSBridge的接口规范
/** JSBridge */
interface IH5JSBridgeCallOptions {
/** 原生能力对应方法名称 */
funcKey: string;
/** 调用方法的参数(可选) */
params?: Record<string, unknown>;
}
interface Window {
/** 定义JSBridge在H5暴露的全局对象 */
H5JSBridge: {
/** 提供call方法供H5调用指定原生能力 */
call: <T>(
options: IH5JSBridgeCallOptions,
/** 通过回调获取调用原生能力的返回结果 */
callback?: (response: T) =>void
) => void,
/** 其他属性/方法 */
}
}
注入JS脚本
- JS脚本
const GlobalCallbackKey = "__js_bridge_callback__";
window[GlobalCallbackKey] = window[GlobalCallbackKey] || {};
window.H5JSBridge = {
call: (options, callback) => {
const { funcKey } = options || {};
if (!funcKey) {
throw new Error("funcKey params must be given.");
}
if (!callback || typeof callback !== "function") {
window.webkit.messageHandlers.nativeBridge.postMessage(options);
return;
}
// 生成一个callbackId简易,作为key存储callback
const callbackId = `__js_bridge_cb_${Date.now()}_${Math.floor(Math.random() * 10)}__`;
window[GlobalCallbackKey][callbackId] = callback;
window.webkit.messageHandlers.nativeBridge.postMessage(
Object.assign({}, options, { __callbackId: callbackId }),
);
},
};
- 注入脚本
struct WebView: UIViewRepresentable {
func makeUIView(context: Context) -> WKWebView {
// 配置 WKWebView
let configuration = WKWebViewConfiguration()
let contentController = WKUserContentController()
// 注入JS脚本
let userScript = WKUserScript(
source: bridgeScript,
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
contentController.addUserScript(userScript)
// 注册消息处理器,名称为 "nativeBridge"(与 H5 约定)
contentController.add(context.coordinator, name: "nativeBridge")
configuration.userContentController = contentController
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = context.coordinator // 可选,用于监听加载状态
context.coordinator.webView = webView
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
let request = URLRequest(url: URL(string: url)!)
uiView.load(request)
}
}
通信逻辑
流程:原生应用接收到H5消息 -> 执行原生代码逻辑 -> 封装传递给H5数据体 -> 执行在window上挂载的回调函数(JS代码)
struct WebView UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(onMessage: onMessage)
}
// 自定义协议
class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
let onMessage: (String) -> Void
weak var webView: WKWebView?
init(onMessage: @escaping (String) -> Void) {
self.onMessage = onMessage
}
//【核心部分】接收H5消息
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
/**
* 匹配JS代码: window.webkit.messageHandlers[message.name]
*/
guard message.name == "nativeBridge" else { return }
let dict = message.body as? [String: Any],
let jsonData = try? JSONSerialization.data(
withJSONObject: dict
),
let jsonString = String(data: jsonData, encoding: .utf8)
{
// onMessage(jsonString)
// 处理消息
handleMessage([
"callbackId": dict["__callbackId"],
"funcKey": dict["funcKey"],
"params": dict["params"],
])
} else {
// onMessage("\(message.body)")
}
}
// 处理H5的消息,原生应用所要处理的一些业务逻辑
func handleMessage(_ params: [String: Any?]) {
let funcKey = params["funcKey"] as? String
// 根据funKey执行不同的原生能力
switch funcKey {
// 一个获取设备信息的方法(demo)
case "getDeviceInfo":
let deviceInfo: [String: Any] = [
"model": UIDevice.current.model,
"systemName": UIDevice.current.systemName,
"systemVersion": UIDevice.current.systemVersion,
"name": UIDevice.current.name,
]
// 有回调,将设备信息作为参数调用回调函数
if let callbackId = params["callbackId"] as? String {
callJSFunction(
"window.__js_bridge_callback__",
arguments: [
"callbackId": callbackId,
"response": deviceInfo,
]
)
} else {
// onMessage("设备信息: \(deviceInfo)")
}
// 其他 case
default:
// onMessage("未知 funcKey: \( funcKey ?? "")")
}
}
//【核心部分】执行JS脚本
func callJSFunction(_ functionName: String, arguments: [String: Any?]) {
let response = arguments["response"] as? [String: Any?] ?? [:]
let callbackId = arguments["callbackId"] as? String ?? ""
var jsCode = "\(functionName)"
if callbackId != "" {
jsCode += ".\(callbackId)"
} else {
// print("未获取到callbackId参数,无法执行")
return
}
// 拼接出一个调用JS函数的字符串:`window.__js_bridge_callback__.${callbackId}(${response})`
if let jsonData = try? JSONSerialization.data(
withJSONObject: response
),
let jsonString = String(data: jsonData, encoding: .utf8)
{
jsCode += "(\(jsonString))"
} else {
jsCode += "({})"
}
// 删除已经完成调用的callback
jsCode += ";(function() { delete window.\(functionName).\(callbackId) })()"
// 执行JS代码
webView?.evaluateJavaScript(jsCode) { result, error in
if let error = error {
print("执行JS 失败: \(error.localizedDescription)")
} else {
print("执行 JS 成功,返回值: \(result ?? "nil")")
}
}
}
}
}
H5调用
window.H5JSBridge.call<DeviceInfo>(
{ funcKey: "getDeviceInfo" },
(response) => {
setDeviceInfo(response);
},
);
思考
JSBridge是一段提前注入的脚本,对外暴露了一些方法可以让H5在JS环境下直接调用,那么这个注入是由谁去执行(H5还是原生)?
优先考虑在原生应用中注入,原因如下:
- 提供更早的执行时机:可以在H5页面解析之前直接注入,实现在执行
<script>脚本时就能调用- 逻辑解耦:在H5中只需调用window.JSBridge上的方法,无需再做其他逻辑,如果涉及不同操作系统的应用时,只要统一JSBridge接口,即可实现H5的无缝衔接
- 安全考虑:H5存在被第三方恶意篡改的风险
- 使用
callbackId管理每个回调函数的意义?
保障每一次的调用中,原生应用都能找到关联的回调去执行脚本
3.【探索】在WebView容器,是如何实现调用window.webkit.messageHandlers.nativeBridge.postMessage就能触发原生应用的消息接收?
4.【探索】原生应用中,是如何做到evaluateJavaScript?
最后
以上仅为个人观点,如有错误欢迎指正。代码仅为Demo,切勿直接应用到生产环境。
参考资料
- 大疆Developer:developer.dji.com/doc/cloud-a…
- Medium: javascript.plainenglish.io/what-is-jsb…