URL Scheme拦截与JSBridge注入

2,286 阅读12分钟

上一篇文章介绍了一文读懂Hybird和Webview,这一篇介绍Native与web之间跨端通信的两种方式。

  1. URL Scheme拦截
  2. JSBridge注入

web端调用Native端

URL Scheme拦截

URI和URL是什么?

URL其实就是我们平常所用到的那个链接url,但是我们没有更深的去了解他,先来看看百度百科关于URI和URL的介绍

在电脑术语中,统一资源标识符(Uniform Resource Identifier,URI)是一个用于标识某一互联网资源名称的字符串。 该种标识允许用户对任何(包括本地和[互联网])的资源通过特定的协议进行交互操作。URI由包括确定语法和相关协议的方案所定义。

Web上可用的每种资源 -[HTML]文档、图像、视频片段、程序等 - 由一个通用资源标识符(Uniform Resource Identifier, 简称"URI")进行定位。

统一资源定位符(Uniform Resource Locator,URL),统一资源名称(Uniform Resource Name,URN)是URI的子集。Web上地址的基本形式是URI,它有两种形式:一种是URL,这是目前URI的最普遍形式。另一种就是URN,这是URL的一种更新形式,URN不依赖于位置,并且有可能减少失效连接的个数。但是其流行还需假以时日,因为它需要更精密软件的支持。

可以看出URL是URI的一个子集,所以你可以看到有地方(IOS)叫URL Scheme,有的地方(Windows)叫URI Scheme,事实上二者都表示资源定位符。

他的格式一般如下:

image.png

看着是不非常熟悉?是不是有一种看了半天原来就是个网址的感觉?其实就是想通过这种方式告诉大家,这东西没什么难的就是个网址(我第一次遇到时候还以为是个什么高大上的新东西呢)。

Scheme是什么?

通过上图我们可以看到,Scheme其实就是URL的头部协议部分

根据使用场景存取资源双方之间的约定不同,我们可以自定义的对URL中头部的Scheme及其后面的参数进行自定义修改,例如:

image.png

下面是几种常见的URL及Scheme:

  • www.baidu.com
  • file:///c:/WINDOWS/clock.exe
  • D:\学习资料\前端\考研\xxx.avi
  • git://github.com/user/
  • magnet:xt.1=urn:sha1:YNCKJUQCZO5C&xt.2=urn:sha1:TXGCOUQC7
  • ed2k://|file|eMule0.49c.zip|2868871|0F88EEFA9D8AD3F43DABAC9982D2450C|/
  • weixin://scanqrcode

上面的链接除了最后一个相信广大程序员男同胞都非常熟悉了,我们这次说的URL Scheme就指的是最后一种。

URL Scheme是什么?

我们在互联网上访问一个URL时管他叫网址;代码中使用他时叫绝对路径或相对路径;在向后端发送请求时叫请求地址;在学习时叫种子。

当我们在web端想打开一个App、在电脑桌面想打开一个桌面应用、在一个App里想打开另一个App,或者在App内部想调用原生功能时所用到的URL就称之为 URL Scheme。他是用于App与内外部沟通时所用到的资源定位符。

在我们的手机系统和脑系统中,每一个原生项目(相机、定位、相册、联系人、短信、摄像头、内存等)都会在系统中存取一个打开/使用他的方法。每当我们下载一款新App时,系统也会为这个App重新注册一个新的名称来方便调用和打开他。

安卓端和原生端有对应的方法可以注册和查询当前系统上所注册的App名称,对原生不是很懂,感兴趣的可以自行查找。

Native端通过访问这些URL唤起对应的功能或打开应用。

例如手机系统所携带的功能:

  • sms://(发短信)
  • tel://(打电话)
  • message://(打开邮件)
  • mailto:(发邮件)
  • app-Prefs://(打开设置)

例如一些常用App的URL Scheme:

  • QQ: mqq://
  • 微信:weixin://
  • 淘宝:taobao://
  • 微博:sinaweibo://
  • 支付宝:alipay://
  • B站:bilibili://

同时就好比我们访问同源(域名、ip、端口号)下不同路由可以显示不同页面一样,URL Scheme也支持通过访问参数的不同来调用或打开App中不同的页面或者功能。例如:

  • 视频:bilibili://video/videoid
  • 番剧:bilibili://bangumi/season/seansonid

URL Scheme调用Native的方式

通过上面对URL Scheme的介绍,我们知道如果想要打开一个App应用或者访问Native端的功能,直接访问它对应的URL Scheme即可。

那我们如何访问这个URL Scheme才能通知到Native端呢?

注意:以下几种通知方式都是建立在当前html5页面处于webview的环境中!!!

主要有以下几种方式:

  1. 使用a标签的href属性 <a href="weixin://"></a>
  2. 使用window.location的href属性 location.href = "weixin://"
  3. 使用window自带的弹窗 alert、confirm、prompt方法 alert("weixin://")
  4. 使用iframe的src属性 <iframe src="weixin://"></iframe>

