laitimes

前端H5与客户端Native交交原則 - JSBridge

author:Flash Gene

overview

In hybrid application development, a common and mature technology solution is to combine native applications with WebViews, so that complex business logic can be implemented through web technology. When implementing this type of hybrid application, it is necessary to address the two-way communication between H5 and Native. JSBridge is an important mechanism for communicating between web and native code in hybrid applications.

Hybrid development

Hybrid development is a development model that refers to the development of apps using multiple development models, which usually involves two types of technologies: native native and Web H5.

  • Native technology mainly refers to iOS and Android, and the native development efficiency is low, and the entire App needs to be repackaged after development, and the user-dependent updates need to be released, with higher performance and higher function coverage
  • Web H5 can better implement release updates and better cross-platform, but the performance is lower and the features are limited

The significance of hybrid development is to absorb the advantages of both, and with the upgrade and iteration of mobile phone hardware and the better support of the system (Android 5.0+, ISO 9.0+) for web features, the disadvantages of H5 are gradually reduced.

The concept and role of JSBridge

  • Communication bridge: JSBridge acts as a communication bridge between web and native applications. With JSBridge, we can communicate bi-directionally between the web and native code, enabling the two to call and pass data to each other.
  • Native function calls: With JSBridge, we can call features in native apps in JavaScript. We can use the web to trigger specific actions in the native app, such as turning on the camera, sending notifications, invoking hardware devices, and so on.
  • Data passing: JSBridge makes it easy to pass data between JavaScript and native code. This means that we can pass complex data structures such as objects, arrays, etc., between the web and native code to meet the functional needs of the application.
  • Callbacks: JSBridge supports callbacks, which allow JavaScript to be notified when the native code has performed certain actions and pass the corresponding results.

Why JSBridge is so important in hybrid app development

  • Cross-platform development: JSBridge allows us to run on different platforms at the same time with a single piece of code in a hybrid application. This means that we can use web technologies to develop the core logic of the application, and call native features through JSBridge when needed, enabling cross-platform development and improving development efficiency.
  • Native Extensions: With JSBridge, we can take full advantage of the features and capabilities provided by the native platform, such as accessing hardware devices, calling system APIs, and more. This allows us to add more rich features to the app and improve the user experience.
  • Flexibility and extensibility: JSBridge provides a flexible and extensible way to communicate between web and native code. Developers can add new native features at any time according to the needs of the application, and call these features in JavaScript through JSBridge to extend and upgrade the functionality of the application.

What JSBridge did

In hybrid mode, H5 will need to use Native's functions, such as opening QR code scanning, calling native pages, obtaining user information, etc., and Native also needs to send pushes and update status to the Web, while JavaScript is running in a separate JS Context (Webview container) isolated from the native runtime environment, so there needs to be a mechanism to achieve two-way communication between the Native side and the Web side This is JSBridge: a mechanism that uses the JavaScript engine or Webview container as the medium to communicate through the protocol to achieve two-way communication between the Native side and the Web side.

Through JSBridge, the web side can call the Native Java interface, and the Native side can also call the Web JavaScript interface through JSBridge to achieve bidirectional calls to each other.

前端H5与客户端Native交交原則 - JSBridge

JSBridge implementation principle

Compare web and native communication to the client/server pattern. JSBridge acts as an HTTP protocol that enables communication between the web and the native.

Encapsulate native interfaces to JavaScript interfaces: Encapsulate native functions that need to be called into JavaScript interfaces on the native side, so that JavaScript code can call them. The JavaScript interface is registered as a global object for JavaScript code to call.

Encapsulating the JavaScript interface on the web side into a native interface: This step is to encapsulate the JavaScript function that needs to be called into a native interface on the web side. These native interfaces are exposed to native code through some mechanism of WebView for native code to call.

Native -> Web

The Native side calls the Web side, and JavaScript, as an interpretive language, has the biggest feature that it can execute a piece of JS code through the interpreter anytime and anywhere, so you can pass the spliced JavaScript code string to the JS parser for execution, and the JS parser here is the webView.

