JS Bridge深入解析

顾名思义,JS Bridge 的意思就是桥,这是一个连接 JS 和 Native 的桥接,也是 Hybrid App 里面的核心。一般分为 JS 调用 Native 和 Native 主动调用 JS 两种形式。

URL Scheme介绍

URL Scheme 是一种特殊的 URL,一般用于在 Web 端唤醒 App,甚至跳转到 App 的某个页面,比如在某个手机网站上付款的时候,可以直接拉起支付宝支付页面。

在浏览器里面通过修改 window.location.hrefweixin:// ,系统就会提示你是否要打开微信。设置为 bixin:// 就会帮你唤起手机的比心。

常用URL Scheme 汇总

一个 URI 的组成结构如下:

通常情况下,App 安装后会在手机系统上注册一个 Scheme,比如 bixin:// 这种,所以我们在手机浏览器里面访问这个 Scheme 地址,系统就会唤起我们的 App。

JS 调用 Native

JS 调用 Native 通信大致有三种方法:

  1. 拦截 Scheme
  2. 弹窗拦截
  3. 注入 JS 上下文

拦截 Scheme

就像JS 和 Java 之间是通过 http/https 接口来获取数据一样,JS也可以这样和客服端进行通信。

客户端是可以拦截页面发起的请求的,如果我们请求一个不存在的地址,上面带了一些参数,通过参数告诉客户端我们需要调用的功能。

比如我要调用扫码功能:

js 复制代码
axios.get('http://bixin?func=scan&callbackId=yyyy')

客户端可以拦截这个请求,去解析参数上面的 func 来判断当前需要调起哪个功能。客户端调起扫码功能之后,会获取 WebView 上面的 callbacks 对象,根据 callbackId 回调它。

基于上面的例子,我们可以把域名和路径当做通信标识,参数里面的 func 当做指令,callbackid 当做回调函数,其他参数当做数据传递。对于不满足条件的 http 请求不应该拦截。

JS侧

JS还有很多种方法可以发起请求:

  1. 使用 a 标签跳转
js 复制代码
<a href="bixin://">点击我打开比心</a>
  1. 重定向
js 复制代码
location.href = "bixin://"
  1. iframe 跳转
js 复制代码
const iframe = document.createElement("iframe");
iframe.src = "bixin://"
iframe.style.display = "none"
document.body.appendChild(iframe)

目前使用最广泛的是 iframe 跳转

以Android举例

在 Android 侧可以用 shouldOverrideUrlLoading 来拦截 url 请求。

java 复制代码
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (url.startsWith("bixin")) {
        // 拿到调用路径后解析调用的指令和参数,根据这些去调用 Native 方法
        return true;
    }
}

拦截 URL Scheme 解析参数的形式,会存在以下几个问题。

  1. 连续续调用 location.href 会出现消息丢失,因为 WebView 限制了连续跳转,会过滤掉后续的请求。
  2. URL 会有长度限制,一旦过长就会出现信息丢失

弹窗拦截

通过在JS中调用以下方法,在Android和IOS中可进行对应的拦截

以Android举例

这种方式是利用弹窗会触发 WebView 相应事件来拦截的。一般是在 setWebChromeClient 里面的 onJsAlertonJsConfirmonJsPrompt 方法拦截并解析他们传来的消息。

java 复制代码
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    // js执行的是prompt('bridge://yupaopao.com?message=' + encodeMessageJson, "")
    Uri uri = Uri.parse(message);
    // 如果url的协议 = 预先约定的 bridge 协议
    if ( uri.getScheme().equals("bridge")) {
        // 然后再解析uri的其他参数,例如action来决定执行具体的方法
        return true;
    }
    return super.onJsPrompt(view, url, message, defaultValue, result);
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    return super.onJsAlert(view, url, message, result);
}

@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
    return super.onJsConfirm(view, url, message, result);
}

注入上下文

这种方式不依赖拦截,主要是通过 WebView 向 JS 的上下文注入对象和方法,可以让 JS 直接调用原生。

以Android举例

安卓4.2之前注入 JS 一般是使用 addJavascriptInterface

