一文弄懂Hybrid原生与H5通信&JSBridge
一. 通信的必要性 & 常见场景
-
H5页面调用原生设备功能:由于浏览器环境的限制,H5页面无法直接访问很多原生设备的功能,如摄像头、文件系统、GPS、蓝牙、NFC等。
-
用户行为的响应和处理:考虑到用户体验的一致性以及性能优化等,H5页面中的用户行为(比如点击、滑动复杂计算等操作可能需要APP进行处理。)
-
数据交换:在一些复杂的应用场景下,H5页面和APP可能需要共享数据。例如,用户在APP中登录后,H5页面可能需要获取用户的登录信息。或者,用户在H5页面中进行了一些操作(比如修改了设置),这些操作的结果可能需要同步到APP中。
-
页面跳转:H5页面可能需要触发APP内的页面跳转。
-
事件通知:H5页面可能需要向APP报告一些事件,例如页面加载完成、数据加载错误等。这些事件通常是通过Javascript Bridge通知给APP的。
二. 通信的方式
- JavaScript Bridge: JavaScript桥是一种可以让JavaScript在原生应用中运行的方法。这是最常见的方式,因为它允许H5和原生应用之间互相通信。
在原生应用内部封装一个WebView组件,然后在WebView内部运行H5代码。在JavaScript与原生之间构建一个“桥梁”,使得JavaScript可以调用原生的API接口,反之亦然。
-
URL Scheme: URL Scheme是一种特殊的URL,可以用来启动应用、切换到应用或在应用中执行特定的操作。URL Scheme可以通过H5的超链接或者JavaScript的window.location方式进行调用
使用场景:
- 跨应用通信(比如在浏览器中打开App)、(打开地图等)
- 深度链接: URL Scheme也可以被用于创建深度链接,也就是可以直接打开应用并导航到特定页面的链接。例如,一个商务应用可能有一个URL Scheme链接,用户点击这个链接后,可以直接在应用中打开特定的产品页面。
三. JS Bridge实现
一句话小结:
- H5给原生发信息: 通过原生在webview组件上(window对象)上注入全局方法,给h5调用,来让h5调用原生特定功能。
- 原生给H5发信息:H5通过在window对象上定义特定方法,让原生应用可以通过WebView的接口evaluateJavaScript(或相似的)方法直接调用这些函数。
3.1. 安卓如何构建桥方法:
在安卓中,你可以通过 WebView 的 addJavascriptInterface
方法来向JavaScript暴露原生接口。例如:
public class WebAppInterface {
Context mContext;
WebAppInterface(Context c) {
mContext = c;
}
@JavascriptInterface
public void showMessage(String message) {
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
}
}
WebView webView = (WebView) findViewById(R.id.webview);
webView.addJavascriptInterface(new WebAppInterface(this), "Android");
在这个例子中,我们创建了一个名为 WebAppInterface
的类,并为其添加了一个名为 showMessage
的方法。然后,我们通过 addJavascriptInterface
方法将此类的一个实例添加到 WebView 中,并命名为 "Android"。这样,JavaScript就可以通过 window.Android.showMessage
来调用此方法。
3.2 IOS如何构建桥方法:
在iOS中,实现JavaScript Bridge的方式有很多,例如通过WKWebView的userContentController
和WKScriptMessageHandler
。这里是一个基本的例子:
let contentController = WKUserContentController()
contentController.add(self, name: "ios")
let config = WKWebViewConfiguration()
config.userContentController = contentController
let webView = WKWebView(frame: .zero, configuration: config)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "ios", let messageBody = message.body as? String {
print(messageBody)
}
}
在这个例子中,我们添加了一个名为"ios"的脚本消息处理器,然后在 didReceive
方法中处理 JavaScript 的消息。
3.3 H5如何调用桥方法给原生发信息:
在H5页面中,你可以直接调用前面我们创建的桥接方法来给原生应用发送消息。例如:
对于安卓:
window.Android.showMessage("Hello, Android!");
对于iOS:
window.webkit.messageHandlers.ios.postMessage("Hello, iOS!");
3.4 原生如何通过桥给H5发信息:
原生应用可以通过 WebView 的 evaluateJavascript
方法(对于安卓)或 evaluateJavaScript
方法(对于iOS)来执行 JavaScript 代码,从而给H5发送消息。例如:
对于安卓:
webView.evaluateJavascript("javascript: showMessage('Hello, Web!')", null);
对于iOS:
webView.evaluateJavaScript("showMessage('Hello, Web!')", completionHandler: nil);
在这些例子中,showMessage
是 H5 页面中的一个 JavaScript 函数,原生应用通过执行这个函数来给 H5 发送消息。
**3.5 Q & A
- h5调用Ios方法的命令为什么比安卓复杂?(window.webkit.messageHandlers.ios.postMessage)
安全性是主要的考虑因素。
- 在 Android 中,使用
addJavascriptInterface
方法可以直接将 Java 对象暴露给 JavaScript,这样可能会导致安全问题,因为 JavaScript 可能会访问并执行 Java 对象的任何公共方法。尽管可以通过@JavascriptInterface
注解来限制哪些方法可以被 JavaScript 调用,但是这种方式仍然需要开发者在编写代码时格外小心。 - iOS 的
window.webkit.messageHandlers.ios.postMessage
方法只允许 JavaScript 向原生代码发送字符串消息,这种方式更安全,因为原生代码可以完全控制如何处理这些消息。
灵活性也是一个考虑因素。
- 在 Android 中,需要为每一个要暴露给 JavaScript 的方法创建一个单独的 Java 方法。
- iOS 的
window.webkit.messageHandlers.ios.postMessage
方法可以接收任何类型的 JavaScript 消息,然后在原生代码中根据消息的内容来决定如何处理。
四. h5封装一个通用的JsBridge库的示例
我们在H5中去调用桥方法的时候,可以考虑抽取通用的桥方法库,让桥的调用像调用普通js方法一样简单,这里提供一个简单示例. PS:实际的实现结合具体的桥协议、业务场景会更加复杂。
4.1 通用特性考虑:
- 环境检测:需要对运行环境进行检测,判断当前是在Android、iOS还是其他环境下,这样可以根据不同环境调用不同的接口。
- 接口封装:将原生提供的接口封装成JavaScript函数,使其可以直接被JavaScript调用。这些函数可能包括调用设备功能(如获取设备信息、访问相机等)、与原生应用交互(如发送事件、接收事件等)等功能。
- 错误处理:在封装的接口中添加错误处理,确保当调用失败时可以提供明确的错误信息。
- 事件系统:提供事件监听和触发的功能,使得JavaScript可以监听和触发原生应用的事件。
下面是一个简化的JSBridge库设计示例:
const JSBridge = (function() {
// Check if JSBridge has already been created
if (window?.JSBridge) { return window.JSBridge; }
const isAndroid = window.android ? true : false;
const isiOS = window.webkit ? true : false;
const callbackMap = new Map();
const eventListeners = new Map();
let callbackCounter = 0;
// Call a native method and return a promise
function callNative(method, params) {
return new Promise((resolve, reject) => {
const callbackId = 'cb_' + callbackCounter++;
callbackMap.set(callbackId, { resolve, reject });
// 具体的message设计要看桥方法的协议
let message = {
method,
params,
callbackId,
};
try {
if (isAndroid) {
window.android.call(JSON.stringify(message));
} else if (isiOS) {
window.webkit.messageHandlers.call.postMessage(message);
} else {
throw new Error('Unsupported environment');
}
} catch (error) {
reject(error);
}
});
}
// Called by native code
function onNativeCallback(callbackId, result, error) {
const callback = callbackMap.get(callbackId);
if (callback) {
if (error) {
callback.reject(error);
} else {
callback.resolve(result);
}
callbackMap.delete(callbackId);
}
}
// Called by native code
function onNativeEvent(eventName, ...args) {
const listeners = eventListeners.get(eventName);
if (listeners) {
listeners.forEach(listener => listener(...args));
}
}
// Add an event listener
function addEventListener(eventName, listener) {
let listeners = eventListeners.get(eventName);
if (!listeners) {
listeners = new Set();
eventListeners.set(eventName, listeners);
}
listeners.add(listener);
}
// Remove an event listener
function removeEventListener(eventName, listener) {
const listeners = eventListeners.get(eventName);
if (listeners) {
listeners.delete(listener);
}
}
// Expose to global scope for native code to call
window.onNativeCallback = onNativeCallback;
window.onNativeEvent = onNativeEvent;
// The public API
return {
callNative,
addEventListener,
removeEventListener,
};
})();
export default JSBridge;
使用者可以像这样使用这个库:
async function getDeviceId() {
try {
const deviceId = await JSBridge.callNative('getDeviceId');
console.log(deviceId);
} catch (error) {
console.error('Failed to get device id:', error);
}
}
getDeviceId();
转载自:https://juejin.cn/post/7260389317175476280