【重要】专为你定制的 JS/Natvie 交互专题

1,659 阅读6分钟

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

自己动手实现一个 Hybrid WebView

如今,端与 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 页面

整体页面是如上图所示。我们逐步剖析是如何实现的。

在前面的章节中(这些章节后续会发出来),已经介绍了在 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("确定要保存该图片吗?") 到端,端会执行 WKUIDelegate 代理方法,我们在这个方法需要弹窗端内的提示框:

- (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 会在这个专题完成后一块放出。