几种方式的比较:

  1. a标签的href属性只有当我们点击他时才会去访问他跳转的链接,依赖于点击事件所以不推荐。
  2. 连续续调用 location.href会出现消息丢失,因为 WebView 限制了连续跳转,会过滤掉后续的请求。
  3. 每次调用原生都弹一个弹窗比较浪费性能,与本来的弹窗相冲突,如果没拦截好可能会多个弹窗出来。
  4. iframe可以通过样式来控制显示还是不显示,且与window上的方法并不冲突,拦截失败也不会导致页面变化。因此iframe是URL Scheme拦截的常用方式。

iframe通知代码如下

function executeScheme(href) {
    // 创建隐形iframe
    var iframe = document.createElement('iframe');
    iframe.style.cssText = 'display:none;width:0px;height:0px;';

    // iframe协议调用
    iframe.src = href;
    (document.body || document.documentElement).appendChild(iframe);

    // 删除iframe
    setTimeout(function () {
        iframe && iframe.parentNode && iframe.parentNode.removeChild(iframe);
    }, 0);
}

Native端拦截Scheme的方式

因为IOS端分为两种webview,且本人是前端开发对Native了解不深,所以以下内容仅以Android端举例。对应的IOS端也有实现的方法,感兴趣的可以自行查找资料。

监听弹窗拦截

当在webview中的html页面使用上述的三种弹窗来拦截Scheme时,在Native端有对应监听他们的方法,可以获取到他们弹窗的内容。

// 拦截 Alert 
@Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 
    if (message.startsWith('Scheme')) // Java不知道是不是这么写?这里的Scheme指的是对应App的Scheme
        // 解析 message 的值,调用对应方法 
    }
    // 如果不是Scheme开头的链接,就调用执行本来的逻辑window.alert()
    return super.onJsAlert(view, url, message, result); 
}

// 拦截 prompt
@Override 
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 
 return super.onJsPrompt(view, url, message, defaultValue, result);
} 

// 拦截 Confirm 
@Override public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 
    return super.onJsConfirm(view, url, message, result); 
} 

使用Native端特有的方法

Android端使用shouldOverrideUrlLoading方法拦截

@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { 
    if (url.startsWith("scheme")) { 
        // 拿到调用路径后解析调用的指令和参数,根据这些去调用 Native 方法
        /**
        * 这里和web类似,可以解析scheme后面的参数来调用对应的方法,同时也可以传递方法所需要的参数,例如:
        * 关闭webview: scheme://web/close
        * 打开原生loading: scheme://web/showLoading?msg=" + encodeMessage
        * 关闭原生loading:scheme://web/hideLoading
        * 打开单按钮消息确认框:scheme://web/showSingle
        * 打开双按钮消息确认框:scheme://web/showDouble
        * 设置webview标题: scheme://web/setTitle?title=encodeURIComponent(title)
        * 打开新的webview: scheme://web/open
        */
       
        return true; 
    } 
}

ios 中使用 UIWebView 内发起网络请求时,可以通过 delegate 在 native 层拦截,然后将捕获的 scheme 进行触发对应的功能或者业务逻辑,使用的是 shouldStartLoadWithRequest 方法。

JSBridge注入

JSBridge通过字面意思来理解,相当于在webview中建立了一座沟通Native和web的桥梁。但是桥梁是可以双向通信的存在,而JSBridge在我看来他只是将需要调用的Native端方法打包为了一个对象挂载到了web端的window上供js使用。

因此他只具备web端调用Native端的能力,并不具备Native端调用web端的能力。

Android注入

安卓端通过addJavascriptInterface方法来注入,根据官网介绍他接收两个参数,第一个参数是一个类表示注入JS的对象,第二个参数是一个字符串表示挂载到window上的名称。

对安卓感兴趣的朋友可以查看他们的官网:在WebView中编译Web应用

// 原生端代码

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JSBridge(this), "JSBridge");

// 注入JSBridge的对象
class JSBridge {
  private Context ctx;
  NativeBridge(Content ctx) {
    this.ctx = ctx;
  }

  //  JSBridge中的原生方法
  @JavascriptInterface
  public void showNativeDialog(String text) {
    new AlertDialog.Builder(ctx).setMessage(text).create().show
  }
}

注意:如果您将 targetSdkVersion 设置为 17 或更高,则必须向您希望 JavaScript(此方法也必须为公开方法)可用的任何方法添加 @JavascriptInterface 注释。如果您未提供注释,那么在 Android 4.2 或更高版本的平台上运行时您的网页将无法访问该方法。

// web端调用
window.JSBridge.showNativeDialog('这是一个弹窗');

IOS注入

IOS中内置了JavaScriptCore这个框架,可以实现执行JS以及注入Native对象等功能。这种方式不依赖拦截,主要是通过 Webview向JS的上下文注入对象和方法,可以让JS直接调用原生。

UIWebView

IOS UIWebview文档已弃用,这种方法不太被使用了。

