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

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
浪浪山小白兔6 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me7 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者7 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
qq_392794487 小时前
前端缓存策略:强缓存与协商缓存深度剖析
前端·缓存