1. Android

Android provides evaluateJavascript to execute JS code, and can get the return value to execute the callback:

String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
  @Override
  public void onReceiveValue(String value) {


  }
});           

2. IOS

WKWebView for IOS uses evaluateJavaScript:

[webView evaluateJavaScript:@"执行的JS代码" 
  completionHandler:^(id _Nullable response, NSError * _Nullable error) {
  // 
}];           

Web -> Native

There are two main ways to call the Native side of the web:

1. URL Schema

A URL schema is a request format for a URL-like URL in the following format:

<protocol>://<host>/<path>?<qeury>#fragment
  
// 我们可以自定义JSBridge通信的URL Schema,比如:
hellobike://showToast?text=hello           

After Native loads the WebView, all requests sent by the Web will pass through the WebView component, so Native can override the methods in the WebView to intercept requests initiated by the Web, and we will judge the format of the request:

  • Conform to our custom URL schema, parse the URL, obtain related operations and operations, and then call native native methods
  • Not conforming to our custom URL schema, we forward it directly and request the real service

For example:

get existOrderRedirect() {
    let url: string;
    if (this.env.isHelloBikeApp) {
      url = 'hellobike://hellobike.com/xxxxx_xxx?from_type=xxxx&selected_tab=xxxxx';
    } else if (this.env.isSFCApp) {
      url = 'hellohitch://hellohitch.com/xxx/xxxx?bottomTab=xxxx';
    }
    return url;
  }           

This approach has been around since early days and is very compatible, but because it is URL-based, it is limited in length and unintuitive, the data format is limited, and it takes time to establish a request.

2. Inject the JS API into the webview

Through the interface provided by the webView, the app injects the relevant interface of Native into the object of the Context(window) of JS

The web side can directly use this exposed global JS object in the global window, and then call the native method.

Android Injection Methods:

  • Before 4.2, Android injected JavaScript objects into addJavascriptInterface, but this interface has vulnerabilities
  • After 4.2, Android introduced a new interface, @JavascriptInterface, to address security issues, so there are compatibility issues with the way Android injects into objects.

IOS Injection Method:

  • UIWebView for iOS: JavaSciptCore supports iOS 7.0 and above
  • WKWebView for iOS: WKScriptMessageHandler supports iOS 8.0 and above

For example:

  • Inject global objects
// 注入全局JS对象
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");


class NativeBridge {
    private Context ctx;
    NativeBridge(Context ctx) {
        this.ctx = ctx;
    }


    // 绑定方法
    @JavascriptInterface
    public void showNativeDialog(String text) {
        new AlertDialog.Builder(ctx).setMessage(text).create().show();
    }
}

           
  • Web call method:
// 调用nativeBridge的方法
window.NativeBridge.showNativeDialog('hello');           

H5 concrete implementation

Abstract functionality into an AppBridge class, encapsulating two methods that handle interactions and callbacks.

Specific steps:

  • The first thing you need to do is define a JavaScript class or object to encapsulate the JSBridge method.
  • In the constructor of a JavaScript class or object, initialize the method of bridging the callback. This method is responsible for receiving the callback data from the native application and performing corresponding actions based on the information in the callback data.
  • Call Native Methods: Define a method that calls native methods in JavaScript. This method needs to receive a mapping of the native class, the name of the native method to be called, and the parameters passed to the native method, and pass this information to the native application.
  • Handling native callbacks: In the method of initializing bridging callbacks, you need to define the logic for handling native callbacks. When you receive the callback data of the native application, perform corresponding operations based on the information in the callback data, such as calling the callback function registered in JavaScript and passing the execution result or error message.

The specific implementation code:

  • Call native methods:
// 定义一个名为 callNative 的方法,用于在 JavaScript 中调用原生方法
callNative<P, R>(classMap: string, method: string, params: P): Promise<R> {
    return new Promise<R>((resolve, reject) => {
        // 生成一个唯一的回调 ID
        const id = v4();
        // 将当前的回调函数保存到 __callbacks 对象中,以 callbackId 作为键
        this.__callbacks[id] = { resolve, reject, method: `${classMap} - ${method}` };
        // 构造通信数据,包括原生类映射、要调用的方法、参数和 callbackId 
        const data = {
            classMap,
            method,
            params: params === null ? '' : JSON.stringify(params),
            callbackId: id,
        };
        const dataStr = JSON.stringify(data);
        // 根据当前环境判断是 iOS 还是 Android,并调用相应平台的原生方法
        if (this.env.isIOS && isFunction(window?.webkit?.messageHandlers?.callNative?.postMessage)) {
            // 如果是 iOS 平台,则调用 iOS 的原生方法
            window.webkit.messageHandlers.callNative.postMessage(dataStr);
        } else if (this.env.isAndroid && isFunction(window?.AppFunctions?.callNative)) {
            // 如果是 Android 平台,则调用 Android 的原生方法
            window.AppFunctions.callNative(dataStr);
        }
    });
}           
  • Callback handling:
// 初始化桥接回调函数,该参数在 constructor 中调用
private initBridgeCallback() {
    // 保存旧的回调函数到 oldCallback 变量中
    const oldCallback = window.callBack;
    // 重新定义 window.callBack 方法,用于处理原生应用的回调数据
    window.callBack = (data) => {
        // 如果存在旧的回调函数,则调用旧的回调函数
        if (isFunction(oldCallback)) {
            oldCallback(data);
        }
        // 获取原生应用的回调信息,包括数据和回调 ID
        console.info('native callback', data, data.callbackId);
        // 从回调数据中获取回调 ID
        const { callbackId } = data;
        // 根据回调 ID 查找对应的回调函数
        const callback = this.__callbacks[callbackId];
        // 如果找到了对应的回调函数
        if (callback) {
            // 如果回调数据中的 code 为 0,则表示执行成功,调用 resolve 方法处理成功的结果
            if (data.code === 0) {
                callback.resolve(data.data);
            } else {
                // 否则,表示执行失败,构造一个错误对象并调用 reject 方法处理错误信息
                const error = new Error(data.msg) as Error & {response:unknown};
                error.response = data;
                callback.reject(error);
            }
            // 删除已经处理过的回调函数
            delete this.__callbacks[callbackId];
        }
    };
}           
  • Use:
// 调用原生方法的封装函数
callNative<P, R>(classMap: string, method: string, params: P) {
    // 从容器中解析出 AppBridge 实例
    const bridge = container.resolve<AppBridge>(AppBridge);
    // 使用 bind 方法将 AppBridge 实例中的 callNative 方法绑定到 bridge 对象上,并保存到 func 变量中
    const func = bridge.callNative.bind(bridge);
    // 调用 func 方法,并传入 classMap、method 和 params 参数,实现调用原生方法的功能
    return func<P, R>(classMap, method, params);
}




// 打开 webview
// 调用 callNative 方法,传入参数 url,classMap 为 'xxxxx/hitch',method 为 'openWebview'
openWebView(url: string): Promise<void> {
    return this.callNative<{url:string}, void>('xxxxx/hitch', 'openWebview', { url });
}




// 获取驾驶证 OCR 信息
getDriverLicenseOcrInfo(
    params: HBNative.getDriverLicenseOcrInfo.Params,
): Promise<HBNative.getDriverLicenseOcrInfo.Result> {
    // 调用 callNative 方法,传入参数 params,classMap 为 'xxxxx/hitch',method 为 'getOcrInfo'
    // 返回一个 Promise 对象,该 Promise 对象用于处理异步结果
    return this.callNative<
        HBNative.getDriverLicenseOcrInfo.Params,
        HBNative.getDriverLicenseOcrInfo.Result>(
            'xxxxx/hitch', 'getOcrInfo', params,
        );
}           

Author: Tong Jian

Source-WeChat public account: Hello Technology

Source: https://mp.weixin.qq.com/s/h6vlkf5rgyI9GpEpPxO9cg