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 通信原理与实践

相关推荐
AlienZHOU5 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc8 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师9 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen10 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒10 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow11 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81811 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007811 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明11 小时前
第 6 章 组件进阶
前端·vue.js
江华森11 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端