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 回调名
ModuleId
和 MethodId
能定位到具体调用的原生方法,params
参数作为原生方法调用的参数,最后通过 CallbackId
回调 JS 的回调函数(我们可以自行脑补react中父子组件通过回调通信的方式)
在使用 H5 的情况下,Webview 是 JS 的执行引擎,同时 Webview 还是页面的渲染引擎。
实现JSBridge主要是两点:
- 将Native端原生接口封装成JavaScript接口
- 将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.src
、location.href
、ajax
等,但 <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:
- Facebook Messenger
- Facebook Paper
- 等都有在使用
它把 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"
}
}
}
}
其中moduleID和
methodID 会映射 native方法
同样的,C++ 通过 JSCore 的 JSObjectSetProperty
方法在 global 对象中塞入了几个 Native API,让 JS 能通过它们来调用 C++ 模块
nativeFlushQueueImmediate // 立即清空 JS 任务队列
nativeCallSyncHook // 同步调用 Native 方法
nativeRequire // 加载 Native 模块
Java 跟 C++ 的互相调用通过 JNI,通过 JNI,C++ 层会暴露出来一些 API 来给 Java 层调用
参考: