Hybrid
- 定义:混合开发,半原生半web开发
- 基本原理:通过JSBridge,H5页面可以调用Native的api,Native也可调用H5页面的方法或者通知H5页面回调。
- 优点:
- 开发成本低,可以跨平台,维护成本低,功能可复用
- 缺点:
- 性能消耗大,不适用交互性较强的app
JSBridge(Native与JS的通信桥梁)
原理: H5-->通过某种方式触发一个url-->Native捕获到url,进行分析-->原生做处理-->Native调用H5的JSBridge对象传递回调。
JavaScript 调用 Native
JavaScript 调用 Native 的方式, 主要有两种:
- 注入JS API
- 拦截 URL Schema。
注入API
原理:通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
对于 iOS 的 UIWebView,实例如下:
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
// Native 逻辑
};
前端调用方式:
window.postBridgeMessage(message);
对于 iOS 的 WKWebView 可以用以下方式:
@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 逻辑
}
}
前端调用方式:
window.webkit.messageHandlers.nativeBridge.postMessage(message);
对于 Android 可以采用下面的方式:
publicclassJavaScriptInterfaceDemoActivityextendsActivity{
private WebView Wv;
@Override
publicvoidonCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Wv = (WebView)findViewById(R.id.webView);
final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this);
Wv.getSettings().setJavaScriptEnabled(true);
Wv.addJavascriptInterface(myJavaScriptInterface, "nativeBridge");
// TODO 显示 WebView
}
publicclassJavaScriptInterface{
Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
publicvoidpostMessage(String webMessage){
// Native 逻辑
}
}
}
前端调用方式:
window.nativeBridge.postMessage(message);
拦截 URL Schema
先解释一下 URL SCHEME:URL Schema是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的,例如: qunarhy://hy/url?url=ymfe.tech,protocol 是 qunarhy,host 则是 hy。
流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native拦截到请求并根据URL Scheme(包括所带的参数)进行相关操作。
在时间过程中,这种方式有一定的 缺陷:
使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患。
创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。 但是之前为什么很多方案使用这种方式呢?因为它 支持 iOS6。而现在的大环境下,iOS6 占比很小,基本上可以忽略,所以并不推荐为了 iOS6 使用这种 并不优雅 的方式。
【注】:有些方案为了规避 url 长度隐患的缺陷,在 iOS 上采用了使用 Ajax 发送同域请求的方式,并将参数放到 head 或 body 里。这样,虽然规避了 url 长度的隐患,但是 WKWebView 并不支持这样的方式。
【注2】:为什么选择 iframe.src 不选择 locaiton.href ?因为如果通过 location.href 连续调用 Native,很容易丢失一些调用。
Native 调用 JavaScript
相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单,毕竟不管是 iOS 的 UIWebView 还是 WKWebView,还是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 即可。
Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。(闭包里的方法,JavaScript 自己都调用不了,更不用想让 Native 去调用了)
对于 iOS 的 UIWebView,示例如下:
result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];
对于 iOS 的 WKWebView,示例如下:
[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];
对于 Android,在 Kitkat(4.4)之前并没有提供 iOS 类似的调用方式,只能用 loadUrl 一段 JavaScript 代码,来实现:
webView.loadUrl("javascript:" + javaScriptString);
而 Kitkat 之后的版本,也可以用 evaluateJavascript 方法实现:
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
publicvoidonReceiveValue(String value){
}
});
【注】:使用 loadUrl 的方式,并不能获取 JavaScript 执行后的结果。
通信原理小总结
通信原理是 JSBridge 实现的核心,实现方式可以各种各样.
-
JavaScript 调用 Native 推荐使用 注入 API 的方式
-
Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。
JSBridge 接口实现
JSBridge 的接口主要功能有两个:调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息)。因此,JSBridge 可以设计如下:window.JSBridge = {
// 调用 Native
invoke: function(msg) {
// 判断环境,获取不同的 nativeBridge
nativeBridge.postMessage(msg);
},
receiveMessage: function(msg) {
// 处理 msg
}
};
在上面的文章中,提到过 RPC 中有一个非常重要的环节是 句柄解析调用 ,这点在 JSBridge 中体现为 句柄与功能对应关系。同时,我们将句柄抽象为 桥名(BridgeName),最终演化为 一个 BridgeName 对应一个 Native 功能或者一类 Native 消息。 基于此点,JSBridge 的实现可以优化为如下:
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, data) {
// 判断环境,获取不同的 nativeBridge
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {}
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {};
// 具体逻辑
}
};
JSBridge 大概的雏形出现了。现在终于可以着手解决这个问题了:消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的? 对于 JSBridge 的 Callback ,其实就是 RPC 框架的回调机制。当然也可以用更简单的 JSONP 机制解释:
当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。
由此可见,callback 参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge:用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。
(function () {
var id = 0,
callbacks = {};
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 传到 Native 端
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId; // Native 将 callbackId 原封不动传回
// 具体逻辑
// bridgeName 和 callbackId 不会同时存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相应句柄
callbacks[callbackId](msg.data); // 执行调用
}
} elseif (bridgeName) {
}
}
};
})();
最后用同样的方式加上 Native 调用的回调逻辑,同时对代码进行一些优化,就大概实现了一个功能比较完整的 JSBridge。其代码如下:
(function () {
var id = 0,
callbacks = {},
registerFuncs = {};
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
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
});
}
}
}
},
register: function(bridgeName, callback) {
if (!registerFuncs[bridgeName]) {
registerFuncs[bridgeName] = [];
}
registerFuncs[bridgeName].push(callback); // 存储回调
}
};
})();
当然,这段代码片段只是一个示例,主要用于剖析 JSBridge 的原理和流程,里面存在诸多省略和不完善的代码逻辑,读者们可以自行完善。 【注】:这一节主要讲的是,JavaScript 端的 JSBridge 的实现,对于 Native 端涉及的并不多。在 Native 端配合实现 JSBridge 的 JavaScript 调用 Native 逻辑也很简单,主要的代码逻辑是:接收到 JavaScript 消息 => 解析参数,拿到 bridgeName、data 和 callbackId => 根据 bridgeName 找到功能方法,以 data 为参数执行 => 执行返回值和 callbackId 一起回传前端。 Native 调用 JavaScript 也同样简单,直接自动生成一个唯一的 ResponseId,并存储句柄,然后和 data 一起发送给前端即可。
JSBridge 如何引用
- 由 Native 端进行注入
注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。 它的优点在于:桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容;与此同时,它的缺点是:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先判断 JSBridge 是否已经注入成功。
- 由 JavaScript 端引用
直接与 JavaScript 一起执行。 与由 Native 端注入正好相反,它的优点在于:JavaScript 端可以确定 JSBridge 的存在,直接调用即可;缺点是:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。
JSBridge实现思路
- 1.设计出一个Native与JS交互的全局桥对象
- JS和Native之间的通信必须通过一个H5全局对象JSbridge实现
- 该对象名为JSBridge,是H5页面中全局对象window的属性
var JSBridge = weindow.JSBridge || (window.JSBridge = {})
- 该对象有三个方法:1:registerHandler(String,Function)H5调用注册本地JS方法,注册后Native可通过JSBridge
- 2.JS如何调用Native
- 3.Native如何得知api被调用
- 4.分析url-参数和回调的格式
- 5.Native如何调用JS
- 6.h5中api方法的注册以及格式