java 复制代码
public void addJavascriptInterface() {
    webView.addJavascriptInterface(new PickerJSBridge(), "PickerBridge");
}
private class PickerJSBridge {
    public void _pick(...) {
    }
}

在 JS 里面调用:

js 复制代码
window.PickerBridge._pick(...)

在 Android4.2 之后提供了 @JavascriptInterface 注解,暴露给 JS 的方法必须要带上这个。 所以前面的 _pick 方法需要带上这个注解。

java 复制代码
private class PickerJSBridge {
    @JavascriptInterface
    public void _pick(...) {
    }
}

Native 调用 JS


Native 调用 JS 一般就是直接 JS 代码字符串,有些类似我们调用 JS 中的 eval 去执行一串代码。一般有 loadUrlevaluateJavascript 等几种方法,这里逐一介绍。 但是不管哪种方式,客户端都只能拿到挂载到 window 对象上面的属性和方法。

以Android举例

在 Android 里面需要区分版本,在安卓4.4之前的版本支持 loadUrl,使用方式类似我们在 a 标签的 href 里面写 JS 脚本一样,都是 javascript:xxx 的形式。这种方式无法直接获取返回值。

java 复制代码
webView.loadUrl("javascript:foo()")

在安卓4.4以上的版本一般使用 evaluateJavascript 这个 API 来调用。这里需要判断一下版本。

java 复制代码
if (Build.VERSION.SDK_INT > 19) //see what wrapper we have
{
    webView.evaluateJavascript("javascript:foo()", null);
} else {
    webView.loadUrl("javascript:foo()");
}

JS Bridge 设计


我们bixin的 Bridge 通信主要是根据prompt弹窗的拦截的来实现的,下面的是几个基础 API:

  1. call(action, params, callback, needShell):这个是调用 Native 功能的方法,传模块名、参数、回调函数给 Native,needShell是返回的结果中不会带 code/result/msg 的壳。
  2. callbackToWeb:主要是供Native执行完call的操作之后回调使用
  3. bindEvent(type, callback):willAppearwillDisappear等事件触发的events
  4. unbindEvent(type, callback):解绑willAppearwillDisappear等事件触发的events
  5. onEvent(type, args):执行willAppearwillDisappear等事件的 events

call方法的实现

call 这个方法,它是提供 JS 调用 Native 功能的方法,MAP中是预设好的方法,responseCallbacks 方法中存的是回调的方法

js 复制代码
var MAP = {
    closeWebview: "page_close",
    triggerWebviewError: "page_triggerError",
    closePullEffect: "ui_disableBounces",
    setNavbarType: "ui_setNavbarType",
    setWebviewDisplayType: "ui_setDisplayType",
    enablePullToRefresh: "ui_enablePullToRefresh",
    closeLoading: "ui_refreshComplete",
    setShareOptions: "share_setOptions",
    openSharePicker: "share_openSharePicker",
    getDeviceInfo: "device_getDeviceInfo",
    disableSwipeBack: "ui_disableSwipeBack",
    dispatchEvent: "app_dispatchEvent",
    saveImage: "image_saveToAlbum",
    openAudioMemos: "audio_simpleRecord",
    openAudioMemosInHeadless: "audio_recordService",
    checkAudioMode: "audio_checkMode",
    getAuthInfo: "audio_checkRecordGranted",
    issueAuthPicker: "audio_acquireRecordAuth",
    isSpecifiedAppInstalled: "app_isAppInstalled",
    openSpecifiedApp: "app_openApp",
    issueCollectRequest: "log_trackEvent"
};

var uniqueId = 1;
var responseCallbacks = {};
js 复制代码
function generateCallbackId(message, callback, needShell) {
    if (callback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();

        responseCallbacks[callbackId] = {
            callback: callback,
            needShell: needShell
        };
        message.callbackId = callbackId;
    }
}

// 如果不带 needShell 参数,则返回的结果中不会带 code/result/msg 的壳
function call(action, params, callback, needShell) {
    var message = {
        action: action,
        params: params,
    };
    if (MAP[action]) {
        message.action = MAP[action];
    }
    generateCallbackId(message, callback, needShell);
    var encodeMessageJson = encodeURIComponent(JSON.stringify(message));
    prompt('bridge://yupaopao.com?message=' + encodeMessageJson, "");
}

