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

相关推荐
Asort1 天前
JavaScript 从零开始(六):控制流语句详解——让代码拥有决策与重复能力
前端·javascript
无双_Joney1 天前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥1 天前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare1 天前
选择文件夹路径
前端
艾小码1 天前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月1 天前
JavaScript作用域与作用域链详解
前端·面试
ace望世界1 天前
android的Parcelable
android
泉城老铁1 天前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅1 天前
JavaScript 作用域常见问题及解决方案
前端·javascript