一文弄懂Hybrid原生与H5通信&JSBridge

一. 通信的必要性 & 常见场景

  1. H5页面调用原生设备功能:由于浏览器环境的限制,H5页面无法直接访问很多原生设备的功能,如摄像头、文件系统、GPS、蓝牙、NFC等。

  2. 用户行为的响应和处理:考虑到用户体验的一致性以及性能优化等,H5页面中的用户行为(比如点击、滑动复杂计算等操作可能需要APP进行处理。)

  3. 数据交换:在一些复杂的应用场景下,H5页面和APP可能需要共享数据。例如,用户在APP中登录后,H5页面可能需要获取用户的登录信息。或者,用户在H5页面中进行了一些操作(比如修改了设置),这些操作的结果可能需要同步到APP中。

  4. 页面跳转:H5页面可能需要触发APP内的页面跳转。

  5. 事件通知:H5页面可能需要向APP报告一些事件,例如页面加载完成、数据加载错误等。这些事件通常是通过Javascript Bridge通知给APP的。

二. 通信的方式

  1. JavaScript Bridge: JavaScript桥是一种可以让JavaScript在原生应用中运行的方法。这是最常见的方式,因为它允许H5和原生应用之间互相通信。

在原生应用内部封装一个WebView组件,然后在WebView内部运行H5代码。在JavaScript与原生之间构建一个"桥梁",使得JavaScript可以调用原生的API接口,反之亦然。

  1. 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暴露原生接口。例如:

java 复制代码
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的userContentControllerWKScriptMessageHandler。这里是一个基本的例子:

swift 复制代码
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页面中,你可以直接调用前面我们创建的桥接方法来给原生应用发送消息。例如:

对于安卓:

javascript 复制代码
window.Android.showMessage("Hello, Android!");

对于iOS:

javascript 复制代码
window.webkit.messageHandlers.ios.postMessage("Hello, iOS!");

3.4 原生如何通过桥给H5发信息:

原生应用可以通过 WebView 的 evaluateJavascript 方法(对于安卓)或 evaluateJavaScript 方法(对于iOS)来执行 JavaScript 代码,从而给H5发送消息。例如:

对于安卓:

java 复制代码
webView.evaluateJavascript("javascript: showMessage('Hello, Web!')", null);

对于iOS:

swift 复制代码
webView.evaluateJavaScript("showMessage('Hello, Web!')", completionHandler: nil);

在这些例子中,showMessage 是 H5 页面中的一个 JavaScript 函数,原生应用通过执行这个函数来给 H5 发送消息。

**3.5 Q & A

  1. 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 通用特性考虑:

  1. 环境检测:需要对运行环境进行检测,判断当前是在Android、iOS还是其他环境下,这样可以根据不同环境调用不同的接口。
  2. 接口封装:将原生提供的接口封装成JavaScript函数,使其可以直接被JavaScript调用。这些函数可能包括调用设备功能(如获取设备信息、访问相机等)、与原生应用交互(如发送事件、接收事件等)等功能。
  3. 错误处理:在封装的接口中添加错误处理,确保当调用失败时可以提供明确的错误信息。
  4. 事件系统:提供事件监听和触发的功能,使得JavaScript可以监听和触发原生应用的事件。

下面是一个简化的JSBridge库设计示例:

js 复制代码
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;

使用者可以像这样使用这个库:

javascript 复制代码
async function getDeviceId() {
  try {
    const deviceId = await JSBridge.callNative('getDeviceId');
    console.log(deviceId);
  } catch (error) {
    console.error('Failed to get device id:', error);
  }
}

getDeviceId();
相关推荐
&白帝&1 小时前
Vue.js 过渡 & 动画
前端·javascript
总是学不会.1 小时前
SpringBoot项目:前后端打包与部署(使用 Maven)
java·服务器·前端·后端·maven
Fanfffff7202 小时前
深入探索Vue3组合式API
前端·javascript·vue.js
光影少年2 小时前
node配置swagger
前端·javascript·node.js·swagger
昱禹2 小时前
关于CSS Grid布局
前端·javascript·css
啊QQQQQ2 小时前
HTML:相关概念以及标签
前端·html
就叫飞六吧3 小时前
vue2和vue3全面对比
前端·javascript·vue.js
Justinc.3 小时前
CSS基础-盒子模型(三)
前端·css
qq_2518364573 小时前
基于ssm vue uniapp实现的爱心小屋公益机构智慧管理系统
前端·vue.js·uni-app
._Ha!n.3 小时前
Vue基础(二)
前端·javascript·vue.js