H5与原生Native的交互以及通信方式

一、背景

考虑到洞察报告后⾯会内嵌到原⽣App中,洞察报告的展⽰形式可以使⽤移动端H5的⽅式进⾏展⽰, 图表以及⽂本的配置需要做相关适配,具体图表如何适配移动端后⾯需要单独调研(需要先确定使⽤ 哪种图表)。⽽H5主要是运⾏在浏览器上,内嵌到App中,需要涉及到双⽅通信的交互,本⽂主要以 web端H5的⻆度调研并整理双⽅的交互及通信⽅式。

⼆、通信容器WebView

H5和原⽣App⼆者通信的桥梁是webView,webView是移动端(原⽣)提供的运⾏web的环境,它是⼀ 种嵌⼊式浏览器,原⽣应⽤可以⽤它来展⽰⽹络内容。可与⻚⾯JavaScript交互,实现混合开发。

iOS容器:UIWebView和WKWebView。

UIWebView是⼀个可加载⽹⻚的对象,它有浏览记录功能,且对加载的⽹⻚内容是可编程的。说⽩了 UIWebView有类似浏览器的功能,我们可以使⽤它来打开⻚⾯,并做⼀些定制化的功能,如可以让js 调某个⽅法可以取到⼿机的GPS信息。

注意⚠️:苹果发布iOS8的时候,新增了⼀个WKWebView组件容器,如果你的APP只考虑⽀持iOS8及 以上版本,那么你就可以使⽤这个新的浏览器控件了。 并且⽬前Apple要求新发布的app不能使⽤ UIWebView,现在应该⽤WKWebView。

Android容器: 在安卓客⼾端中,webView容器与⼿机⾃带的浏览器内核⼀致,多为androidchrome。不存在兼容性和性能问题。

RN容器 : 在react-native开发中,从rn 0.37版本开始官⽅引⼊了组件,在安卓中调⽤原⽣浏览器,在 IOS中默认调⽤的是UIWebView容器。从IOS12开始,苹果正式弃⽤UIWebView,统⼀采⽤ WKWebView。RN从0.57起,可指定使⽤WKWebView作为WebView的实现。

三、通信⽅式⸺介绍

1. JavaScript 调⽤ Native

JavaScript 调⽤ Native 的⽅式,主要有两种:注⼊ API 和 拦截 URL SCHEME

注⼊API

注⼊ API ⽅式的主要原理是,通过 WebView 提供的接⼝,向 JavaScript 的 Context(window)中注 ⼊对象或者⽅法,让 JavaScript 调⽤时,直接执⾏相应的 Native 代码逻辑,达到 JavaScript 调⽤ Native 的⽬的。

拦截 URL SCHEME

URL SCHEME是⼀种类似于url的链接,是为了⽅便app直接互相调⽤设计的,形式和普通的 url 近 似,主要区别是 protocol 和 host ⼀般是⾃定义的,例如: qunarhy://hy/url?url=ymfe.tech, protocol 是 qunarhy,host 则是 hy。

拦截 URL SCHEME 的主要流程是:Web 端通过某种⽅式(例如 iframe.src)发送 URL Scheme 请 求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进⾏相关操作。

2. Native调⽤JavaScript

因为app是宿主,可以直接访问H5,所以这种调⽤⽐较简单,直接从外部调⽤ JavaScript 中的⽅法即 可,因此 Native调⽤JavaScript时,只需要将JavaScript 的⽅法挂载在全局的 window 上即可

四、双向通信

1. H5和 iOS 的双向通信

iOS上UIWebView⽤JavaScriptCore框架,WKWebView⽤MessageHandler。

1.1 H5→IOS

因为H5不能直接访问宿主app,所以这种调⽤就⽐较复杂⼀点,常⽤的有两种⽅式:

⽅式⼀:拦截URL SCHEME

由 h5 发起⼀个⾃定义协议请求,app 拦截这个请求后,再由 app 调⽤ h5 中的回调函数。

