zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

webview原理和JSBridge

2023-09-27 14:27:09 时间

什么是webview?

webview是一个嵌入式的浏览器,我们平常使用频率最高的就是客户端内嵌的webview
特点:运行在webview中的JS代码有能力调用原生的系统API,没有传统浏览器沙箱的限制。

什么是JSBridge?

上面说了,运行在webview中的JS代码有能力调用原生的系统API,那么具体是怎么实现的呢?
核心就是JSBridge。

当我们在native内打开webview网页,native会在全局的window下,为我们注入一个Bridge。这个Bridge里面,会包含我们与native交互的各种方法、比如判断第三方App是否安装、获取网络信息等等功能。

const bridge = window.JSBridge;
console.log(bridge.getNetInfomation());

JSBridge实现原理

Web端和Native可以类比于Client/Server模式,Web端调用原生接口时就如同Client向Server端发送一个请求类似,JSBridge在此充当类似于HTTP协议的角色,实现JSBridge主要是两点:

  • 将Native端原生接口封装成JavaScript接口
  • 将Web端JavaScript接口封装成原生接口

Native->Web

首先来说Native端调用Web端,这个比较简单,JavaScript作为解释性语言,最大的一个特性就是可以随时随地地通过解释器执行一段JS代码,所以可以将拼接的JavaScript代码字符串,传入JS解析器执行就可以,JS解析器在这里就是webView。

iOS

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

Android

mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
            //这里的value即为对应JS方法的返回值
        }
});

Web->Native

目前javascript和客户端(后面统称native)交互的常见方式有两种

  • schema
  • JSBridge

1、URL Schema

这里的URL Scheme既可以单独和原生通信,JSBridge实现的原理也是通过拦截URL Schema

我们可以自定义JSBridge通信的URL Schema,比如:jsbridge://showToast?text=hello
Native加载WebView之后,Web发送的所有请求都会经过WebView组件,所以Native可以重写WebView里的方法,拦截Web发起的请求,我们对请求的格式进行判断:
如果符合我们自定义的URL Schema,对URL进行解析,拿到相关操作、操作,进而调用原生Native的方法
如果不符合我们自定义的URL Schema,我们直接转发,请求真正的服务

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

  • a标签
  • location.href
  • 使用iframe.src
  • 发送ajax请求

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

安卓提供了shouldOverrideUrlLoading方法拦截
IOSUIWebView使用shouldStartLoadWithRequest,
IOSWKWebView则使用decidePolicyForNavigationAction
这种方式从早期就存在,兼容性很好,但是由于是基于URL的方式,长度受到限制而且不太直观,数据格式有限制,而且建立请求有时间耗时。所有有了JSBridge

2、JSBridge

对于Webview中发起的网络请求,Native都有能力去捕获/截取/干预。所以JSBridge的核心就是设计一套url方案,让Native可以识别,从而做出响应,执行对应的操作就完事。
例如,正常的网络请求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
我们可以自定义协议,改成jsbridge://methodName?param1=value1&param2=value2。
Native拦截jsbridge开头的网络请求,做出对应的动作。
最常见的做法就是创建一个隐藏的iframe来实现通信。

App将Native的相关接口注入到JS的window对象中,一般来说这个对象内的方法名与Native相关方法名是相同的,Web端就可以直接在全局window下使用这个全局JS对象,进而调用原生端的方法。

Android(4.2+)提供了addJavascriptInterface注入:

// 注入全局JS对象
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");

class NativeBridge {
  private Context ctx;
  NativeBridge(Context ctx) {
    this.ctx = ctx;
  }

  // 增加JS调用接口
  @JavascriptInterface
  public void showNativeDialog(String text) {
    new AlertDialog.Builder(ctx).setMessage(text).create().show();
  }
}

在Web端直接调用这个方法即可:

window.NativeBridge.showNativeDialog('hello');

带回调的调用

上面已经说到了Native、Web间双向通信的两种方法,但站在一端而言还是一个单向通信的过程 ,比如站在Web的角度:Web调用Native的方法,Native直接相关操作但无法将结果返回给Web,但实际使用中会经常需要将操作的结果返回,也就是JS回调。

所以在对端操作并返回结果,有输入有输出才是完整的调用,那如何实现呢?

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

开源的JSBridge

DSBridge,主要通过注入API的形式,DSBridge for Android、DSBridge for IOS
JsBridge,主要通过拦截URL Schema,JsBridge

以DSBridge-Android为例:

// Web端代码
<body>
  <div>
    <button id="showBtn">获取Native输入,以Web弹窗展现</button>
  </div>
</body>
// 引入SDK
<script src="https://unpkg.com/dsbridge@3.1.3/dist/dsbridge.js"></script>
<script>
  const showBtn = document.querySelector('#showBtn');
  showBtn.addEventListener('click', e => {
    // 注意,这里代码不同:SDK在全局注册了dsBridge,通过call调用Native方法
    dsBridge.call('getNativeEditTextValue', '', value => {
      window.alert('Native输入值' + value);
    })
  });
</script>
// Android代码
// 使用dwebView替换原生webView
dwebView.addJavascriptObject(new JsApi(), null);

class JSApi {
  private Context ctx;
  public JSApi (Context ctx) {
    this.ctx = ctx;
  }

  @JavascriptInterface
  public void getNativeEditTextValue(Object msg, CompletionHandler<String> handler) {
    String value = ((MainActivity)ctx).editText.getText().toString();
    // 通过handler将value传给Web端,实现回调的JSB调用
    handler.completed(value);
  }
}

IOS的WKWebView和UIWebView

WKWebView在IOS8发布时,也随之一起诞生。在这之前IOS端一直使用的是UIWebView。

为什么jsbridge是异步的?

JavaScript 是运行在一个单独的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由于这些Context 与原生运行环境的天然隔离,我们可以将这种情况与 RPC(Remote Procedure Call,远程过程调用)通信进行类比,将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用。如此一来我们可以按照通常的 RPC 方式来进行设计和实现。

在 JSBridge 的设计中,可以把前端看做 RPC 的客户端,把 Native 端看做 RPC 的服务器端,从而 JSBridge 要实现的主要逻辑就出现了:通信调用(Native 与 JS 通信) 和 句柄解析调用。(如果你是个前端,而且并不熟悉 RPC 的话,你也可以把这个流程类比成 JSONP 的流程)

Native 和 Javascript 通信的原理是 JSBridge 实现的核心,实现方式可以各种各样,但是万变不离其宗。这里,笔者推荐的实现方式如下:

JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。

Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。

也可以将URL SCHEME的Restful形式的接口参数格式同注入API方式相结合,做到接口调用方式上的兼容(只改动底层实现)

参考资料

https://zhuanlan.zhihu.com/p/58691238
https://www.cnblogs.com/songyao666/p/14540073.html
https://github.com/marcuswestin/WebViewJavascriptBridge
https://github.com/lzyzsd/JsBridge
https://www.jianshu.com/p/be491bfbca0d
https://blog.csdn.net/qq_30868289/article/details/104233030