jsBridge 以及 Web 和 APP 交互通信方式

jsBridge 到底是什么?

jsBridge 是一种技术,主要用于解决 Web 前端和原生应用间的通信问题。这一技术在混合开发(Hybrid App Development)和一些原生应用内嵌 H5 页面的场景中被广泛应用。通过 jsBridge,开发者可以直接在 JavaScript 中调用原生代码,如获取设备信息、调用系统功能等,极大的提高了开发效率。

首先需要了解 WebView

WebView 是一个浏览器控件或者组件,它能够帮助开发者将网页或者 HTML 内容嵌入到原生应用中。

WebView 控件除了能加载指定的 URL 外,还可以对 URL 请求、JavaScript 的对话框、加载进度、页面交互进行强大的处理,之后会提到拦截请求、执行 js 脚本都依赖于此。

Android 中的 WebView:

Android 中的 WebView 是一个继承自 View 的控件,它可以加载并显示网页,同时也提供了一些方法供你与 JavaScript 交互。你可以使用 loadUrl(String url) 方法加载一个网页,使用 evaluateJavascript(String script, ValueCallback<String> resultCallback) 方法执行 JavaScript 代码。你还可以通过 addJavascriptInterface(Object object, String name) 方法向 JavaScript 环境中添加一个 Java 对象,使得 JavaScript 可以调用该对象的方法。

iOS 中的 WebView:

iOS 中提供了 UIWebViewWKWebView 两个用于展示网页的控件。UIWebView 在 iOS 2.0 就被引入,但自 iOS 8.0 起,Apple 推荐使用 WKWebView 替代 UIWebViewWKWebView 提供了 load(URLRequest)loadHTMLString(String baseURL: URL?) 方法用于加载网页,通过 evaluateJavaScript(String completionHandler: ((Any?, Error?) -> Void)?) 方法执行 JavaScript 代码。同时,iOS 中的 WKWebView 通过 WKScriptMessageHandler 协议和 WKUserContentController 类来实现 Native 与 JavaScript 的交互。

了解 WebView 很重要,它才是连接原生和 Web 的桥梁。因为 Web 前端大多对原生开发不了解,如果一开始就去了解所谓的 jsBridge,反而会迷惑。

Web 和 Native 的交互

Web 和 Native 的交互分为 Native 调用 js 和 js 调用 Native。

Native -> js

原生调 js 的方式比较简单。JavaScript 作为解释性语言,最大的一个特性就是可以随时随地地通过解释器执行一段 js 代码,所以可以将拼接的 JavaScript 代码字符串,传入 js 解析器执行就可以,js 解析器在这里就是 WebView 组件。

所以 WebView 执行拼接的 JavaScript 字符串,从外部调用 JavaScript 方法,JavaScript 的方法必须在全局的 window 上。

Android

Android 4.4 之前只能用 loadUrl 来实现,效率低,无法获得返回结果,且调用的时候会刷新 WebView:

java 复制代码
webView.loadUrl("javascript:" + javaScriptString);

Android 4.4 之后提供了 evaluateJavascript 来执行 js 代码,效率高,获取返回值方便,调用时候不刷新 WebView:

java 复制代码
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value){
        xxx
    }
});

iOS

UIWebView 使用 stringByEvaluatingJavaScriptFromString

c 复制代码
NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];

WKWebView 使用 evaluateJavaScript

c 复制代码
[webView evaluateJavaScript:@"执行的JS代码" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
  
}];
swift 复制代码
func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)
// javaScriptString 需要调用的 JS 代码
// completionHandler 执行后的回调

js -> Native

简单的说,主要是两类方法:拦截 URL,注入 API。

拦截 URL Schema

URL Schema 是类 URL 的一种请求格式,格式如下:

<protocol>://<host>/<path>?<qeury>#fragment

我们可以自定义 jsBridge 通信的 URL Schema,比如:jsbridge://showToast?text=hello

Native 加载 WebView 之后,Web 发送的所有请求都会经过 WebView 组件,所以 Native 可以重写 WebView里的方法,从来拦截 Web 发起的请求,我们对请求的格式进行判断:

  • 如果符合我们自定义的 URL Schema,对 URL 进行解析,拿到相关操作,进而调用原生 Native 的方法
  • 如果不符合我们自定义的 URL Schema,我们直接转发,请求真正的服务

Web 发送 URL 请求的方法有这么几种:

  1. a 标签
  2. location.href
  3. 使用 iframe.src
  4. 发送 ajax 请求

这些方法,a 标签需要用户操作,location.href 可能会引起页面的跳转丢失调用,发送 ajax 请求Android 没有相应的拦截方法,所以使用 iframe.src 是经常会使用的方案:

  • 安卓提供了 shouldOverrideUrlLoading 方法拦截
  • UIWebView 使用 shouldStartLoadWithRequest,WKWebView 则使用decidePolicyForNavigationAction

Android:

java 复制代码
public class CustomWebViewClient extends WebViewClient {
  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    ......
    // 场景一: 拦截请求、接收 scheme
    if (url.equals("xxx")) {

      // handle
      ...
      // callback
      view.loadUrl("javascript:setAllContent(" + json + ");")
      return true;
    }
    return super.shouldOverrideUrlLoading(url);
  }
}

iOS 的 WKWebview:

swift 复制代码
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    if ([navigationAction.request.URL.absoluteString hasPrefix:@"xxx"]) {
        [[UIApplication sharedApplication] openURL:navigationAction.request.URL];
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

这种方式有一定的缺陷:

  • 使用 iframe.src 发送 URL SCHEME 会有 URL 长度的隐患。
  • 创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。

注入 API

注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。

iOS 的 UIWebView 提供了 JavaSciptCore

swift 复制代码
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
    // Native 逻辑
};

