Explore WKWebView additions
System & Services 进阶 24m

探索 WKWebView 的新增功能

Explore WKWebView additions

2021年6月11日

在 Apple 官方观看视频

一句话判断

WKWebView 在 iOS 15 终于支持了 Find Interaction 和同步 JavaScript 调用——这两个功能如果你做过内嵌浏览器的 app,就知道它们有多刚需。

这场 Session 讲了什么

WKWebView 是 iOS app 中嵌入 Web 内容的标准组件。iOS 15 对它做了多项增强,覆盖了查找交互、JavaScript 互操作、内容安全和媒体能力四个方面。

Find Interaction 是最显眼的改进。WKWebView 现在原生支持了页面内搜索(Find on Page)功能,使用系统标准的 UIFindInteraction API。之前要实现这个功能需要自己写 JavaScript 注入方案,现在只需要设置 isFindInteractionEnabled = true 就能使用系统级的搜索 UI。

JavaScript 互操作方面,WKWebView 新增了 callAsyncJavaScript 方法,可以在一个调用中执行一段异步 JavaScript 并等待结果。之前要执行异步 JS 代码需要通过 message handler 回传结果,流程很繁琐。

媒体方面,WKWebView 现在支持了 Media Capture API,Web 页面可以直接访问摄像头和麦克风(在用户授权后)。PIcture in Picture 也得到了增强。

值得深挖的点

callAsyncJavaScript 的安全模型

callAsyncJavaScript 方法接收一段 JavaScript 代码字符串,在页面上下文中执行,返回一个 Any 类型的结果。和旧的 evaluateJavaScript 不同,它支持 async/await 语法——你的 JavaScript 可以包含 await 关键字,Swift 端会等到 Promise resolve 后再返回。

但要注意安全性:这段代码运行在页面的 JavaScript 上下文中,可以访问页面的所有 DOM 和全局变量。如果你的 app 加载的是不可信的网页,不应该在 callAsyncJavaScript 中传入包含敏感信息的代码。

UIFindInteraction 的自定义

UIFindInteraction 提供了标准的搜索栏 UI(类似 Safari 的 Find on Page),但你也可以通过 UIFindInteractionDelegate 来自定义搜索行为。比如限制只搜索特定 DOM 节点内的文本,或者在搜索结果高亮时添加自定义样式。

代码片段

// 启用 WKWebView 的页面搜索功能
import WebKit

class WebViewController: UIViewController {
    var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let config = WKWebViewConfiguration()
        webView = WKWebView(frame: view.bounds, configuration: config)
        
        // 启用 Find Interaction
        webView.isFindInteractionEnabled = true
        
        // 添加搜索按钮
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .search,
            target: self,
            action: #selector(showFind)
        )
        
        view.addSubview(webView)
        webView.load(URLRequest(url: URL(string: "https://example.com")!))
    }
    
    @objc func showFind() {
        // 调用系统的搜索 UI
        guard let findInteraction = webView.findInteraction else { return }
        findInteraction.presentFindNavigator(showingReplace: false)
    }
}
// 使用 callAsyncJavaScript 执行异步 JS 代码
func extractPageData() async throws -> [String: String] {
    let result = try await webView.callAsyncJavaScript(
        """
        // 这段 JS 代码可以使用 async/await
        const response = await fetch('/api/metadata');
        const data = await response.json();
        return {
            title: document.title,
            description: data.description,
            url: window.location.href
        };
        """
    )
    
    // result 是 JS 返回值的 Swift 表示
    if let dict = result as? [String: String] {
        return dict
    }
    return [:]
}
// 配置摄像头和麦克风权限
let config = WKWebViewConfiguration()

// 允许网页请求媒体设备访问
config.preferences.elementFullscreenEnabled = true

// 通过 WKUserContentScript 注入自定义媒体权限处理
let script = WKUserScript(
    source: """
    navigator.mediaDevices.getUserMedia({ video: true })
        .then(stream => {
            window.webkit.messageHandlers.cameraGranted.postMessage('ok');
        })
        .catch(err => {
            window.webkit.messageHandlers.cameraGranted.postMessage('denied');
        });
    """,
    injectionTime: .atDocumentEnd,
    forMainFrameOnly: true
)

config.userContentController.addUserScript(script)
config.userContentController.add(self, name: "cameraGranted")

最佳实践

如果你的 app 需要支持页面搜索,isFindInteractionEnabled 是最简单的方案,但 UI 是系统固定的样式。如果需要自定义搜索 UI,可以用 webView.find(_:configuration:completionHandler:) 方法手动执行搜索,然后自己渲染搜索结果。

callAsyncJavaScript 的返回值类型映射遵循 JSON 规则:JS 的 number 映射为 NSNumberstring 映射为 Stringobject 映射为 Dictionary。如果你的 JS 返回了 undefined,Swift 端会收到 nil

还有什么值得关注

  • WKWebView 现在支持 contentWorld 隔离,可以在独立的 JavaScript 环境中执行代码,避免与页面脚本冲突。
  • 新增了 mediaType 属性,可以强制 WebView 以打印媒体类型渲染页面,方便生成打印友好的预览。
  • WKWebViewConfigurationupgradeKnownHostsToHTTPS 默认开启,会自动将已知的 HTTP 请求升级为 HTTPS。
WWDC 2021