// 原生端代码
// 获取 JS 上下文
JSContext *context = [webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 注入 Block
context[@"callHandler"] = ^(JSValue * data) {
    // 处理调用方法和参数  调用 Native 功能  回调 JS Callback
}
// web端
window.callHandler(JSON.stringify({
    type: "scan",
    data: "",
    callback: function(data) {
    }
}));

WKWebView

WKWebView 里面通过 addScriptMessageHandler 来注入对象到 JS 上下文,可以在 WebView 销毁的时候调用 removeScriptMessageHandler 来销毁这个对象。

前端调用注入的原生方法之后,可以通过 didReceiveScriptMessage 来接收前端传过来的参数。

WKWebView *wkWebView = [[WKWebView alloc] init];
WKWebViewConfiguration *configuration = wkWebView.configuration;
WKUserContentController *userCC = configuration.userContentController;

// 注入对象
[userCC addScriptMessageHandler:self name:@"nativeObj"];
// 清除对象
[userCC removeScriptMessageHandler:self name:@"nativeObj"];

// 客户端处理前端调用
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    // 获取前端传来的参数
    NSDictionary *msgBody = message.body;
    // 如果是 nativeObj 就进行相应处理
    if (![message.name isEqualToString:@"nativeObj"]) {
        // 
        return;
    }
}

使用 addScriptMessageHandler 注入的对象实际上只有一个 postMessage 方法,无法调用更多自定义方法。前端的调用方式如下:

window.webkit.messageHandlers.nativeObj.postMessage(data);

需要注意的是,这种方式要求 iOS8 及以上,而且返回不是同步的。和 UIWebView 一样的是,也支持直接传 JSON 对象,不需要 stringify。

JSBridge注入注意事项

我们在进入项目的第一件事往往就是为这个webview设置标题title,但是要注意项目初始化的这些方法必须要等到JSBridge已经注入到window中才可以使用,不然会报错。

使用这种通信方式时要格外注意页面中调用JSBridge方法一定要在JSBridge注入到window之后。

两种方式的优缺点比较

Scheme拦截

优点:

  1. 最大的优点就是可以跨App端使用,比如我们可以在一个App打开另一个App或者访问另一个App的页面。因为他不是注入到App本地对象中的,而是通过资源定位符URL访问的。
  2. 兼容性比较好,因为使用URL不是很依赖机器系统及环境

缺点:

  1. 使用不够直观,我们习惯于直接调用方法,而不是访问协议。

  2. url的大小有限制,不可以过长。

JSBridge注入

优点:

  1. 使用起来非常直观方便,就和调用原生方法一样。
  2. 没有长度限制,对传入的参数个数没有要求。

缺点:

  1. 依赖系统和环境,有兼容性问题。

    Android的JavascriptInterface在Android4.2之前因为没有注解导致暴露了包括系统类方法在内的其他不应暴露的接口,存在较大的安全隐患。而ios的WKScriptMessageHandler仅支持ios8.0+的版本。

  2. 使用方法时必须要等到注入完成之后(算不上问题,方法一般都是在初始化之后才会被调用)。

  3. 因为他是一个对象,所以他只能访问这个对象上提供给他的方法,无法直接访问到外部App。(但是也可以把访问外部App的方法写到Native端)。

综上所述,目前安卓版本和ios版本都比较高,且从当前App访问外部App的情况较少。为了使用方便当下还是采取JSBridge通信的方式居多。

Native端调用web端

Native 调用 JS 一般是使用 JS 代码字符串,有些类似我们调用 JS 中的 eval 去执行一串代码。一般有 loadUrlevaluateJavascript 等方法。

不管哪种方法,客户端都只能拿到挂载到 window 对象上面的属性和方法。

Android

在 Android 里面需要区分版本,在安卓4.4之前的版本支持 loadUrl,使用方式类似我们在 a 标签的 href 里面写 JS 脚本一样,都是javascript:xxx 的形式。

在安卓4.4以上的版本一般使用 evaluateJavascript 这个 API 来调用。这里需要判断一下版本。

if (Build.VERSION.SDK_INT > 19) //see what wrapper we have
{
    webView.evaluateJavascript("javascript:foo()", null);
} else {
    webView.loadUrl("javascript:foo()");
}

通过evaluateJavascript方法可以获取到JavaScript的返回值,而第一种不可以。因此evaluateJavascript是官方推荐的通信方式。

IOS

UIWebView

在 IOS 的 UIWebView 里面使用 stringByEvaluatingJavaScriptFromString 来调用 JS 代码。这种方式是同步的,会阻塞线程。

results = [self.webView stringByEvaluatingJavaScriptFromString:"foo()"];

WKWebView

WKWebView 可以使用 evaluateJavaScript 方法来调用 JS 代码。

[self.webView evaluateJavaScript:@"document.body.offsetHeight;" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
    // 获取返回值 response
    }];

参考文档

JS Bridge 通信原理

除了直接访问Native端之外,我们有时还需要一个回调函数来知晓用户在Native端操作了什么。例如:

  • 打开本地相册,用户选择的是哪一张照片;
  • 打开省市区三级联动地址,选择了什么地址;
  • 打开了一个输入框,输入了什么信息;

这些都需要一个回调来告知给web端。

篇幅有限,有关通信时回调的方法请看我的下一篇文章:webview与Native通信之回调函数处理

码字、查找资料不易,各位看官如果觉得有所收获还请动动小手点个赞啦~