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

标签: jsbridge web app | 发表时间:2023-10-09 16:21 | 作者:choreau
出处:https://juejin.cn/frontend

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:

  webView.loadUrl("javascript:" + javaScriptString);

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

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

iOS

UIWebView 使用 stringByEvaluatingJavaScriptFromString

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

WKWebView 使用 evaluateJavaScript

  [webView evaluateJavaScript:@"执行的JS代码" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
  
}];

  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,我们直接转发,请求真正的服务

image.png

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:

  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:

  - (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

  JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

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

前端调用方式:

  window.postBridgeMessage(message);

iOS的 WKWebView 提供了 WKScriptMessageHandler

  @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 逻辑
    }
}

前端调用方式:

  window.webkit.messageHandlers.nativeBridge.postMessage(message);

Android 提供了 addJavascriptInterface

  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 逻辑
         }
     }
}

前端调用方式:

  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 可以设计如下:

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

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

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

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

  (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。其代码如下:

  (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); // 存储回调
        }
    };
})();

参考

  • https://juejin.cn/post/6936814903021797389
  • https://juejin.cn/post/6844903585268891662
  • https://juejin.cn/post/6844904070881214471

相关 [jsbridge web app] 推荐:

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

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

创建Chrome Web app

- Hobbes - Marshal&#39;s Blog
编写了一个Chrome下使用的Web app. 点击这个应用,就可访问我的博客. 其实有它特殊的好处,直接访问网站,程序无法自动获取HTML5的权限,比如存储限制. 安装应用,相当于安装了一个配置文件,浏览器将打开必要的权限. 如果你也使用Chrome,可以通过这里下载安装. 开发这样的应用并不难,过程类似以前开发Chrome扩展(编写最简单的chrome扩展).

iOS Web App初步

- - 新浪UED
iOS Web App开发,配合HTML5,是目前比较热门的话题. 今天,先抛开HTML5,我们来尝试在PhoneGap框架上进行简单的开发. PhoneGap是一个使用HTML,CSS和JavaScript的,创建移动跨平台移动应用程序的快速开发平台. 它使开发者能够利用iPhone,Android,Palm,Symbian,WP7,Bada和Blackberry等智能手机的核心功能——包括地理定位,加速器,联系人,声音和振动等,此外PhoneGap拥有丰富的插件,可以以此扩展无限的功能.

Web App和Native App 谁将是未来

- - 互联网旁观者
未来是Web App的天下,还是Native App的天下. 作为设计师,我们是应该努力把客户端的体验提升到最优,还是在网页应用层面上做更多的设计. 那么,我们首先应该立体的认识一下Web App和Native App. Web 无需安装,对设备碎片化的适应能力优于App,它只需要通过XHTML、CSS和JavaScript就可以在任意移动浏览器中执行.

Web app设计浅谈

- Jason - 网易用户体验设计中心博客
HTML5技术的强势发展,为互联网带来的最大改变就是: web从“已死”的预言中回过头来给Native app一记沉重的回马枪,web app成为举世瞩目的明星开始走在各大公司研发的时刻表中. Google 、微软、苹果三大巨头紧锣密鼓地在web app的研发产品领域圈地设岗,并试图建立以自己为中心的”云“服务平台,企图在web app时代到来的时候充当霸主.

Web将死,App当立?

- - 创意科技 - 果壳网
作为一位著名的软件开发者和博客世界的先驱者,戴夫·温纳(Dave Winer)于1997年创办了Scripting News(最早的博客网站之一). 由于他总是与微软、谷歌等大公司隔空对骂,所以在博客世界颇为出名. 最近他在博客中谈了自己对于web将死的看法. “我总是听人说道,web将死,app才是未来.

App VS Web,谁主沉浮?

- Pancho Tsui - 月光博客
  琢磨一下:移动互联网的未来,到底是web后来居上,还是app居中主流呢.   到目前为止,貌似答案都是app,甚至《连线》杂志都发表了“web已死”的宣言. 没错,app确实有很大优势,尤其在利用硬件性能上,比web要强大很多,苹果主推app而不是web,就因为app另起炉灶,专为移动设备开发,速度、流畅性、交互习惯、页面布局都得到最用心的考量,也就是最棒的用户体验.

论Web App、Hybrid App、Native App设计差异

- - 百度MUX
目前主流应用程序大体分为三类:Web App、Hybrid App、 Native App. 一、Web App、Hybrid App、Native App 纵向对比. 首先,我们来看看什么是 Web App、Hybrid App、 Native App. Web App 指采用Html5语言写出的App,不需要下载安装.