JSBridge principle and packaging

The functions of mobile applications are becoming more and more abundant, and some functions need to be realized in H5

This requires H5 to interact with Native. We use JSBridge to communicate between them

JSBridge defines the communication mode between Native and H5

Native calls H5 through the bridge object of the contract

H5 call Native through the bridge object of the contract


1, Interaction mode

1. Pseudo protocol url scheme (applicable to all devices)

The pseudo protocol is similar to url. H5 triggers the pseudo protocol, and the system judges according to priority:

  1. Whether it is a system application. If yes, open the system application
  2. Whether an app registers the current pseudo protocol. If yes, open the app
  3. native triggers the url event, captures the pseudo protocol, parses the pseudo protocol, and calls the method

Specific process:

  1. H5 call Native

    Trigger pseudo protocol, for example:

    window.location.href= 'xxx://event=a&data=b&successName=c&failName=d'

  2. Native call H5

    H5 registers methods in the window for native calls (the same as native calls H5 in API interaction mode)


2. Interaction through API

Specific process:

  1. H5 call Native

    1.1. H5 calls Android: native is registered through addjavascript interface and then called by H5

    1.2. H5 calls iOS: native is registered through JavaScript core (above iOS 7) and then called by H5

  2. Native call H5

    2.1. Android calls H5: H5 registers methods on the window. native calls H5 through loadUrl. Versions 4.4 and above can be called through evaluateJavascript

    2.2. iOS calls H5: H5 registers the method on the window, and native calls H5 through stringByEvaluatingJavascriptFromString


3. Distinction

The difference is how H5 calls Native and top note Native responds to H5.

3.1 how H5 calls Native:

Pseudo protocol:

window.location.href = 'xxx://event=a&data=b&successName=c&failName=d'

API:

window.JSActionBridge.handlerAction(
    event,
    data,
    successName,
    failName
)
window.webkit.messageHandlers.JSActionBridge.postMessage({
    method: 'handlerAction',
    data: {
        actionEvent: event,
        paramsJsonValue: data,
        successCallback: successName,
        errorCallback: failName
    }
})

3.2 Native response H5 is not listed


3.3 benefits of API interaction:

  • It's easier to write in H5 without creating a URL
  • H5 is more convenient to pass parameters. The method of intercepting URL is used. The parameters need to be spliced behind the URL. If they contain special characters, they need to be escaped, otherwise there will be errors in parsing parameters, such as & =?
  • for example
    window.location.href = 'xxx://event=a&data=http://www.baidu.com/xxx.html?p=2&successName=c&failName=d'

    Here, the value obtained by parsing the parameter is not the desired value, which needs to be corrected http://www.baidu.com/xxx.html?p=2 Escape

2, Scheme

At present, the company uses API to interact internally (Android needs more than 4.2 and iOS needs more than 7)

1. Agreed name

Primary pass JSActionBridge register JSBridge object
H5 adopt JSActionBridge obtain JSBridge object

2. Native JSBridge object registration

Android:

webView.addJavascriptInterface(new JSActionBridge(), "JSActionBridge")

iOS:

// Register via JavaScript core
self.context = [[JSContext alloc] int];
self.context[@"JSActionBridge"] = self.bridgeobj;

3. Call

3.1 definition of native method

Android: 
/*
* @params {String} actionEvent: Native event name and method name to be called
* @params {String|Object|Array} paramsJsonValue:  The parameters passed to the native should preferably be in json format
* @params {String} successCallBack: The name of the callback function to be executed after calling the native method successfully. This function is defined on the window
* @params {String} errorCallback: The name of the callback function to be executed after calling the native method fails, which is defined on the window
*/
void handlerAction(String actionEvent, String paramsJsonValue, String successCallBack, String errorCallback)

3.2 H5 call Native

JSActionBridge.handlerAction(actionEvent, paramsJsonValue, successCallBack, errorCallback)

3.3 Native call H5

Android calls H5

String callbackUrl = "javascript:" + callback + "(\"" + jsonData + "\")";
webView.loadUrl(callbackUrl);

iOS call H5

[webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat: @"callBack(%@)", jsonData]];

3, Data Convention

Native method definition

void handlerAction(String actionEvent, String paramsJsonValue, String successCallBack, String errorCallback)

1. Both successful and failed callback functions return json data

Data format:

{
    "code":0, 
    "desc":"success", 
    "data": nativeReturnData
}

2. When the contract code === 0, the call is successful, otherwise it fails

3. actionEvent classification

  • Jump to native PAGE

    Home page, login registration page, etc
  • Get native data

    Obtain user information, equipment information, etc
  • Call native functions

    Jump to new page, wake up password input box, view network status, set title, etc
  • other

4, JSBridge encapsulation

1. Agreement:

  • Android transfer string
  • iOS delivery json

2. successCallBack/errorCallback callback data format:

{
    "code":0, 
    "desc":"success", 
    "data": nativeData
}

3. JSBridge encapsulation

