跨端相关


1.webview

webView是移动端(原生)提供的运行web的环境,它是一种嵌入式浏览器,原生应用可以用它来展示网络内容。可与页面JavaScript交互,实现混合开发。

webview渲染的作用是:webview可以内嵌在移动端,实现前端的混合式开发,大多数混合式开发框架都是基于webview模式进行二次开发的。webview可以直接使用html文件(网络上或本地assets中)作布局,可和JavaScript交互调用。webview是一个基于webkit的引擎。

所以webview是H5跨端常用方式

2.JSBridge

Web端和Native可以类比于Client/Server模式,Web端调用原生接口时就如同Client向Server端发送一个请求类似,JSB在此充当类似于HTTP协议的角色。

H5 中的跨端通信称为 JSBridge,在进行一次 JSBridge 调用的时候会携带调用参数

ModuleId: 模块 ID
MethodId: 方法 ID
params: 参数
CallbackId: JS 回调名

ModuleIdMethodId 能定位到具体调用的原生方法,params 参数作为原生方法调用的参数,最后通过 CallbackId 回调 JS 的回调函数(我们可以自行脑补react中父子组件通过回调通信的方式)

image-20220611220825018

在使用 H5 的情况下,Webview 是 JS 的执行引擎,同时 Webview 还是页面的渲染引擎。

实现JSBridge主要是两点:

  1. 将Native端原生接口封装成JavaScript接口
  2. 将Web端JavaScript接口封装成原生接口

Native to Web

Native 调用 JS 比较简单,只要 H5 将 JS 方法暴露在 Window 上给 Native 调用即可。

JavaScript作为解释性语言,最大的一个特性就是可以随时随地地通过解释器执行一段JS代码,所以可以将拼接的JavaScript代码字符串,传入JS解析器执行就可以,JS解析器在这里就是webView。

  • Android 4.4之前只能用loadUrl来实现,并且无法执行回调
val jsCode = String.format("window.showWebDialog('%s')", text)
webView.loadUrl("javascript: " + jsCode)
  • Android 4.4之后提供了evaluateJavascript来执行JS代码,并且可以获取返回值执行回调;
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
  @Override
  public void onReceiveValue(String value) {
     //拿到返回值后进行处理   
  }
});
Android 版本 API 特点
低于4.4 WebView.loadUrl 无法执行回调
高于4.4 WebView.evaluateJavascript 可以拿到 JS 执行完毕的返回值
  • iOS的UIWebView使用stringByEvaluatingJavaScriptFromString;
NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];
  • iOS的WKWebView使用evaluateJavaScript;
[webView evaluateJavaScript:@"执行的JS代码" completionHandler:^(id _Nullable response, NSError * _Nullable error) {

}];
iOS 版本 API 特点
低于8.0 UIWebView.stringByEvaluatingJavaScriptFromString 无法执行回调
高于8.0 WKWebView.evaluateJavaScript 可以拿到 JS 执行完毕的返回值

Web to Native

JS调用native方法

方案 调用方法 速度 注意事项
注入api addJavascriptInterface 较快 Android < 4.2 存在安全漏洞
url拦截 shouldOverrideUrlLoading 最慢
JS回调时机 onJsPrompt 较快
日志输出 console.log 最快 scheme://dddd

(1)全局注入API

也可以解释为在window注入native封装好的方法,前端通过 window.xxx调用,

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

1、 客户端定义js映射对象

具体有安卓 webview 的 addJavascriptInterface,iOS UIWebview 的 JSContext,iOS WKWebview 的 scriptMessageHandler。

 public class AndroidToJS {
    // 定义JS需要调用的方法
    // 被JS调用的方法必须加入@JavascriptInterface注解
    @JavascriptInterface
    public void callAndroid(String msg){
        Log.e("zw","JS调用了Android的callAndroid(),msg : " + msg);
    }}

2、注入js方法

webView.addJavascriptInterface(new AndroidToJS(),"android");

3、前端调用

window.NativeBridge.callAndroid('hello');

4、优缺点

优点:通信时间短,调用方便。

缺点:使用 webView.addJavascriptInterface 方法进行注入。此方法存在漏洞(安全隐患),在 Android4.2 以上提供 @javascriptInterface 注解来规避该漏洞,但对于4.2以下版本则没有任何方法。所以使用该方法有一定的风险和兼容性问题。

(2)scheme拦截

可以解释为,发请求,然后客户端拦截请求,调用方法

客户端和前端定义scheme规范,前端加载scheme,客户端会拦截scheme,如果scheme格式符合规范,客户端会解析scheme中的参数,获取对应的方法和参数名,然后调起客户端原生方法。

1、前端加载scheme

