Walk four directions, a wide knot of good karma, not friends for four directions, but for peace.

JSBridge

In mobile H5 development, we often invoke features on the (Android/iOS) side, some of which we can’t implement with the H5, and some of which are simply too lazy to develop again with the H5. The goal of JSBridge is to somehow evoke these end-to-end methods in H5.

H5 communicates with Native

Without further ado, let’s start with the code, which implements the function of invoking Native methods in H5:

// bridgeCall.js
function schemeJump (url) { // Execute the URL through the iframe subwindow
  let iframe = document.createElement('iframe')
  iframe.src = url
  iframe.style.display = 'none'
  document.documentElement.appendChild(iframe)
  setTimeout((a)= > {
    document.documentElement.removeChild(iframe)
  }, 0)}export default function bridgeCall (type, module, method, args) {
  let url = `fusion://${type}? `
  if (module) {
    url += `module=The ${module}& `
  }
  url += `method=${method}& `
  let param = args.map((arg) = > {
    return encodeURIComponent(JSON.stringify(arg))
  })
  url += `arguments=%5B${param}%5D&`
  url += `origin=The ${window.location.hostname}`
  schemeJump(url)
}
Copy the code

Before explaining this code, let’s get some background.

WebView

In Android, there is a called Android. Its the WebView component, it inherits from Android. The widget. AbsoluteLayout, allowing developers to load and show some H5 pages in it. If you spread webViews all over the page and all view representation is done by H5, then all you need to provide on the Android side is a WeView container. At the same time, a single piece of code can run both across browsers and Android, greatly reducing development costs. WKWebView has also been implemented on iOS since iOS 8.0, and its features and usage are similar to other WebViews. Since then, mobile H5 development based on WebView has become a best practice across multiple platforms (browsers, Android, iOS, etc.).

One of the most important features of the WebView, as a container for hosting H5 pages, is that it can capture all network requests made in the container. In fact, if YOU want JS to arouse Native methods, you only need to build a communication bridge between JS and Native, which is exactly realized by this feature of WebView.

The message

We can send a message to the Native end by making a network request, as in the above code through the child window iframe.src. Of course, it is also possible to make a request using location.href, but since location.href is the address of the current page, it is not recommended.

function schemeJump (url) { // Initiate a network request through the iframe subwindow
  let iframe = document.createElement('iframe')
  iframe.src = url
  iframe.style.display = 'none' // Do not display iframe
  document.documentElement.appendChild(iframe)
  setTimeout((a)= > {
    document.documentElement.removeChild(iframe)
  }, 0)}Copy the code

When communicating with Native, some parameters can be passed in. In the current scenario, there are communication type, module name, method name and input parameter, etc.

/** * @param type Communication type * @param module module name * @param method Method name * @param args input parameter */
function bridgeCall (type, module, method, args) {
  let url = `fusion://${type}? ` // The agreed agreement
  if (module) {
    url += `module=The ${module}& `
  }
  url += `method=${method}& `
  let param = args.map((arg) = > {
    return encodeURIComponent(JSON.stringify(arg))
  })
  url += `arguments=%5B${param}%5D&`
  url += `origin=The ${window.location.hostname}`
  schemeJump(url)
}
Copy the code

When the Native terminal captures the request of this protocol header, it will parse it, and the pseudocode is as follows:

IF the url to match"fusion://"DO parsing parametertype,module,method,args
  IF type= = ="invokeNative"DO implement module method FUNCS[module][method](args) END IF END IFCopy the code

We can also use Ajax to send network requests, as long as the Native side synchronizes a set of parsing rules with the H5. In real development, however, because Ajax is used to interact with the server, it is best to use the iframe child window to send the request.

Implement the callback

After invoking Native methods, it is often necessary to perform some callbacks. Since the client cannot execute JS code directly, but can obtain global variables in WebView, the callback method can be mounted on the global variables, and then the client can call the callback method on the global variables.

We can set up a singleton manager globally to manage the callbacks after all Native calls to facilitate the complexity of calling multiple Native methods at the same time.

// manager.js
function isFunction (func) {
  return Object.prototype.toString.call(callback) === '[object Function]'
}

