前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >H5如何与原生App通信?

H5如何与原生App通信?

原创
作者头像
用户9253515
发布2021-12-17 11:19:42
5.8K0
发布2021-12-17 11:19:42
举报
文章被收录于专栏:Android开发技术Android开发技术

前言

为了提高开发效率,开发人员往往会使用原生app里面嵌套前端h5页面的快速开发方式,这就要涉及到h5和原生的相互调用,互相传递数据,接下来就实践项目中的交互方式做一个简单的记录分享,废话不多说,直接上正文:

由于安卓和ios的处理方式不一样,所以我们要分开处理 先贴上判断访问终端的代码

//判断访问终端

function browserVersion(){

代码语言:txt
复制
var u = navigator.userAgent;
代码语言:txt
复制
return {
代码语言:txt
复制
    trident: u.indexOf('Trident') > -1, //IE内核
代码语言:txt
复制
    presto: u.indexOf('Presto') > -1, //opera内核
代码语言:txt
复制
    webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核
代码语言:txt
复制
    gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1,//火狐内核
代码语言:txt
复制
    mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端
代码语言:txt
复制
    ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端
代码语言:txt
复制
    android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, //android终端
代码语言:txt
复制
    iPhone: u.indexOf('iPhone') > -1 , //是否为iPhone或者QQHD浏览器
代码语言:txt
复制
    iPad: u.indexOf('iPad') > -1, //是否iPad
代码语言:txt
复制
    webApp: u.indexOf('Safari') == -1, //是否web应该程序,没有头部与底部
代码语言:txt
复制
    weixin: u.indexOf('MicroMessenger') > -1, //是否微信 (2015-01-22新增)
代码语言:txt
复制
    qq: u.match(/\sQQ/i) == " qq" //是否QQ
代码语言:txt
复制
};

通信原理之先了解webview

IOS容器 在IOS客户端中,我们首先要提到的是一个叫UIWebView的容器,苹果对他的介绍是:

UIWebView是一个可加载网页的对象,它有浏览记录功能,且对加载的网页内容是可编程的。说白了UIWebView有类似浏览器的功能,我们使用可以它来打开页面,并做一些定制化的功能,如可以让js调某个方法可以取到手机的GPS信息。

但需要注意的是,Safari浏览器使用的浏览器控件和UIwebView组件并不是同一个,两者在性能上有很大的差距。幸运的是,苹果发布iOS8的时候,新增了一个WKWebView组件容器,如果你的APP只考虑支持iOS8及以上版本,那么你就可以使用这个新的浏览器控件了。

WKWebView重构了原有UIWebView的14个类,3个协议,性能提升的同时,赋予了开发者更加细致的配置(这些配置仅针对客户端IOS开发,对于前端H5来说,保持两种容器调用方法的一致性很重要)。

Android容器 在安卓客户端中,webView容器与手机自带的浏览器内核一致,多为android-chrome。不存在兼容性和性能问题。

RN容器 在react-native开发中,从rn 0.37版本开始官方引入了组件,在安卓中调用原生浏览器,在IOS中默认调用的是UIWebView容器。从IOS12开始,苹果正式弃用UIWebView,统一采用WKWebView。

RN从0.57起,可指定使用WKWebView作为WebView的实现

// rn js code

<WebView useWebKit={true} source={{ url: 'https://m.douyu.com' }} />

WebView组件不要嵌套在或原生点击组件中,会造成H5内页面滚动失效

h5向ios客户端发送消息;

在ios中,并没有现成的api让js去调用native的方法,但是UIWebView与WKWebView能够拦截h5内发起的所有网络请求。所以我们的思路就是通过在h5内发起约定好的特定协议的网络请求,如'jsbridge://bridge2.native?params=' + encodeURIComponent(obj)然后带上你要传递给ios的参数;然后在客户端内拦截到指定协议头的请求之后就阻止该请求并解析url上的参数,执行相应逻辑

在H5中发起这种特定协议的请求方式分两种:

通过localtion.href;

通过location.href有个问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。

通过iframe方式;

使用iframe方式,以唤起Native;以唤起分享组件为例

// h5 js code 将它封装一下

function createIframe(url){

代码语言:txt
复制
var url = 'jsbridge://getShare?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.douyu.com&cbName=jsCallClientBack';
代码语言:txt
复制
var iframe = document.createElement('iframe');
代码语言:txt
复制
iframe.style.width = '1px';
代码语言:txt
复制
iframe.style.height = '1px';
代码语言:txt
复制
iframe.style.display = 'none';
代码语言:txt
复制
iframe.src = https://segmentfault.com/a/url;
代码语言:txt
复制
document.body.appendChild(iframe);
代码语言:txt
复制
setTimeout(function() {
代码语言:txt
复制
    iframe.remove();
代码语言:txt
复制
}, 100);

}

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

// IOS swift code

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {

代码语言:txt
复制
    let url = request.URL
代码语言:txt
复制
    let scheme = url?.scheme
代码语言:txt
复制
    let method = url?.host
代码语言:txt
复制
    let query = url?.query
代码语言:txt
复制
    if url != nil && scheme == "jsbridge" {
代码语言:txt
复制
        switch method! {
代码语言:txt
复制
            case "getShare":
代码语言:txt
复制
                self.getShare()
代码语言:txt
复制
            default:
代码语言:txt
复制
                print("default")
代码语言:txt
复制
        }
代码语言:txt
复制
        return false
代码语言:txt
复制
    } else {
代码语言:txt
复制
        return true
代码语言:txt
复制
    }
代码语言:txt
复制
}

看不懂就略过,非重点。。。。。

这里我们在请求参数中加上了cbName=jsCallClientBack,这个jsCallClientBack为JS调用客户端所定义的回调函数,在业务层jsBridge封装中,我们传入一个匿名函数作为回调,底层将这个函数绑定在window的jsbridge对象下并为其定义一个独一无二的key,这个key就是jsCallClientBack,客户端在处理完逻辑后,会通过上面已经介绍过的方法来回调window下的方法。

ps: 在将回调绑定在window下时,特别注意要使用bind保持函数内this的原有指向不变

IOS客户端调用H5方法

Native调用Javascript语言,是通过UIWebView组件的stringByEvaluatingJavaScriptFromString方法来实现的,该方法返回js脚本的执行结果。

// IOS swift code

webview.stringByEvaluatingJavaScriptFromString("window.methodName()")

从上面代码可以看出它其实就是执行了一个字符串化的js代码,调用了window下的一个对象,如果我们要让native来调用我们js写的方法,那这个方法就要在window下能访问到。但从全局考虑,我们只要暴露一个对象如JSBridge给native调用就好了。

调用客户端原生方法的回调函数也将绑在window下供客户端成功反调用,实际上一次调用客户端方法最后产生的结果是双向互相调用。

H5调用Android客户端方法

在安卓webView中有三种调用native的方式:

通过schema方式,客户端使用shouldOverrideUrlLoading方法对url请求协议进行解析。这种js的调用方式与ios的一样,使用iframe来调用native方法。 通过在webview页面里直接注入原生js代码方式,使用addJavascriptInterface方法来实现。

// android JAVA code

class JSInterface {

代码语言:txt
复制
@JavascriptInterface
代码语言:txt
复制
public String getShare() {
代码语言:txt
复制
    //...
代码语言:txt
复制
    return "share";
代码语言:txt
复制
}

}

webView.addJavascriptInterface(new JSInterface(), "AndroidNativeApi");

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

使用prompt,console.log,alert方式,这三个方法对js里是属性原生的,在android webview这一层是可以重写这三个方法的。一般我们使用prompt,因为这个在js里使用的不多,用来和native通讯副作用比较少。

// android JAVA code

class WebChromeClient extends WebChromeClient {

代码语言:txt
复制
@Override
代码语言:txt
复制
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
代码语言:txt
复制
    // 重写window下的prompt,通过result返回结果
代码语言:txt
复制
}
代码语言:txt
复制
@Override
代码语言:txt
复制
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
代码语言:txt
复制
}
代码语言:txt
复制
@Override
代码语言:txt
复制
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
代码语言:txt
复制
}

}