通过iframe.src发起一个请求,客户端webview能拦截这个请求,做相应的处理。

Web 端发出请求的方式非常多样,例如 <a/>iframe.srclocation.hrefajax 等,但 <a/> 需要用户手动触发,location.href 可能会导致页面跳转,安卓端拦截 ajax 的能力有所欠缺,因此绝大多数拦截式实现方案均采用iframe 来发送请求

var iframe = document.createElement('iframe');  
iframe.style.width = '1px';  
iframe.style.height = '1px';  
iframe.style.display = 'none';  
iframe.src = 'jsbridge://getNetwork?callback=networkInfo';  
document.body.appendChild(iframe);
// 100毫秒后移除
setTimeout(function() {  
    iframe.remove();
}, 100)l

一个标准的 URL 由 <scheme>://<host>:<port><path> 组成,相信大家都有过从微信或手机浏览器点击某个链接意外跳转到其他 App 的经历,如果有仔细留意过这些链接的 URL 你会发现目前主流 App 都有其专属的一个 scheme 来作为该应用的标识,例如微信的 URL scheme 就是 weixin://

JSB 的实现借鉴这一思路,定制业务自身专属的一个 URL scheme 来作为 JSB 请求的标识,例如字节内部实现拦截式 JSB 的 SDK 中就定义了 bytedance:// 这样一个 scheme。

实际上这个跳转地址只是一个非法的不存在的url

2、android 重写shouldOverrideUrlLoading

Native 拦截请求的钩子方法:

平台 API
Android shouldOverrideUrlLoading
iOS 8+ decidePolicyForNavigationAction
iOS 8- shouldStartLoadWithRequest

在webview中重写shouldOverrideUrlLoading根据定义的scheme原则进行拦截

public boolean shouldOverrideUrlLoading(WebView view, String url) {
  if(isValidScheme()){
      //拦截处理scheme
      return handleScheme()
  } 
  return false;
}
复制代码

3、优缺点

优点:兼容性好,安卓和 IOS 的各个版本都能支持此功能。

缺点:调用时延比较高 200 - 400ms,在安卓上表现明显;URL scheme 长度有限,内容过多可能会丢失字符;不支持同步返回结果,所有信息传送都需要调用 iframe 请求,使用 callback 得到返回的数据。

一部分问题(缺点)的解决

  • 连续发送时消息丢失

以下代码:

window.location.href = "jsbridge://callNativeNslog?{"data": "111", "cbName": ""}";
window.location.href = "jsbridge://callNativeNslog?{"data": "222", "cbName": ""}";

js 此时的诉求是在同一个运行逻辑内,快速的连续发送出 2 个通讯请求,用客户端自己 IDE 的 log,按顺序打印 111,222,那么实际结果是 222 的通讯消息根本收不到,直接会被系统抛弃丢掉。

缘由:由于 h5 的请求归根结底是一种模拟跳转,跳转这件事情上 webview 会有限制,当 h5 连续发送多条跳转的时候,webview 会直接过滤掉后发的跳转请求,所以第二个消息根本收不到,想要收到怎么办?js 里将第二条消息延时一下。

//发第一条消息
location.href = "jsbridge://callNativeNslog?{"data": "111", "cbName": ""}";

//延时发送第二条消息
setTimeout(500,function(){
    location.href = "jsbridge://callNativeNslog?{"data": "222", "cbName": ""}";
});

但这并不能保证此时是否有其余地方经过这种方式进行请求,为系统解决此问题,js 端能够封装一层队列,全部 js 代码调用消息都先进入队列并不马上发送,而后 h5 会周期性好比 500 毫秒,清空一次队列,保证在很快的时间内绝对不会连续发 2 次请求通讯。

(3)JS回调时机

基本没用….因为以下方法ios都不支持,作了解就可以了

1、Confirm

客户端可以拦截confirm。iOS的UIWebview不支持(不过好像WKWebView 支持)。使用场景相对较多,不适合用来做jsb。

前端调用

window.confirm('dance://app.toast?title=hello')

2、alert

客户端可以拦截alert。iOS的UIWebview不支持。但是alert使用场景较多,不适合用来做jsb。

前端调用

window.alert('dance://app.toast?title=hello')

3、Prompt

客户端可以拦截prompt,参数同上。iOS的UIWebview不支持。可自定义返回值,多用于安卓jsb方案。

前端调用

const url = 'jsbridge://' + method + '?' + JSON.stringify(args);
window.prompt(url)

Android端拦截

/ Java: 重载 onJsPrompt 方法,提取 prompt 内容判断是否需要拦截
    class MyWebViewClient extends WebChromeClient {
      @Override
      public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        if (message.startsWith("bridge://")) {
          // 解析 // 后面的 action 和参数,调用相关的函数
          result.confirm("Yes!");
        }
        return true;
      }
    }
    webView.setWebViewClient(new MyWebViewClient());

