一、一些基础
定义:Hybrid是半Native半web开发模式 Hybrid模式中,底层功能API均由原生容器通过某种方式提供,然后业务逻辑由H5页面完成,最终原生容器加载H5页面,完成整个App
实现过程:前端做好静态页面打包成文件交给客户端,客户端拿到前端静态页面,以文件形式存储在app中。客户端在一个webview中,使用file协议加载静态页面。
版本更新:客户端每次启动,都去server端检查版本号。如果server端版本号大于客户端版本号,就去下载最新的zip包,下载完之后解压包,然后将现有文件覆盖。
webview:是app中的一个组件,app可以有webview,也可以没有。用于加载H5页面等,即一个小型的浏览器内核,这里是一个统称。iOS 的是 UIWebView 或 WKWebView,Android 的是WebView。
二、传统JavaScript调用Native
注入 API
注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到JavaScript 调用 Native 的目的
对于 iOS 的 UIWebView,实例如下:
#import <JavaScriptCore/JavaScriptCore.h> //引入官方的库文件
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
// Native 逻辑
};
前端调用方式:
window.postBridgeMessage(message);
ios7才出现官方提供的第三方库”JavaScriptCore”,即可开放api给JS调用。
拦截 URL scheme
URL SCHEME是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的。
例如:weixin://dl/scan 扫一扫
拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。
demo: https://github.com/liangyuqi/hybrid-weixin(只是演示,无法正常运行,微信有严格的权限验证,外部页面不能随意使用schema)
优点:支持ios6
缺点:1.url长度限制,2.比起注入更为耗时
三、传统Native调用Javascript
Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。
对于 iOS 的 UIWebView,示例如下:
result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];
对于 iOS 的 WKWebView,示例如下:
[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];
对于 Android,在 Kitkat(4.4)之前并没有提供 iOS 类似的调用方式,只能用 loadUrl 一段 JavaScript 代码,来实现:
webView.loadUrl("javascript: 方法名('参数,需要转为字符串')");
而 Kitkat 之后的版本,也可以用 evaluateJavascript 方法实现:
webView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value){
//这里的value即为对应JS方法的返回值
}
});
- 4.4之前Native通过loadUrl来调用JS方法,只能让某个JS方法执行,但是无法获取该方法的返回值
- 4.4之后,通过evaluateJavascript异步调用JS方法,并且能在onReceiveValue中拿到返回值
- 不适合传输大量数据(大量数据建议用接口方式获取)
- webView.loadUrl(“javascript: 方法名(‘参数,需要转为字符串’)”);函数需在UI线程运行,因为webView为UI控件(但是有一个坏处是会阻塞UI线程)
四、关于JSBridge
定义:JSBridge就是定义Native和JS的通信,Native只通过一个固定的桥对象调用JS,JS也只通过固定的桥对象调用Native。
核心是 构建 Native 和非 Native 间消息通信的通道,而且是 双向通信的通道。
这里实现一下注入api的方法,可以理解为JSBridge是一种交互理念,之前提过的url scheme也是其中的一种实现,JSBridge交互的一个很大特点就是便于拓展,而且没有重大的安全性问题,所以也就是为什么它广为流行。
JSBridge 的接口主要功能有两个:调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息)。
一个 BridgeName(桥名)对应一个 Native 功能或者一类 Native 消息。
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, data) {
// 判断环境,获取不同的 nativeBridge
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {}
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {};
// 具体逻辑
}
};
简单雏形有了,但是消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的?
类似jsonp请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。
用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。
(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); // 存储回调
}
};
})();