WKWeb​View

iOS 与 web 之间的关系非常复杂,这种复杂关系甚至可以追溯到几十年前系统建立初期。

其实现在很难说清第一代 iPhone 横空出世是一件多么困难的事情。我们现今司空见惯的触摸屏在当时只是诸多方案中的一种。最早期的产品原型是物理键盘、触摸屏、触控笔的结合,屏幕尺寸才是 5” x 7”。甚至当时 iPod 的轮子都是一个严肃的备选方案。

但最最重要的决定或许都是由软件而非硬件决定的。

iPhone 应该如何运行软件呢?像 OS X 上的应用程序,或者 web 页面,以及 Safari 都应该如何运行起来呢?仿照 OS X 去构建 iPhone OS 的方法已经广泛地被大家熟知了,这种方法到今天也留下了不少争议。

我们来回忆一下 Steve Jobs 2007 年 WWDC keynote 上这句臭名昭著的话:

iPhone 包含了整个 Safari 引擎。因此,你可以创作应用 Ajax 技术的 Web 2.0 应用,表现上和使用上都和 iPhone 原生应用一模一样。这些应用可以和 iPhone 的各种服务完美地集成到一起:这些应用可以有打电话的功能,可以发邮件,可以在 Google Maps 上寻找地标。

Web 一直是 iOS 系统上的二级公民_(讽刺的是,其实现今移动网页响应式设计的出现大多是 iPhone 推动的)_。UIWebView 笨重难用,还有内存泄漏,和 Nirtro JavaScript 引擎谈笑风生的 Safari 不知道要比它高到哪里去了。

然而,这所有的一切都会因为 WKWebViewWebKit 框架其他部分的出现而发生改变。


WKWebView 是现代 WebKit API 在 iOS 8 和 OS X Yosemite 应用中的核心部分。它代替了 UIKit 中的 UIWebView 和 AppKit 中的 WebView,提供了统一的跨双平台 API。

自诩拥有 60fps 滚动刷新率、内置手势、高效的 app 和 web 信息交换通道、和 Safari 相同的 JavaScript 引擎,WKWebView 毫无疑问地成为了 WWDC 2014 上的最亮点。

UIWebView & UIWebViewDelegate 这个两个东西是如何在 WKWebKit 中被重构成 14 个类 3 个协议的呢。虽然这次的变化确实带来了不少的新功能,但请一定不要因此感到恐慌!

WKWebKit Framework

Classes

  • WKBackForwardList: 之前访问过的 web 页面的列表,可以通过后退和前进动作来访问到。
    • WKBackForwardListItem: webview 中后退列表里的某一个网页。
  • WKFrameInfo: 包含一个网页的布局信息。
  • WKNavigation: 包含一个网页的加载进度信息。
    • WKNavigationAction: 包含可能让网页导航变化的信息,用于判断是否做出导航变化。
    • WKNavigationResponse: 包含可能让网页导航变化的返回内容信息,用于判断是否做出导航变化。
  • WKPreferences: 概括一个 webview 的偏好设置。
  • WKProcessPool: 表示一个 web 内容加载池。
  • WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
    • WKScriptMessage: 包含网页发出的信息。
    • WKUserScript: 表示可以被网页接受的用户脚本。
      • WKWebViewConfiguration: 初始化 webview 的设置。
  • WKWindowFeatures: 指定加载新网页时的窗口属性。

Protocols

  • WKNavigationDelegate: 提供了追踪主窗口网页加载过程和判断主窗口和子窗口是否进行页面加载新页面的相关方法。
  • WKScriptMessageHandler: 提供从网页中收消息的回调方法。
  • WKUIDelegate: 提供用原生控件显示网页的方法回调。

UIWebViewWKWebView 的 API 区别

WKWebView 继承了 UIWebView 大部分的接口,这让 app 来继承 WKWebKit 也简单了许多(同时随着更新 iOS 8 的越来越多这也成为了某种必需)。

有兴趣的同学可以看一下这两个类的 API 区别:

UIWebView WKWebView
var scrollView: UIScrollView! { get } var scrollView: UIScrollView! { get }
  var configuration: WKWebViewConfiguration! { get }
var delegate: UIWebViewDelegate! var UIDelegate: WKUIDelegate!
  var navigationDelegate: WKNavigationDelegate!
  var backForwardList: WKBackForwardList! { get }

页面加载

UIWebView WKWebView
func loadRequest(request: NSURLRequest!) func loadRequest(request: NSURLRequest!) -> WKNavigation!
func loadHTMLString(string: String!, baseURL: NSURL!) func loadHTMLString(string: String!, baseURL: NSURL!) -> WKNavigation!
func loadData(data: NSData!, MIMEType: String!, textEncodingName: String!, baseURL: NSURL!)  
  var estimatedProgress: Double { get }
  var hasOnlySecureContent: Bool { get }
func reload() func reload() -> WKNavigation!
  func reloadFromOrigin() -> WKNavigation!
func stopLoading() func stopLoading()
var request: NSURLRequest! { get }  
  var URL: NSURL! { get }
  var title: String! { get }

访问历史

UIWebView WKWebView
  func goToBackForwardListItem(item: WKBackForwardListItem!) -> WKNavigation!