一般而言安卓客户端选用1、2方案中的一种进行通信,从前端层面来讲,推荐客户端都使用schema协议的方式,便于前端jsBridge底层代码的维护与迭代。

Android客户端调用H5方法

在安卓APP中,客户端通过webview的loadUrl进行调用:

// android JAVA code

webView.loadUrl("javascript:window.jsBridge.getShare()");

H5端将方法绑定在window下的对象即可,无需与IOS作区分

H5调用RN客户端

我们知道RN的webView组件实际上就是对原生容器的二次封装,因此我们不需要直接通过schema协议来通信,只需要使用浏览器postMessage、onMessage来传递消息即可,类似于iframe,而真正的通信过程RN已经帮我们做了。

// h5 js code

window.postMessage(data);

// rn js code

<WebView

代码语言:txt
复制
  ref="webView"
代码语言:txt
复制
  source={require('../html/index.html')}
代码语言:txt
复制
  injectedJavaScript={'window.androidConfig = {}'}    // 通过这个props可以在webView初始化时注入属性方法
代码语言:txt
复制
  onMessage={e => {
代码语言:txt
复制
      let { data } = e.nativeEvent;
代码语言:txt
复制
    //...
代码语言:txt
复制
  }}

/>

RN客户端调用H5

postMessage是双向的,所以也可以在RN里发消息,H5里接消息来触发对应的回调