(4)日志输出

客户端可以拦截console.log,参数同上。安卓和iOS通用。

1、前端调用

console.log('dance://app.toast?title=hello')

2、Android端拦截

WebView.setWebChromeClient(new WebChromeClient() {
  public void onConsoleMessage(String message, int lineNumber, String sourceID) {

    return true;
  }
});

调用与回调

跨端方法一般都不是单向的,一般调用完毕我们都需要执行回调啊、什么返回一个参数之类的,而执行 js 回调函数方式本质是 native 调用 h5 的 js 方法

但是由于调用方调用被调用方和调用方回调被调用方在代码层面都是同一个接口(你也可以了理解为同一个命名的方法),为了区分本次调用是 API 调用还是回调调用,sn 需要做区分,比如请求方带过去的 key 名叫 req_sn(callbackId),回调带过去的叫 res_sn(responseId)

以拦截式调用为例,发起带 req_sn 的请求前,如果有回调函数,会以 req_sn 的值为 key,把回调函数存到对应的回调函数列表。当接收到带有 res_sn 的调用后,会以 res_sn 的值为 key,从对应的回调函数列表取出回调函数,并以接收到的数据为参数,执行回调函数

分享一个JSBridge

An iOS/OSX bridge for sending messages between Obj-C and JavaScript in UIWebViews/WebViews:

它把 JSB封装的就像是一个跨越两端的 EventEmitter

https://github.com/marcuswestin/WebViewJavascriptBridge

3.RN跨端

传统的jsb方案下,使用Webview 作为是 JS 的执行引擎,而在使用RN情况下,RN 团队选择了JSCore(JSCore 的对外接口是用 C 和 C++ 编写的,C++ 在 iOS 和安卓系统上也有良好的跨端运行的功能)作为执行引擎,用来跑react代码(JS代码加载和解析),自带的java层作为渲染引擎

Native 模块(客户端原生模块)在 Android 系统下是 Java 模块,JS 通过模块 ID(moduleID) 和方法 ID(methodID) 来进行调用

JavaScript 模块是由 JS 实现

以安卓为例,JS 环境中会维护一份所有 Native 模块的 moduleID 和 methodID 的映射 NativeModules,用来调用 Native 模块的时候查找对应 ID;Java 环境中也会维护一份 JavaScript 模块的映射 JSModuleRegistry,用来调用 JS 代码。而在实际的代码中,Native 模块和 JS 模块的通信需要通过中间层也就是 C++ 层的过渡,也就是说 Native 模块和 JS 模块实际上都只是在和 C++ 模块进行通信。

RN环境中会塞入4个api作为调用js的入口:

callFunctionReturnFlushedQueue // 让 C++ 调用 JS 模块
invokeCallbackAndReturnFlushedQueue // 让 C++ 调用 JS 回调
flushedQueue // 清空 JS 任务队列
callFunctionReturnResultAndFlushedQueue // 让 C++ 调用 JS 模块并返回结果

JS 还在 global 中还设置了 __fbGenNativeModule 方法,用来给 C++ 调用后在 JS 环境生成 Java 模块的映射对象,也就是 NativeModules 模块。

数据结构类似于(跟实际的数据结构有偏差):

{
    "Timing": {
        "moduleID": "1001",
        "method": {
            "createTimer": {
                "methodID": "10001"
            }
        }
    }
}

其中moduleIDmethodID 会映射 native方法

同样的,C++ 通过 JSCore 的 JSObjectSetProperty 方法在 global 对象中塞入了几个 Native API,让 JS 能通过它们来调用 C++ 模块

nativeFlushQueueImmediate // 立即清空 JS 任务队列
nativeCallSyncHook // 同步调用 Native 方法
nativeRequire  // 加载 Native 模块

Java 跟 C++ 的互相调用通过 JNI,通过 JNI,C++ 层会暴露出来一些 API 来给 Java 层调用

参考:

https://zhuanlan.zhihu.com/p/473710695

https://juejin.cn/post/7097845525587689486


文章作者: Hello
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hello !
 上一篇
服务器与部署 服务器与部署
1.部署方式利用脚手架打包后得到dist文件 将自己电脑作为服务器,部署本地 :window -> 使用nginx或者tomcat 远程主机,进行部署:linux centos -> nginx 如 注意 :使用nginx时,关
2021-03-08
下一篇 
Vue(中) Vue(中)
4.Vue CLI在开发大型项目的时候,则必须使用到Vue CLI,CLI是Command-Line Interface ,翻译为命令行界面,俗称脚手架 使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置(终于不用
2021-03-01
  目录