在iOS中,并没有现成的 api 让 js 去调⽤ Native的⽅法,但是UIWebView与WKWebView能够拦截h5 内发起的所有⽹络请求,这些请求都可以通过delegate函数在iOS中得到通知,所以我们的思路就是通过在h5内发起约定好的特定协议的⽹络请求,即拦截URL SCHEME。

举个🌰:

javascript 复制代码
// javascript 
window.location.href = 'jsbridge://methodName?params=' + encodeURIComponent(obj)

请求地址: 'jsbridge://methodName?params=' + encodeURIComponent(obj) ,只需要在客⼾端WebView中发现是jsbridge:// 开头的地址,就拦截该请求并解析约定的参数,就可以 使客⼾端执⾏相应的逻辑。

在H5中发起这种特定协议的请求⽅式分两种,都是通过拦截URL SCHEME的⽅式:

  1. 通过localtion.href; 缺点:通过location.href有个问题,就是如果我们连续多次修改 window.location.href的值,在Native层只能接收到最后⼀次请求,前⾯的请求都会被忽略 掉。创建请求,需要⼀定的耗时,功能复杂时,耗时会较⻓。
  2. 通过iframe的⽅式,即使⽤ iframe.src 发送 URL SCHEME

使⽤iframe⽅式,唤起Native客⼾端的分享组件为例:

ini 复制代码
// h5 js code 将它封装一下
  function createIframe(url){
    var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
    var iframe = document.createElement('iframe');
    iframe.style.width = '1px';
    iframe.style.height = '1px';
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);
    setTimeout(function() {
        iframe.remove();
    }, 100);
  }

然后客⼾端通过拦截这个请求,并且解析出相应的⽅法和参数: 这⾥以ios为例:

swift 复制代码
// IOS swift code
 func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    print("shouldStartLoadWithRequest")
    let url = request.URL
    let scheme = url?.scheme
    let method = url?.host
    let query = url?.query
  
    if url != nil && scheme == "jsbridge" {
        print("scheme == (scheme)")
        print("method == (method)")
        print("query == (query)")
  
        switch method! {
            case "getData":
                self.getData()
            case "putData":
                self.putData()
            case "gotoWebview":
                self.gotoWebview()
            case "gotoNative":
                self.gotoNative()
            case "doAction":
                self.doAction()
            case "configNative":
                self.configNative()
            default:
                print("default")
        }
  
        return false
    } else {
        return true
    }
 }
拦截URL SCHEME总结

缺点: 使⽤ iframe.src 发送 URL SCHEME 会有 url ⻓度的隐患。有些⽅案为了规避 url ⻓度隐患 的缺陷,在 iOS 上采⽤了使⽤ Ajax 发送同域请求的⽅式,并将参数放到 head 或 body ⾥。这样,虽 然规避了 url ⻓度的隐患,但是 WKWebView 并不⽀持这样的⽅式

优点: ⽀持 iOS6,但是⼤环境下iOS6 占⽐很⼩,这种⽅式并不优雅。

⽅式⼆:注⼊API

由 app 向 h5 注⼊⼀个全局 js 对象,然后在 h5 直接访问这个对象。 iOS 的 UIWebView实例如下:

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

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

在javascript中使⽤:

javascript 复制代码
// javascript
window.postBridgeMessage(message);

对于 iOS 的 WKWebView 可以⽤以下⽅式:

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

在javascript中使⽤:

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

1.2 IOS→H5的通信方式

因为app是宿主,可以直接访问H5,所以这种调用比较简单,直接从外部调用 JavaScript 中的方法即可,因此JavaScript 的方法必须在全局的 window 上。那么native端调用JavaScript的方法如下:

通过UIWebView组件的stringByEvaluatingJavaScriptFromString或者WKWebViewevaluateJavaScript的方法来实现的,该方法返回js脚本的执行结果。

java 复制代码
// IOS swift code
// UIWebView
webview.stringByEvaluatingJavaScriptFromString("window.methodName()")
// WKWebView
wkWebView evaluateJavaScript("window.methodName()")

使用这种方式调用js的方法,就需要将这个方法挂载到window下,从全局考虑,我们只要暴露一个对象如JSBridge让native调用就好了,所以在这里可以对Native的代码做一个简单的封装:

