JSBridge 原理与封装

移动应用功能越来越丰富,部分功能需要以 H5 方式来实现

这就需要 H5 与 Native 进行交互,我们使用 JSBridge 来进行二者通信

JSBridge 定义 Native 和 H5 的通信方式

Native 通过协定的桥对象调用 H5

H5 通过协定的桥对象调用 Native


一、交互方式

1、伪协议 url scheme(适用于所有设备)

伪协议 与 url 类似,H5 触发伪协议,系统按优先级判断:

  1. 是否是系统应用,是则打开系统应用
  2. 是否有 app 注册当前伪协议,有则打开该 app
  3. native 触发 url 事件,捕获该伪协议,解析伪协议,调用方法

具体过程:

  1. H5 调用 Native

    触发伪协议,例如:

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

  2. Native 调用 H5

    H5 在 window 注册方法供 native 调用(同 API 交互方式中 Native 调用 H5 一样)


2、通过 API 交互

具体过程:

  1. H5 调用 Native

    1.1、 H5 调用 Android: native 通过 addJavascriptInterface 注册,之后供 H5 调用

    1.2、 H5 调用 iOS:native 通过 javascriptCore(需 iOS7 以上) 注册,之后供 H5 调用

  2. Native 调用 H5

    2.1、 Android 调用 H5:H5 在 window 上注册方法,native 通过 loadUrl 调用 H5,4.4及以上版本可以通过 evaluateJavascript 调用

    2.2、 iOS 调用 H5:H5 在 window 上注册方法,native 通过 stringByEvaluatingJavascriptFromString 调用 H5


3、区别

区别在于 H5 如何调用 Native 以及 Native 响应 H5 当前调用指定方法的方式 不一样

3.1 H5 如何调用 Native:

伪协议:

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 响应 H5 就不列出了


3.3 API 交互方式的好处

  • 在 H5 中写起来更简单,不用创建 URL 的方式
  • H5 传递参数更方便,使用拦截 URL 的方式,参数需要拼接到 url 后面,如果含有特殊字符,则需要转义,否则解析参数会出错,如 & = ?
  • 例如
    window.location.href = 'xxx://event=a&data=http://www.baidu.com/xxx.html?p=2&successName=c&failName=d'

    这里解析参数得到的不是想要的值,需要对 http://www.baidu.com/xxx.html?p=2 进行转义

二、方案

目前公司内部使用 API 方式交互(Android 需要 4.2 以上,iOS 需要 7 以上)

1、约定名称

原生通过 JSActionBridge 注册 JSBridge 对象
H5 通过 JSActionBridge 获取 JSBridge 对象

2、原生 JSBridge 对象注册

Android:

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

iOS:

// 通过 javascriptCore 注册
self.context = [[JSContext alloc] int];
self.context[@"JSActionBridge"] = self.bridgeobj;

3、调用

3.1 Native 方法定义

Android:
/*
* @params {String} actionEvent: 需要调用的原生事件名、方法名
* @params {String|Object|Array} paramsJsonValue: 传给原生的参数,最好约定为 json 格式
* @params {String} successCallBack:调用原生方法成功后,需要执行的的回调函数名,该函数定义在 window 上
* @params {String} errorCallback:调用原生方法失败后,需要执行的的回调函数名,该函数定义在 window 上
*/
void handlerAction(String actionEvent, String paramsJsonValue, String successCallBack, String errorCallback)

3.2 H5 调用 Native

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

3.3 Native 调用 H5

Android 调用 H5

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

iOS 调用 H5

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

三、数据约定

原生方法定义

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

1、成功或失败回调函数都返回 json 数据

数据格式:

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

2、约定 code === 0 时表示调用成功,否则失败

3、actionEvent 归类

  • 跳转原生页面

    首页、登录注册页等
  • 获取原生数据

    获取用户信息、获取设备信息等
  • 调用原生功能

    跳转新页面、唤醒密码输入框、查看网络状态、设置标题等
  • 其他

四、JSBridge 封装

1、协定:

  • Android 传送字符串
  • iOS 传送 json

2、successCallBack/errorCallback 回调数据格式:

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

3、JSBridge 封装

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

    // APP离线环境
    isOfflineApp: isApp && location.protocol === 'file:',

    // H5 调用 App
    /**
     * 调用 App 方法
     * @param event:事件名称
     * @param data:json 数据
     * @param successCallBack: 成功回调
     * @param failCallBack: 失败回调
     * @returns {Promise<object>}
     */
    callApp (event, data = {}) {
        return new Promise((resolve, reject) => {
            // 不在 App 内调用,提示不在 App 内
            if (!this.isApp) {
                resolve('not in app')
                return
            }

            // 构造唯一 key
            const callbackKey =
                Date.now() + '' + Math.floor(Math.random() * 100000)
            const successName = `s${callbackKey}`
            const failName = `f${callbackKey}`
            // 使用构造好的 `成功\失败的 key` 在 window 上注册一个函数,使得 App 可以调用
            this.registerFn(successName, function(data) {
                if (data.code === -1) {
                    // 失败则 reject
                    reject(data)
                } else {
                    // 成功则 resolve
                    resolve(data.data)
                }
            })
            this.registerFn(failName, function(err) {
                reject(err)
            })

            // ******开始调用 App 提供的方法******
            // 安卓
            if (isAndroid) {
                // 安卓需要将数据字符串化
                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
                    }
                })
            }
        })
    },

    /**
     * 注册 H5 供 app 调用的方法
     * @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) {
                // 安卓环境需要做转换
                data = data || '{}'
                fn(JSON.parse(data))
            }
        }
    },

    /**
     * 注销 H5 供 app 调用的方法
     * @param fnName
     */
    unregisterFn (fnName) {
        if (typeof fnName !== 'string') {
            throw TypeError('Illegal fnName: Not an string')
        }

        delete window[fnName]
    },

    /**
     * 跳转至app模块
     * @param url
     * @param isWaitingResult
     * @returns {*|Promise<Object>}
     */
    gotoNativeModule (url, isWaitingResult = false) {
        if (this.isApp) {
            this.callApp('goto_native_module', {
                url,
                isWaitingResult
            })
        }
    },

    /**
     * 在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
        }
    },

    /**
     * 监控页面活动状态
     * @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
        )
    },
    /**
     * 调用app方法 注册全局事件
     * @param event:事件名称
     * @param data:json数据
     * @param successCallBack: 主动设置成功回调
     * @param failCallBack: 主动设置失败回调
     * @returns {Promise<object>}
     */
    callAppNoPromise (event, data = {}, successCallBackName, failCallBackName) {
        if (!this.isApp) {
            return
        }
        // 安卓
        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
                }
            })
        }
    },

    /**
     * 获取app用户信息,防止多次获取
     * @param needUpdate 是否需要再次更新
     * @returns {*|Promise<Object>}
     */
    _appUser: {
        _loaded: false, // 是否从APP加载过数据,该字段不是从app中获取
        userName: '',
        phoneNum: '',
        userId: '',
        userToken: '', // 用户token,没有则代表未登录
    },
    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
posted @ 2021-02-21 17:08  青S衫%  阅读(906)  评论(0编辑  收藏  举报