preface

In the process of Hybrid development, due to the cognitive difference between the students at the front end and the client, there are certain communication costs and information asymmetry when solving some bridge problems. From a front-end perspective, this article describes how the Bridge approach interacts with the client and the various intermediate processes that occur along the way.

Communication between Native and Webview

  • JavaScript calls Native methods

There are three main ways for JavaScript to call Native methods in Webview:

  1. Native to the Webview Contextwindow) to inject aExpose the specified Native method (Android)Receiving JavaScript messages (iOS)The object.
  2. Intercept a specific URL Scheme in Webview, and execute the corresponding Native method according to the URL.
  3. Interception of JavaScriptconsole.logalertpromptconfirmAnd execute the corresponding Native method.

In the current mainstream JSSDK implementation, the first two communication modes are mainly adopted, and the injection is the main method, and the interception is the bottom line strategy for communication. This paper will mainly introduce the implementation principles and application scenarios based on these two modes.

Injection (the first approach) provides better performance and a better development experience, but is not compatible with all system versions.

Before Android version 4.2, JavascriptInterface exposes other interfaces that should not be exposed, including system class (Java.lang.Runtime) methods, which has a large security risk. WKScriptMessageHandler for iOS only supports iOS 8.0+.

Therefore, in the lower version of the system, interception (that is, the second way) will be used as the method of communication with Native.

  • Native calls JavaScript methods

There are two main ways for Native to call JavaScript methods within a particular Webview:

  1. directlyJavaScript statements are executed through urls, e.g.javascript:alert('calling... ');
  2. Through the Android and iOS namesake methodevaluateJavascript()To execute JavaScript statements.

The second method is only compatible with Android 4.4+ and iOS 8.0+. Compared with the first method, it has the advantage that it can get the return value of JavaScript and is the official recommended communication method.

Calling NATIVE methods

Combined with various mainstream JSSDK implementations, the process of calling NATIVE methods is roughly as follows:

Calling compatible methods

We refer to the entry points where JavaScript calls native methods or listens to native events as compatible methods, which are often mapped to a particular native method or native event listener depending on the host environment.

For example, toast(), which is a compatible method to notify clients to pop up with a specified text, can be implemented in the JSSDK as follows:

Here, core.pipecall () is the core entry for calling native methods in JSSDK, which mainly involves the following parameters:

  • method: Compatible methodMethod name
  • params: Compatible methodThe arguments
  • callback: Get the callback function returned by Native and call it in business codeCompatible methodWhen defining
  • rules: Compatible methodOf the rules, includingThe native method name of the mapping, the incoming/outgoing parameter processing function, and the host ID,Version compatibility Information, there may be complex numbers, and the applicable one will be matched laterThe rulesIf not, it will report an error and adoptOut rules

The SDK Bridge entrance

After entering pipeCall(), the container environment checkup and the onInvokeStart() lifecycle functions are then executed in turn. Then, Native readable realmethods and realParams are resolved through the rules in the input parameters, as well as any reference processing and environment information that may be used in the callback:

async pipeCall({ method, params, callback, rules }) {
    if(! isBrowser &&this.container === 'web') {
      return Promise.resolve()
    }
    let config = {
      method,
      params,
    }
    if (this.onInvokeStart) {
      config = this.onInvokeStart(hookConfig)
    }
    const { realMethod, realParams, rule, env } = await this.transformConfig(method, params, rules)
    ...
}
Copy the code

In transformConfig(), the applicable rule is matched, the name of the native method is mapped, and the entry is completed:

async transformConfig(method, params, rules) {
    const env = this.env || (await this.getEnv)
    const rule = this.getRuleForMethod(env, rules) || {}

    let realMethod = (rule.map && rule.map.method) || method

    const realParams = rule.preprocess ? rule.preprocess(params, { env, bridge: this.bridge }) : params
    return { realMethod, realParams, rule, env }
  }
Copy the code

Finally, call the SDK-injected window.jsbridge-call ().

In the incoming callback, we do global outbound processing, method outbound processing (obtained from the previously parsed rule), execute the previously passed callback function in the business code, and finally execute the onInvokeEnd() lifecycle function of the environment variable:

return new Promise((resolve, reject) = > {
        this.bridge.call(
          realMethod,
          realParams,
          (realRes) = > {
            let res = realRes
            try {
              if (globalPostprocess && typeof globalPostprocess === 'function') {
                res = globalPostprocess(res, { params, env })
              }
              if (rule.postprocess && typeof rule.postprocess === 'function') {
                res = rule.postprocess(res, { params, env })
              }
            } catch (error) {
              if (this.onInvokeEnd) {
                this.onInvokeEnd({ error: error, config: hookConfig })
              }
              reject(error)
            }
            if (typeof callback === 'function') {
              callback(res)
            }
            resolve(res)
            if (this.onInvokeEnd) {
              this.onInvokeEnd({ response: res, config: hookConfig })
            }
          },
          Object.assign(this.options, options),
        )
      })
Copy the code

Call Bridge methods

The window.jbridge.call () method spells out a Message to communicate with the Native based on the incoming arguments, and adds the incoming callback argument to the global callbackMap property, identified by a callbackId. The structure design of Message is as follows:

export interface JavaScriptMessage {
    func: string;    // Where func is the native method name
    params: object; __msg_type: JavaScriptMessageType; __callback_id? :string; __iframe_url? :string;
}
Copy the code

Then the spell good Message through the window. The JSBridge. SendMessageToNative sent to Native (), there will appear two situations:

private sendMessageToNative(message: JavaScriptMessage): void {
    if (String(message.JSSDK) ! = ="1" && this.nativeMethodInvoker) {
        const nativeMessageJSON = this.nativeMethodInvoker(message);
        /** * If this method returns, the client calls synchronously */
        if (nativeMessageJSON) {
            const nativeMessage = JSON.parse(nativeMessageJSON);
            this.handleMessageFromNative(nativeMessage); }}else {
        // Fallback to the iframe call mode if no global API is detected
        this.javascriptMessageQueue.push(message);
        if (!this.dispatchMessageIFrame) {
            this.tryCreateIFrames();
            return;
        }
        this.dispatchMessageIFrame.src = `The ${this.scheme}The ${this.dispatchMessagePath}`; }}Copy the code

Injection call

In the case of Native injection of JS2NativeBridge object, the SDK will add the nativeMethodInvoker method under windowing.JSBridge during initialization to directly call the Bridge API exposed by Native. Enter Message in JSON format:

const nativeMessageJSON = this.nativeMethodInvoker(message);
/** * If this method returns, the client calls synchronously */
if (nativeMessageJSON) {
    const nativeMessage = JSON.parse(nativeMessageJSON);
    this.handleMessageFromNative(nativeMessage);
}
Copy the code

There are two branches here. If the Native implementation is called synchronously, the result can be fetched directly and the front end executes the callback function. If the implementation is asynchronous, then the client executes the callback function.

Interception calls

If Native does not inject JS2NativeBridge object, the interception policy of matching URL Scheme through iframe will be degraded. When the SDK is initialized, a Message queue is generated to temporarily store the Message to be executed and consumed when Native intercepts the URL:

// Fallback to the iframe call mode if no global API is detected
this.javascriptMessageQueue.push(message);
if (!this.dispatchMessageIFrame) {
    this.tryCreateIFrames();
    return;
}
this.dispatchMessageIFrame.src = `The ${this.scheme}The ${this.dispatchMessagePath}`;
Copy the code

Native object injection during SDK initialization

During initialization, the SDK will create the corresponding nativeMethodInvoker according to the object injection of Native:

/** * Probe client injection call API */
export function detectNativeMethodInvoker() :NativeMethodInvoker|undefined {
  let nativeMethodInvoker;

  if (global.JS2NativeBridge && global.JS2NativeBridge._invokeMethod) { // Standard implementation
      nativeMethodInvoker = (message: JavaScriptMessage) = > {
          return global.JS2NativeBridge._invokeMethod(JSON.stringify(message));
      };
  }

  return nativeMethodInvoker;
}
Copy the code

Listening for NATIVE events

Combined with various mainstream JSSDK implementations, the process of listening to NATIVE events is roughly as follows:

Calling compatible methods

In order to reverse the ability of Native to call JavaScript, you need to listen to Native’s Native events for callback processing. In the case of onAppShow(), this method is Native to notify the JavaScript container (Activity or ViewController) that it has come back to the foreground, and to execute the corresponding callback function:

import core from "./core"
import rules from "./onAppShow.rule"