this.refs.webView.postMessage({

代码语言:txt
复制
cbName: 'xxx',
代码语言:txt
复制
param: {}

});

前端jsBridge的封装

在了解了js与客户端底层的通信原理后,我们可以将IOS、安卓统一封装成jsBridge提供给业务层开发调用。

class JsBridge {

代码语言:txt
复制
static lastCallTime
代码语言:txt
复制
constructor() {
代码语言:txt
复制
    if (UA.isReactNative()) {
代码语言:txt
复制
       document.addEventListener('message', function(e) {
代码语言:txt
复制
           window.jsClientCallBack[e.data.cbName](e.data.param);
代码语言:txt
复制
       });
代码语言:txt
复制
     }
代码语言:txt
复制
}
代码语言:txt
复制
// 通用callNtive方法
代码语言:txt
复制
callClient(functionName, data, callback) {
代码语言:txt
复制
    // 避免连续调用
代码语言:txt
复制
    if (JsBridge.lastCallTime && (Date.now() - JsBridge.lastCallTime) < 100) {
代码语言:txt
复制
        setTimeout(() => {
代码语言:txt
复制
            this.callClient(functionName, data, callback);
代码语言:txt
复制
        }, 100);
代码语言:txt
复制
        return;
代码语言:txt
复制
    }
代码语言:txt
复制
    JsBridge.lastCallTime = Date.now();
代码语言:txt
复制
    data = data || {};
代码语言:txt
复制
    if (callback) {
代码语言:txt
复制
        const cbName = randomName();
代码语言:txt
复制
        Object.assign(data, { cbName });
代码语言:txt
复制
        window.jsClientCallBack[cbName] = callBack.bind(this);
代码语言:txt
复制
    }
代码语言:txt
复制
    if (UA.isIOS()) {
代码语言:txt
复制
        data.forEach((key, value) => {
代码语言:txt
复制
            try {
代码语言:txt
复制
                data[key] = JSON.stringify(value);
代码语言:txt
复制
            } catch(e) { }
代码语言:txt
复制
        });
代码语言:txt
复制
        var url = 'jsbridge://' + functionName + '?' parseJSON(data);
代码语言:txt
复制
        var iframe = document.createElement('iframe');
代码语言:txt
复制
        iframe.style.width = '1px';
代码语言:txt
复制
        iframe.style.height = '1px';
代码语言:txt
复制
        iframe.style.display = 'none';
代码语言:txt
复制
        iframe.src = url;
代码语言:txt
复制
        document.body.appendChild(iframe);
代码语言:txt
复制
        setTimeout(() => {
代码语言:txt
复制
            iframe.remove();
代码语言:txt
复制
        }, 100);
代码语言:txt
复制
    } else if (UA.isAndroid()) {    //  这里安卓客户端使用的是上面说的第二种通信方法
代码语言:txt
复制
        window.AndroidNativeApi && 
代码语言:txt
复制
        window.AndroidNativeApi[functionName] && 
代码语言:txt
复制
        window.AndroidNativeApi[functionName](JSON.stringify(data));
代码语言:txt
复制
    } else if (UA.isReactNative()) {     //rn的<webView>组件可以设置props.userAgent来让H5识别
代码语言:txt
复制
        window.postMessage(
代码语言:txt
复制
          JSON.stringify(data);
代码语言:txt
复制
        );
代码语言:txt
复制
    } else {
代码语言:txt
复制
        console.error('未获取platform信息,调取api失败');
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
// 业务层自定义方法
代码语言:txt
复制
getShare(data, callBack) {
代码语言:txt
复制
        //..
代码语言:txt
复制
}

}

在核心封装的基础上,我们可以还做更多的优化,比如将每个回调函数调用后自我销毁释放内存

四、调试

安卓使用chrome://inspect进行调试,需要翻墙 IOS使用mac safari的develop选项进行调试 使用RN的http://localhost:8081/debugger-ui 只能调试RN代码,无法调试webView代码,RN下webView调试和对应native相同,但是在chrome://inspect下会出现样式问题。 除非是纯RN编写,直接打包成APP,否则不建议在RN下调用webView组件

相关教程

Android基础系列教程: Android基础课程U-小结_哔哩哔哩_bilibili Android基础课程UI-布局_哔哩哔哩_bilibili Android基础课程UI-控件_哔哩哔哩_bilibili Android基础课程UI-动画_哔哩哔哩_bilibili Android基础课程-activity的使用_哔哩哔哩_bilibili Android基础课程-Fragment使用方法_哔哩哔哩_bilibili Android基础课程-热修复/热更新技术原理_哔哩哔哩_bilibili

本文转自 https://juejin.cn/post/6885988193402159118,如有侵权,请联系删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 相关教程
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档