func goBack() func goBack() -> WKNavigation!
func goForward() func goForward() -> WKNavigation!
var canGoBack: Bool { get } var canGoBack: Bool { get }
var canGoForward: Bool { get } var canGoForward: Bool { get }
var loading: Bool { get } var loading: Bool { get }

调用 Javascript

UIWebView WKWebView
func stringByEvaluatingJavaScriptFromString(script: String!) -> String!  
  func evaluateJavaScript(javaScriptString: String!, completionHandler: ((AnyObject!, NSError!) -> Void)!)

原生混用

UIWebView WKWebView
var keyboardDisplayRequiresUserAction: Bool  
var scalesPageToFit: Bool  
  var allowsBackForwardNavigationGestures: Bool

页码

WKWebView 目前缺少关于页码相关的 API。

  • var paginationMode: UIWebPaginationMode
  • var paginationBreakingMode: UIWebPaginationBreakingMode
  • var pageLength: CGFloat
  • var gapBetweenPages: CGFloat
  • var pageCount: Int { get }

重构分离开的 WKWebViewConfiguration

下面这些 UIWebView 的属性被重构进了在初始化 WKWebView 传入的设置对象:

  • var allowsInlineMediaPlayback: Bool
  • var mediaPlaybackRequiresUserAction: Bool
  • var mediaPlaybackAllowsAirPlay: Bool
  • var suppressesIncrementalRendering: Bool

JavaScript ↔︎ Swift 对话机制

相对于 UIWebView 最大的提升就是数据在可以 app 和 web 内容之间传递。

使用用户脚本来注入 JavaScript

WKUserScript 允许在正文加载之前或之后注入到页面中。这个强大的功能允许在页面中以安全且唯一的方式操作网页内容。

一个简单的例子如下,用户改变背景的用户脚本被插入到网页中:

let source = "document.body.style.background = \"#777\";"
let userScript = WKUserScript(source: source, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)

let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)

let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
self.webView = WKWebView(frame: self.view.bounds, configuration: configuration)

WKUserScript 对象可以以 JavaScript 源码形式初始化,初始化时还可以传入是在加载之前还是结束时注入,以及脚本影响的是这个布局还是仅主要布局。于是用户脚本被加入到 WKUserContentController 中,并且以 WKWebViewConfiguration 属性传入到 WKWebView 的初始化过程中。

这个样例可以简单扩展为更为高级的页面修改方法,例如去除广告、隐藏评论等,更复杂的样例见此:让所有出现的”the cloud”变为”my butt”

马杀鸡 魂斗罗(Message Handlers 抱歉我憋了一天,真的不会翻译)

web 和 app 通讯机制也通过 message handler 有很大提升。

就想在Safari 审查元素功能中的 console.log 能在调试终端打印信息一样,网页中的信息也可以通过调用这个函数被传到 app 里:

window.webkit.messageHandlers.{NAME}.postMessage()

这个 API 真正神奇的地方在于 JavaScript 对象可以_自动转换_为 Objective-C 或 Swift 对象。

Handler 的名字可以通过 WKScriptMessageHandler 协议中的 addScriptMessageHandler() 接口函数设置:

class NotificationScriptMessageHandler: NSObject, WKScriptMessageHandler {
    func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage!) {
        println(message.body)
    }
}

let userContentController = WKUserContentController()
let handler = NotificationScriptMessageHandler()
userContentController.addScriptMessageHandler(handler, name: "notification")

于是当通知进入 app 的时候,比如说在页面中创建一个新对象,相关信息就可以这样传递:

window.webkit.messageHandlers.notification.postMessage({body: "..."});

添加用户脚本来对 web 事件监听并用 Message Handler 将信息传回 app。

同样的方法也可以用来收集页面信息用于 app 的页面展示或数据分析。

例如,如果某人要针对 NSHipster.com 做一个特别的浏览器,就可以加一个能够呼出相似文章列表的按钮:

// document.location.href == "https://nshipster.com/webkit"
function getRelatedArticles() {
    var related = [];
    var elements = document.getElementById("related").getElementsByTagName("a");
    for (i = 0; i < elements.length; i++) {
        var a = elements[i];
        related.push({href: a.href, title: a.title});
    }

    window.webkit.messageHandlers.related.postMessage({articles: related});
}

如果你的 app 只是对网页内容做了很简单的一层包装,那么 WKWebView 可以彻底改变这种状况。你对于性能和兼容性的所有愿望都将变为现实。所有你想要的东西都在这了。

如果你是一个原生纯粹主义者,你可能会被 iOS 8 新带来强大和扩展性功能吓到。有一个秘密就是,例如 Messages 这种原生应用都应用了 WebKit 来渲染复杂的页面元素。你可能尚且没有意识到,但事实是,webview 在移动开发最佳实践中应该得到一席之地。

作者 Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon. He is the founder of NSHipster and Flight School, and the creator of several open source libraries, including AFNetworking and Alamofire.

翻译者
Croath Liu

云游四海的工程师

下一篇文章

协议是 Swift 当中泛型实现的基础,然而 Swift 中却缺少内建的提供方法默认实现的机制。不过仍然有一种办法可以解决这个问题,这个办法之前你可能没有留意到。