callbackToWeb

callbackToWeb 是供native调用的,及native在执行完对应的 call 方法之后调用 callbackToWeb 进行执行回调

js 复制代码
function callbackToWeb() {
    var jsonData = '';
    for (var i = 0; i < arguments.length; i++) {
        jsonData += arguments[i];
    }
    jsonData = JSON.parse(jsonData);

    if (jsonData.callbackId) {
        var responseData = jsonData.responseData;
        var savedData = responseCallbacks[jsonData.callbackId];
        if (!savedData || !responseData) {
            return;
        }
        var callback = savedData.callback;
        var needShell = savedData.needShell;
        if (!needShell && responseData.hasOwnProperty('code') && responseData.hasOwnProperty('result')) {
            callback(responseData.result);
        } else {
            callback(responseData);
        }
        delete responseCallbacks[jsonData.callbackId];
    }
}

bindEvent、unbindEvent、onEvent

event事件目前有 willAppearwillDisappear 及页面出现、页面消失两个。event[type] 需要和客户端约定好,先使用 bindEvent 将具体的事件注册到 events ,等到Native触发了 onEvent 然后再执行具体 events[type]

js 复制代码
var events = {};

function bindEvent(type, callback) {
    if (callback) {
        if (!events.hasOwnProperty(type)) events[type] = [];
        events[type].push({ type: type, callback: callback });
    }
}

function unbindEvent(type, callback) {
    if (!events.hasOwnProperty(type)) return;

    if (!callback) {
        delete events[type];
    } else {
        events[type] = events[type].filter(function (it) {
            return it.callback !== callback;
        })
    }
}
function onEvent (type, args) {
    // 兼容 2.0 以前版本的 bridge
    if (type === 'pullToRefresh' && window.onYppRefresh) {
        window.onYppRefresh(args)
    }
    var targets = events[type] || [];
    targets.forEach(function (it) {
        it.callback && it.callback({ type: type, data: args });
    })
},
    

最后就是将这些 _YPP_JS_BRIDGE_ 挂载到 window

js 复制代码
 window._YPP_JS_BRIDGE_ = {
    bindEvent: bindEvent,
    unbindEvent: unbindEvent,
    call: call,

    /** native 调用此方法回调前端 */
    callbackToWeb: callbackToWeb,
    /** native 调用此方法来触发前端事件 */
    onEvent: function (type, args) {
        // 兼容 2.0 以前版本的 bridge
        if (type === 'pullToRefresh' && window.onYppRefresh) {
            window.onYppRefresh(args)
        }
        var targets = events[type] || [];
        targets.forEach(function (it) {
            it.callback && it.callback({ type: type, data: args });
        })
    },
};

call方法通信大致流程图如下:

event事件与call几乎一致,只不过少了js通过prompt和native进行通信的过程

文章参考:JS Bridge 通信原理与实践

相关推荐
摇光9310 分钟前
promise
前端·面试·promise
麻花201333 分钟前
WPF学习之路,控件的只读、是否可以、是否可见属性控制
服务器·前端·学习
.54833 分钟前
提取双栏pdf的文字时 输出文件顺序混乱
前端·pdf
jyl_sh41 分钟前
WebKit(适用2024年11月份版本)
前端·浏览器·客户端·webkit
狼叔1 小时前
前端潮流KK:科技达人与多面手,如何找到自己的乐趣?-浪说回顾
前端
zhanghaisong_20151 小时前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
Eric_见嘉1 小时前
真的能无限试(白)用(嫖)cursor 吗?
前端·visual studio code
DK七七2 小时前
多端校园圈子论坛小程序,多个学校同时代理,校园小程序分展示后台管理源码
开发语言·前端·微信小程序·小程序·php
老赵的博客2 小时前
QSS 设置bug
前端·bug·音视频
Chikaoya2 小时前
项目中用户数据获取遇到bug
前端·typescript·vue·bug