scss 复制代码
//下面为伪代码
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
 webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}

2. H5和Android的双向通信

2.1 H5→Android

⽅式⼀:拦截URL SCHEME

通过WebViewClientshouldOverrideUrlLoading ⽅法对url协议进⾏解析,这种js的调⽤ ⽅式与ios的⼀样。

⽅式⼆:注⼊原⽣js

通过在webview⻚⾯⾥直接注⼊原⽣js代码⽅式,使⽤ addJavascriptInterface ⽅法来实现, 这种⽅式最简洁,仅将Android对象和JS对象映射即可,但在 Android 4. 2 以下存在漏洞(点击查看具体文章)。因此在 4.2 中引⼊新的接⼝ @JavascriptInterface 来替代这个接⼝,解决安全问题。

在android里注入原生js代码实现如下:

typescript 复制代码
class JSInterface {
    @JavascriptInterface // 被JS调用的方法必须加入@JavascriptInterface注解
    public String getUserData() {
        return "UserData";
    }
}

// 通过addJavascriptInterface()将Java对象映射到JS对象
//参数1:Javascript对象名
//参数2:Java对象名
webView.addJavascriptInterface(new JSInterface(), "AndroidJS"); //JSInterface类对象映射到js的AndroidJS对象

上面的代码就是在页面的window对象里注入了AndroidJS对象。在js里可以直接调用

javascript 复制代码
// js
window.AndroidJS.getUserData())

⽅式三:拦截js对话框信息

通过 WebChromeClientonJsAlert()onJsConfirm()onJsPrompt()方法回调拦截JS对话框alert()confirm()prompt() 消息,一般我们使用prompt,因为它可以返回任意类型的值(alert()对话框没有返回值;confirm()对话框只能返回两种状态:确定 / 取消),并且这个在js里使用的不多,用来和native通讯副作用比较少。

  1. 通过 WebChromeClientonJsAlert()onJsConfirm()onJsPrompt()方法回调拦截JS对话框alert()confirm()prompt() 消息,一般我们使用prompt,因为它可以返回任意类型的值(alert()对话框没有返回值;confirm()对话框只能返回两种状态:确定 / 取消),并且这个在js里使用的不多,用来和native通讯副作用比较少。
scss 复制代码
//js
prompt("js://demo?arg1=111&arg2=222");
less 复制代码
// Android
class SetWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 这里就可以对js的prompt进行处理,通过result返回结果
    }
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {

    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

三种⽅式总结

调用方式 优点 缺点 适用场景
通过WebViewClientshouldOverrideUrlLoading方法对url协议进行解析 不存在漏洞问题 使用复杂,需要约定协议,从native层向web层传递值比较繁琐; 会有 url 长度的隐患 不需要返回值的情况下的互调场景
通过在webview页面里直接注入原生js代码方式,使用addJavascriptInterface方法来实现 方便简洁 Android 4.2以下版本存在漏洞 Android 4.2以上版本相对简单的互调场景
通过 WebChromeClientonJsAlert()onJsConfirm()onJsPrompt()方法回调拦截js对话框信息 不存在漏洞问题 使用复杂,需要进行协议的约束 能满足大多数情况下的互调场景,一般Android 4.2以下版本使用这种方式

2.2 Android→H5

与iOS类似,也是直接从外部调用 JavaScript 中的方法,将JavaScript 的方法必须在全局的 window 上

Android调用window上的JS代码的方法有2种:

  1. 客户端通过webviewloadUrl进行调用:
arduino 复制代码
// android JAVA code
 webView.loadUrl("javascript:window.jsBridge.getShare()");

H5端将方法绑定在window下的对象即可,无需与IOS作区分,需注意:JS代码调用一定要在 onPageFinished()回调之后才能调用,否则调用不生效,onPageFinished()属于WebViewClient类的方法,主要在页面加载结束时调用。

  1. 通过WebViewevaluateJavascript()
typescript 复制代码
 webView.evaluateJavascript("javascript:window.jsBridge.getShare()",new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此处为 js 返回的结果
        }
    });