export default {
  GLOBAL_INSTANCE: {
    callbacks: [].callbackId: 0
  },
  callbackJs (callbackId, args) {
    let callback = this.globalInstance.callbacks[callbackId]
    if (callback && isFunction(callback)) {
      callback()
    }
  }
}
Copy the code

The Native method is then invoked for further encapsulation and its callbacks are recorded, using the closure (decorator) pattern.

// jsBridge.js
import manager from './manager.js'
import bridgeCall from './bridgeCall.js'

window.manager = manager // mount it globally, so that it can be called on the end
function isFunction (func) {
  return Object.prototype.toString.call(callback) === '[object Function]'
}

export default class JSBridge {
    constructor () {}
    invokeNative (module, method) {
        return function (. args) { // hold the tangible parameter module, method
            for (var i = 0; i < args.length; i++) {
                if (isFunction(args[i])) {
                    args = args.slice(0, +i + 1)
                    manager.GLOBAL_INSTANCE.callbacks.push(args[i]) // Record the callback method
                    args[i] = manager.GLOBAL_INSTANCE.callbackId++ // Upload the callback index to the end
                    break
                }
            }
            bridgeCall('invokeNative'.module, method, args)
        }
    }
}
Copy the code

The following is an example of a call:

import JSBridge from './jsBridge.js'
let bridge = new JSBridge()
let requestLogin = bridge.invokeNative('COMMON_MODULE'.'requestLogin')
requestLogin({ saveSession: true }, function (res) {
    // do sth
})
Copy the code

When invoking Native methods, we need to know which methods are supported by the client, and an API list should be provided on the client.

Fusion

In large platforms, there are often multiple clients that are developed independently of each other. Each client has its own set of interfaces, which may have different names, input parameters, and callback parameters.

If an H5 page needs to write different code to invoke Native methods on different ends, there is no reusability at all. In addition, learning different apis for each end to implement the same function can be a huge burden and waste for developers.

In such cases, platforms often roll out a bridge of components that integrate various environments, allowing developers to invoke methods that implement the same functionality on different ends in a unified form. This component is commonly referred to as a Fusion(Polymer).

Suppose there is a function for selecting a local image method:

Modules (not required) The name of the parameter The callback parameter
The environment A common_module photograph opts: { type } Blob
Environment B local_module chooseImage opts: { suffix } dataUrl
Environmental C other_module openAlbum opts: {} dataUrl
  • In environment A, the value of type of the incoming attribute of photograph is 1 to enable the camera, and 2 to open the photo album, which are mandatory items. The callback argument is a shot or selected image in Blob object format.
  • In environment B, chooseImage’s function is to open album, and suffix is used to limit the suffixes of optional pictures, which is not mandatory. Pictures with all suffixes are allowed to be selected by default if not configured. The callback argument is the selected image in the dataUrl string format.
  • In environment C, The function of openAlbum is to open the album, with no input parameter, and the callback parameter is the selected image in Blob object format.

In line with the principle of maximum compatibility, the unified interface is constructed, and the result should be

The name of the parameter The callback parameter
chooseImage { type, suffix } dataUrl
  • When it is called, the method will first make a judgment of the environment and invoke Native methods on different ends according to the environment.
  • Because the attribute type is only available in environment A, it is non-mandatory as A parameter to the unified interface. Pass in A default value when determining environment A.
  • The dataUrl format is used here because it is commonly usedwindow.URL.createObjectURL(blob)Convert the BLOB object to a dataUrl string. In dealing with other methods should also be done according to custom compatibility.

The bridge model

At this point, it is important to mention the pattern used for this structure. As the name implies, JSBridge is closely related to the bridge pattern.

The bridge pattern is a structural pattern used to decouple abstraction from implementation so that the two can vary independently.

This pattern involves an interface that acts as a bridge so that the implementation of functionality is independent of the implementation of the interface, and the two types of classes can be structured without affecting each other.

In this case, abstraction refers to JSBridge’s abstraction of the interface, while implementation refers to the implementation of functionality on the client side. H5 just needs to know how to call JSBridge’s methods and doesn’t need to care about how those functions are implemented on the client side or whether the implementation of those functions has changed. As long as JSBridge can keep the invocation (names, incoming parameters, callback parameters, and so on) unchanged, the H5 and client code can be modified independently of each other, which is a very loose structure.

The following diagram briefly illustrates the intermediary role of JSBridge.