interface JSBridgeRequest {}
interface JSBridgeResponse {}

interface Subscription {
  remove: () = > void
  listener: (_: JSBridgeResponse) = > void
}

function onAppShow(
  callback: (_: JSBridgeResponse) => void, once? :boolean
) :Subscription {
  return core.pipeEvent({
    event: "onAppShow",
    callback,
    rules,
    once,
  })
}

onAppShow.rules = rules
export default onAppShow
Copy the code

Here, core.pipeEvent() is the core entry for listening for native events in JSSDK, which mainly involves the following parameters:

  • event: Compatible methodName of the listener method
  • callback: get to theNative eventsCallback function, called in the business codeCompatible methodWhen defining
  • rules: Compatible methodOf the rules, includingThe native method name of the mapping, the incoming/outgoing parameter processing function, and the host ID,Version compatibility Information, there may be complex numbers, and the applicable one will be matched laterThe rulesIf not, it will report an error and adoptOut rules
  • once: Used to determine whether to invoke only once

The SDK Bridge entrance

After entering pipeEvent(), the container environment verification is performed, and then the incoming parameters are processed through the rules in the incoming parameters, the realMethod that is Native readable is analyzed, and the outbound parameters that may be used in the callback (the same as calling the Native method). Finally, call the SDK-injected window.jsbridge-on ().

In the incoming callbacks, the global callbacks are processed, the method callbacks are processed (obtained from the previously parsed rule), and the previously passed callbacks in the business code are executed.

Finally, pipeEvent() returns a method to remove the listener and the callback function passed in:

pipeEvent({ event, callback, rules, once }) {
    if(! isBrowser &&this.container === 'web') {
      return {
        remove: () = > {},
        listener: callback,
      }
    }
    const promise = this.transformConfig(event, null, rules)

    const excutor = promise.then(({ realMethod, rule, env }) = > {
      function realCallback(realRes) {
        let res = realRes
        if (globalPostprocess && typeof globalPostprocess === 'function') {
          res = globalPostprocess(res, { env })
        }
        if (rule.postprocess && typeof rule.postprocess === 'function') {
          res = rule.postprocess(res, { env })
        }
        if (rule.postprocess) {
          if(realRes ! = =null) {
            // Callback is called only if any data except null is returned
            callback(res)
          }
        } else {
          callback(res)
        }
      }
      const callbackId = this.bridge.on(realMethod, realCallback, once)
      return [realMethod, callbackId]
    })
    return {
      remove: () = > {
        excutor.then(([realMethod, callbackId]) = > {
          this.bridge.off(realMethod, callbackId)
        })
      },
      listener: callback,
    }
  }
Copy the code

Call the Bridge listener method

The window.jsbridg.event () method adds the passed callback argument to the global callbackMap property, identified by a callbackId; Next, add the native event to the global eventMap property and bind the generated callbackId to the corresponding native event in the eventMap:

public on(
    event: string.callback: Callback,
    once: boolean = false) :string {
    if (
        !event ||
        typeofevent ! = ='string' ||
        typeofcallback ! = ='function'
    ) {
        return;
    }
    const callbackId = this.registerCallback(event, callback);
    this.eventMap[event] = this.eventMap[event] || {};
    this.eventMap[event][callbackId] = {
        once
    };
}
Copy the code

Remove the Bridge listener method

public off(event: string.callbackId: string) :boolean {
    if(! event ||typeofevent ! = ='string') {
        return true;
    }

    const callbackMetaMap = this.eventMap[event];
    if (
        !callbackMetaMap ||
        typeofcallbackMetaMap ! = ='object'| |! callbackMetaMap.hasOwnProperty(callbackId) ) {return true;
    }
    this.deregisterCallback(callbackId);
    delete callbackMetaMap[callbackId];
    return true;
}
Copy the code

If you’re interested…

Byte smart invites you to submit your resume, business development is fast, HC more ~

We are engaged in the front-end research and development of smart Working light/Tutoring APP and related educational products at home and abroad. Business scenarios include H5, Flutter, mini program and various Hybrid scenarios. In addition, our team has some practice and precipitation in monorepo, micro front end, Serverless and other cutting-edge front-end technologies. Common technology stacks include but are not limited to React, TS and Nodejs.

Scan the qr code below to get the push-code:

Welcome to “ByteFE”

Resume delivery email: [email protected]