二者区别:

  1. loadUrl() 会刷新页面,evaluateJavascript() 则不会使页面刷新,所以 evaluateJavascript() 的效率更高
  2. loadUrl() 得不到js的返回值,对性能要求较低时可以使用这个方式;evaluateJavascript() 可以获取返回值
  3. evaluateJavascript() 在 Android 4.4 之后才可以使用

五、前端JSBridge的封装

1.什么是JSbridge

JSBridge主要是给 JavaScript 提供调用 Native 功能的接口,让混合开发中的前端部分可以方便地使用 Native 的功能(例如:地址位置、摄像头)。而且 JSBridge 的功能不止调用 Native 功能这么简单宽泛。实际上,JSBridge 就像其名称中的Bridge的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是构建 Native 和非 Native 间消息通信的通道,而且这个通信的通道是双向的。

2.JSBridge通信实现原理

上面的通信原理就是 JSBridge 实现的核心,实现方式可以各种各样,但是万变不离其宗。推荐的实现方式如下:

  • JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。
  • Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。

3.JSBridge 接口实现

从上面的剖析中,可以得知,JSBridge 的接口主要功能有两个:调用 Native(给 Native 发消息)接被 Native 调用(接收 Native 消息) 。因此,JSBridge 可以设计如下:

代码的实现过程可以直接看代码中的注释。

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

    window.JSBridge = {
        // h5 调用 Native,调用时会将回调 id 存放到本地变量cbList中
        invoke: function(bridgeName, callback, data) {
            // 判断环境,获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            // 调用native将参数传递进去 ==> 通信方式可任意选择
            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
                        });
                    }
                }
            }
        },
        // js注册方法,native主动发起调用
        register: function(bridgeName, callback) {
            if (!registerFuncs[bridgeName])  {
                registerFuncs[bridgeName] = [];
            }
            registerFuncs[bridgeName].push(callback); // 存储回调
        }
    };
}
)();

调用示例

javascript 复制代码
// 主动发消息
JSBridge.invoke('ui.callNative', (data) => data,{}) 
// 注册在本地,被动接受客户端调用
JSBridge.register("ui.datatabupdate", (data) => data);

4. JSBridge如何引⽤

对于 JSBridge 的引⽤,常⽤有两种⽅式,各有利弊。

4.1由Native端进⾏注⼊。

注⼊⽅式和 Native 调⽤ JavaScript 类似,直接执⾏桥的全部代码。

优点:桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容。

缺点:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先判断 JSBridge 是否已经注入成功。

4.2由JavaScript端引⽤

直接与 JavaScript ⼀起执⾏。与由 Native 端注⼊正好相反。 优点:JavaScript 端可以确定 JSBridge 的存在,直接调用即可。

缺点:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。

六、补充

⽀持跨平台的JSbridge⸺DSBridge

DSBridge for IOS

DSBridge for Android

相关推荐
肥肠可耐的西西公主14 分钟前
前端(小程序)学习笔记(CLASS 2):WXML模板语法与WXSS模板样式
前端·学习·小程序
aningxiaoxixi19 分钟前
android property 系统
android
speop36 分钟前
TASK05【Datawhale 组队学习】系统评估与优化
android·java·学习
Magnetic_h1 小时前
【iOS】类结构分析
开发语言·笔记·学习·ios·objective-c
逆袭的菜鸟X1 小时前
RxJS 高阶映射操作符详解:map、mergeMap 和 switchMap
前端
bubiyoushang8881 小时前
HTML5的新语义化标签
前端·html·html5
会飞的鱼先生2 小时前
vue3自定义指令来实现 v-copy 功能
前端·javascript·vue.js
陈天伟教授2 小时前
Web前端开发 - 制作简单的焦点图效果
java·开发语言·前端·前端开发·visual studio
_殊途2 小时前
前端三件套之html详解
前端·html
不思念一个荒废的名字3 小时前
【黑马JavaWeb+AI知识梳理】后端Web基础03 - MySQL概述
前端·数据库·mysql