前端调用方式:

js 复制代码
window.postBridgeMessage(message);

iOS的 WKWebView 提供了 WKScriptMessageHandler

swift 复制代码
@interface WKWebVIewVC ()<WKScriptMessageHandler>

@implementation WKWebVIewVC

- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = [[WKUserContentController alloc] init];
    WKUserContentController *userCC = configuration.userContentController;
    // 注入对象,前端调用其方法时,Native 可以捕获到
    [userCC addScriptMessageHandler:self name:@"nativeBridge"];

    WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];

    // TODO 显示 WebView
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"nativeBridge"]) {
        NSLog(@"前端传递的数据 %@: ",message.body);
        // Native 逻辑
    }
}

前端调用方式:

js 复制代码
window.webkit.messageHandlers.nativeBridge.postMessage(message);

Android 提供了 addJavascriptInterface

java 复制代码
public class JavaScriptInterface DemoActivity extends Activity {
private WebView Wv;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Wv = (WebView)findViewById(R.id.webView);     
        final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this);    	 

        Wv.getSettings().setJavaScriptEnabled(true);
        Wv.addJavascriptInterface(myJavaScriptInterface, "nativeBridge");

        // TODO 显示 WebView

    }

    public class JavaScriptInterface {
         Context mContext;

         JavaScriptInterface(Context c) {
             mContext = c;
         }

         public void postMessage(String webMessage){	    	
             // Native 逻辑
         }
     }
}

前端调用方式:

js 复制代码
window.nativeBridge.postMessage(message);

在 4.2 之前,Android 注入 JavaScript 对象的接口是 addJavascriptInterface,但是这个接口有漏洞,可以被不法分子利用,危害用户的安全,因此在 4.2 中引入新的接口 @JavascriptInterface(上面代码中使用的)来替代这个接口,解决安全问题。所以 Android 注入对对象的方式是 有兼容性问题的。(4.2 之前很多方案都采用拦截 prompt 的方式来实现,因为篇幅有限,这里就不展开了。)

jsBridge 的实现(带回调的交互)

Native、Web 间可以交互,但站在一端而言还是一个单向通信的过程,比如站在 Web 的角度:Web 调用 Native 的方法,Native 直接进行相关操作但无法将结果返回给 Web,但实际使用中会经常需要将操作的结果返回,也就是 js 回调。

jsBridge 的接口主要功能有两个:调用 Native(给 Native 发消息)被 Native 调用(接收 Native 消息) 。因此,jsBridge 可以设计如下:

js 复制代码
window.JSBridge = {
    // 调用 Native
    invoke: function(msg) {
        // 判断环境,获取不同的 nativeBridge
        nativeBridge.postMessage(msg);
    },
    receiveMessage: function(msg) {
        // 处理 msg
    }
};

那么有回调的交互如何实现呢?

其实基于之前的单向通信就可以实现,我们在一端调用的时候在参数中加一个 callbackId 标记对应的回调,对端接收到调用请求后,进行实际操作,如果带有 callbackId,对端再进行一次调用,将结果、callbackId 回传回来,这端根据 callbackId 匹配相应的回调,将结果传入执行就可以了。

可以看到实际上还是通过两次单项通信实现的。

js 复制代码
(function () {
    var id = 0,
        callbacks = {};

    window.JSBridge = {
        // 调用 Native
        invoke: function(bridgeName, callback, data) {
            // 判断环境,获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            window.nativeBridge.postMessage({
                bridgeName: bridgeName,
                data: data || {},
                callbackId: thisId // 传到 Native 端
            });
        },
        receiveMessage: function(msg) {
            var bridgeName = msg.bridgeName,
                data = msg.data || {},
                callbackId = msg.callbackId; // Native 将 callbackId 原封不动传回
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } elseif (bridgeName) {

            }
        }
    };
})();

最后用同样的方式加上 Native 调用的回调逻辑,同时对代码进行一些优化,就大概实现了一个功能比较完整的 jsBridge。其代码如下:

js 复制代码
(function () {
    var id = 0,
        callbacks = {},
        registerFuncs = {};

    window.JSBridge = {
        // 调用 Native
        invoke: function(bridgeName, callback, data) {
            // 判断环境,获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            nativeBridge.postMessage({
                bridgeName: bridgeName,
                data: data || {},
                callbackId: thisId // 传到 Native 端
            });
        },
        receiveMessage: function(msg) {
            var bridgeName = msg.bridgeName,
                data = msg.data || {},
                callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
                responstId = msg.responstId;
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } elseif (bridgeName) {
                if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
                    var ret = {},
                        flag = false;
                    registerFuncs[bridgeName].forEach(function(callback) => {
                        callback(data, function(r) {
                            flag = true;
                            ret = Object.assign(ret, r);
                        });
                    });
                    if (flag) {
                        nativeBridge.postMessage({ // 回调 Native
                            responstId: responstId,
                            ret: ret
                        });
                    }
                }
            }
        },
        register: function(bridgeName, callback) {
            if (!registerFuncs[bridgeName])  {
                registerFuncs[bridgeName] = [];
            }
            registerFuncs[bridgeName].push(callback); // 存储回调
        }
    };
})();

参考

相关推荐
ZJ_.10 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营14 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood40 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端42 分钟前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248942 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_748235612 小时前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O4 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js