import { isIOS, isAndroid, isApp } from './utils.js'
const JSBridge = {
    isApp,

    // APP offline environment
    isOfflineApp: isApp && location.protocol === 'file:',

    // H5 call App
    /**
     * Call App method
     * @param event: Event name
     * @param data: json data
     * @param successCallBack: Successful callback
     * @param failCallBack: Failed callback
     * @returns {Promise<object>}
     */
    callApp (event, data = {}) {
        return new Promise((resolve, reject) => {
            // Call not in App, prompt not in App
            if (!this.isApp) {
                resolve('not in app')
                return
            }

            // Construct unique key
            const callbackKey =
                Date.now() + '' + Math.floor(Math.random() * 100000)
            const successName = `s${callbackKey}`
            const failName = `f${callbackKey}`
            // Use the constructed "success \ failure key" to register a function on the window so that the App can call
            this.registerFn(successName, function(data) {
                if (data.code === -1) {
                    // reject if failed
                    reject(data)
                } else {
                    // resolve if successful
                    resolve(data.data)
                }
            })
            this.registerFn(failName, function(err) {
                reject(err)
            })

            // ******Start calling the method provided by App******
            // Android
            if (isAndroid) {
                // Android needs to string the data
                data = JSON.stringify(data)
                window.JSActionBridge.handlerAction(
                    event,
                    data,
                    successName,
                    failName
                )
            }

            // iOS
            if (isIOS) {
                window.webkit.messageHandlers.JSActionBridge.postMessage({
                    method: 'handlerAction',
                    data: {
                        actionEvent: event,
                        paramsJsonValue: data,
                        successCallback: successName,
                        errorCallback: failName
                    }
                })
            }
        })
    },

    /**
     * Register H5 methods for app calls
     * @param fnName
     * @param fn
     */
    registerFn (fnName, fn) {
        if (typeof fnName !== 'string') {
            throw TypeError('Illegal fnName: Not an string')
        }
        if (typeof fn !== 'function') {
            throw TypeError('ol. fn: Not an function')
        }

        window[fnName] = function (data) {
            if (isIOS) {
                fn(data)
            }
            if (isAndroid) {
                // Android environment needs to be converted
                data = data || '{}'
                fn(JSON.parse(data))
            }
        }
    },

    /**
     * Unregister H5 methods for app calls
     * @param fnName
     */
    unregisterFn (fnName) {
        if (typeof fnName !== 'string') {
            throw TypeError('Illegal fnName: Not an string')
        }

        delete window[fnName]
    },

    /**
     * Jump to app module
     * @param url
     * @param isWaitingResult
     * @returns {*|Promise<Object>}
     */
    gotoNativeModule (url, isWaitingResult = false) {
        if (this.isApp) {
            this.callApp('goto_native_module', {
                url,
                isWaitingResult
            })
        }
    },

    /**
     * Newly opened in APP
     * @param url
     * @param titleBarVisible
     * @param title
     */
    gotoNewWebview (url, titleBarVisible = true, title = '') {
        if (this.isApp) {
            this.gotoNativeModule(
                `xxx://webview?url=${encodeURIComponent(
                    url
                )}&titleBarVisible=${titleBarVisible}&title=${title}`
            )
        } else {
            window.location.href = url
        }
    },

    /**
     * Monitor page activity status
     * @param activated
     * @param deactivated
     */
    watchPageActivity (activated, deactivated) {
        const successCallBackName =
            'command_watch_activity_status_success_callback'
        const failCallBackName = 'command_watch_activity_status_fail_callback'
        const successCallBack = window[successCallBackName]
        const failCallBack = window[successCallBackName]

        this.registerFn(successCallBackName, function (data) {
            if (
                Object.prototype.toString.apply(successCallBack) ===
                '[object Function]'
            ) {
                successCallBack(data)
            }

            try {
                if (typeof data === 'string') {
                    data = JSON.parse(data)
                }

                if (data.data.status === 'visible') {
                    activated(data)
                } else {
                    deactivated(data)
                }
            } catch (e) {
                console.log(successCallBackName, e)
            }
        })

        this.registerFn(failCallBackName, function (data) {
            if (
                Object.prototype.toString.apply(failCallBack) ===
                '[object Function]'
            ) { 
                failCallBack(data) 
            }
        })

        this.callAppNoPromise(
            'command_watch_activity_status',
            {},
            successCallBackName,
            failCallBackName
        )
    },
    /**
     * Call app method to register global events
     * @param event: Event name
     * @param data: json data
     * @param successCallBack: Active setting successful callback
     * @param failCallBack: Active setting failure callback
     * @returns {Promise<object>}
     */
    callAppNoPromise (event, data = {}, successCallBackName, failCallBackName) {
        if (!this.isApp) {
            return
        }
        // Android
        if (isAndroid) {
            data = JSON.stringify(data)
            window.JSActionBridge.handlerAction(
                event,
                data,
                successCallBackName,
                failCallBackName
            )
        }
        // ios
        if (isIOS) {
            window.webkit.messageHandlers.JSActionBridge.postMessage({
                method: 'handlerAction',
                data: {
                    actionEvent: event,
                    paramsJsonValue: data,
                    successCallback: successCallBackName,
                    errorCallback: failCallBackName
                }
            })
        }
    },

    /**
     * Obtain app user information to prevent multiple access
     * @param needUpdate Need to update again
     * @returns {*|Promise<Object>}
     */
    _appUser: {
        _loaded: false, // Whether data has been loaded from the app. This field is not obtained from the app
        userName: '',
        phoneNum: '',
        userId: '',
        userToken: '', // User token. If there is no token, it means that the user is not logged in
    },
    getAppUser (needUpdate) {
        return new Promise(resolve => {
            if (!this.isApp) {
                resolve(this._appUser)
            }
            if (needUpdate || !this._appUser._loaded) {
                this.callApp('get_user_info').then(res => {
                    this._appUser = res
                    this._appUser._loaded = true
                    resolve(this._appUser)
                })
            } else {
                resolve(this._appUser)
            }
        })
    }
}
export default JSBridge

Tags: Javascript

Posted by NTFS on Sun, 17 Apr 2022 08:15:13 +0930