2018 年距离第一代 iOS 系统发布(2007 年)已经过去 11 年,这 11 年中移动端日益成熟,Web 端的时代逐步转移到了移动端,自然而然 Web 端的开发技术栈开始逐步移动到移动端。这就引发一个尴尬的局面,Web 端的同学不了解移动端的开发知识,移动端不了解 Web 端的开发知识。为了解决这个问题,知识小集打算从基础出发,介绍 JavaScript 与 iOS 交互时用到的技术点,比如 JavaScriptCore、JavaScript 基础、JavaScriptCore 的实际使用场景(深度剖析 JSPatch 的实现)等。而今天这篇就是其中的一篇,主要介绍一个 Hybrid WebView 的实现。接下来我们会把文章逐步发出来供我们的读者朋友参考,我们初步定的目录如下(如果你不想错过我们这个专题,关注我们的公众号【知识小集】吧,关于这个专题有什么建议都可以通过公众号告诉我们):
前言
JavaScript 基础知识
JavaScript 进阶
JavaScript-native 调试
开启本地 Webserver
WKWebView 概述
JavaScriptCore 总览
JavaScript 与 ObjectiveC 间的类型转换
JavaScript 与 ObjectiveC 通信
ObjectiveC 与 JavaScript 通信
自己动手实现一个 Hybrid WebView
JSPatch 中的 JavaScriptCore
JSPatch 中的 Runtime
JSPatch 原理深度剖析
JSPatch 杂谈
读 Aspects 理解 runtime
如今,端与 Web 页的交互越来越频繁,很多页面都交给 Web 页面来实现,而有些情况下 Web 需要与端进行交互。面对这种需求,各种第三方库源源不断出现,而 WebViewJavascriptBridge 无疑是 star 最多的一个。其实目前在 iOS 开发当中,大多数都切换到了 WKWebView,且对 Web 的交互越来越重,所以不妨自己实现一个 Hybrid WebView 来满足自己的业务需求。一个 Hybrid WebView 最基本的应该满足双方可以自由通信。
WebView 上的事件可以传递到端上;
WebView 可以从端上获取数据;
端可以监听到 WebView 上发生的事件。
本文旨在说明一个 Hybrid WebView 需要的技术手段,所以打算从一个具体的需求出发,一步一步搭建一个 Hybrid WebView。大多数的文章只会讲解端上如何实现,而本文会结合前端一块讲讲两端是如何实现的。
Web 页面上有一张图和一个保存按钮,当点击保存按钮时会提示用户是否需要保存图片到相册。如果保存成功,按钮的标题将变为已保存,否则标题为保存到相册。如果已保存,下次进入 Web 页时显示已保存。
分析上面的需求,可以拆分为:
页面加载后,需要获取图片是否已经保存过,如果已保存,按钮的标题为“已保存”,否则为“保存到相册”;
点击按钮需要提示用户“是否需要保存图片到相册”,点击“保存”执行保存操作。点击取消将什么也不做;
保存成功,按钮上的标题需要变为“已保存”。
分析完上面具体需求后,转换为技术需要考虑的问题:
页面加载后,Web 页可以从端上获取到图片是否已经保存的状态;
点击保存按钮,需要在端上提示用户,用户点击保存需要把图片保存到相册,这时需要获取到当前显示的图片,也就是说需要把 Web 页面中的数据传递到端;
保存成功后需要修改 Web 页面按钮的标题。
整体页面是如上图所示。我们逐步剖析是如何实现的。
在前面的章节中(这些章节后续会发出来),已经介绍了在 Web 页面中执行 JavaScript 。可以把一段 JavaScript 代码嵌入到 HTML 中,这时在 HTML 中可以直接调用 JavaScript 代码,而 JavaScript 可以通过 DOM 动态来操作 HTML 中的标签,这样即可以达到动态修改 Web 页的目的。
Web与端通信的JS代码,这段代码是嵌入在 HTML 中的。
<script>
// 标记保存的状态
var saved = false;
// 保存事件
function saveaction(){
if (saved) {
return;
}
alert("确定要保存该图片吗?");
// 发送消息给客户端 JS 中发送消息给 OC
var param = {url : "https://raw.githubusercontent.com/iOS-Tips/iOS-tech-set/master/images/qrcode.jpg"};
window.webkit.messageHandlers.JSBridge.postMessage(JSON.stringify(param));
};
// 保存成功后端会调用这个方法通知Web页保存成功
function save_success(){
change_state(true);
};
// 修改是否已保存的状态,修改按钮标题
function change_state(issaved){
saved = issaved;
var button = document.getElementById('saveid');
if (issaved){
// 如果已经保存,修改按钮的标题为已保存,否则显示 保存到相册
button.innerText = "已保存";
} else {
button.innerText = "保存到相册";
}
}
</script>
保存到相册 按钮,监听点击事件,当点击按钮后会调用 saveaction
函数。
<div id="saveid" class="save_button" onclick="saveaction()">保存到相册</div>
而 saveaction
函数首先会发一个 alert("确定要保存该图片吗?")
到端,端会执行 UINavigationBarDelegate
代理方法,我们在这个方法中需要弹出端内的提示框:
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"保存" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
self.isOKAction = YES;
completionHandler();
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
self.isOKAction = NO;
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
当用户点击保存按钮后,会保存图片到相册。所以客户端需要拿到图片的地址,这是需要给端发送图片的地址。如果想给端发送一条消息,直接在 Web 页通过 JavaScript 执行,其中 xxxx 是端与Web之间约定的名字。
window.webkit.messageHandlers.xxxx.postMessage(JSON.stringify(param))
而我们此时定义的名字是 JSBridge
,当用户点击保存后,需要根据Web传递过来的 URL 保存图片。
var param = {url : "https://raw.githubusercontent.com/iOS-Tips/iOS-tech-set/master/images/qrcode.jpg"};
window.webkit.messageHandlers.JSBridge.postMessage(JSON.stringify(param));
当端接收到 Web 发过来的消息后,会调用 WKScriptMessageHandler
的代理方法,在这个方法中我们来下载图片并保存到相册:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.body isKindOfClass:[NSString class]]) {
if ([message.name isEqualToString:kScriptMsgName] && self.isOKAction) {
// 保存图片
NSDictionary *msgInfo = [NSJSONSerialization JSONObjectWithData:[message.body dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:msgInfo[@"url"]]]];
if (image) {
UIImageWriteToSavedPhotosAlbum(image, self, @selector(imageSavedToPhotosAlbum:didFinishSavingWithError:contextInfo:), nil);
}
}
}
}
当把图片保存到相册后,需要刷新 Web 页面上的按钮的标题,这时需要执行 Web 页中已经定义好的 change_state
方法:
- (void)updateSaveState:(BOOL)isSave
{
NSString *script = isSave ? @"change_state(true);" : @"change_state(false);";
[self.webView evaluateJavaScript:script completionHandler:^(id _Nullable msg, NSError * _Nullable error) {}];
}
至此,我们还剩下最后一件事没有完成,当加载出 WebView 后,需要根据本地是否已经保存了图片更新按钮的标题,直接调用 updateSaveState
函数即可。
本文主要介绍一个 Hybrid WebView 如何实现,它仅仅是从一个具体的需求出发,而如果做一个通用 Hybrid WebView 框架需要两端设计一种通信规则。具体细节可以参考味精的两篇关于 Hybrid 的实践 (从零收拾一个hybrid框架)。本文的 demo 会在这个专题完成后一块放出。