探索 WKWebView 的新增强功能
Discover WKWebView enhancements
2020年6月25日
一句话判断
WKWebView 在 iOS 14 终于变成了一等公民——新增的 WKWebpagePreference、内容世界隔离和安全的 JavaScript 注入机制,让它从”嵌入浏览器”进化为”可靠的混合开发组件”。
这场 Session 讲了什么
WKWebView 在 iOS 14 和 macOS Big Sur 中获得了一系列重要更新。Session 覆盖了三个主要方向:内容安全策略的增强、JavaScript 交互的改进、以及新的 Web 内容配置选项。
最显著的变化是 WKContentWorld 的引入。这是一个新的安全边界概念,将 JavaScript 执行环境分为三个层级:page(网页原生脚本环境)、defaultClient(应用注入脚本的默认环境)和自定义命名空间。这个设计解决了一个长期痛点:应用注入的 JavaScript 和网页自身的 JavaScript 可能产生命名冲突和变量污染。现在,你可以在隔离的 content world 中执行脚本,完全避免与网页代码的干扰。
Session 还介绍了 WKWebpagePreference,允许开发者针对特定域名配置 Web 内容的渲染策略。比如你可以设置某些可信域名为”推荐使用 Safari 渲染”,而其他域名则使用更保守的策略。这对混合应用(Hybrid App)的安全策略配置非常有用。
值得深挖的点
WKContentWorld 的隔离机制
WKContentWorld.defaultClient 是应用注入脚本的推荐执行环境。在这个 world 中执行的 JavaScript 无法访问 page world 中的变量,反之亦然。两个 world 之间的通信只能通过 WKScriptMessage 进行。这就像浏览器中的跨域隔离——你不用担心注入的脚本和网页脚本互相干扰。Session 强烈建议所有应用注入脚本都使用 defaultClient world,不再直接使用 page world。
JavaScript 求值的异步回调改进
新的 callAsyncJavaScript 方法让你可以从 Swift 直接调用网页中的异步 JavaScript 函数,并获取 Promise 的解析结果。以前要实现这个功能需要手动通过 WKScriptMessage 桥接,现在只需一行代码。
代码片段
import WebKit
// 使用 WKContentWorld 安全地注入 JavaScript
class WebViewController: UIViewController {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// 配置 WKWebView
let config = WKWebViewConfiguration()
// 在 defaultClient world 中注入脚本(推荐方式)
let script = WKUserScript(
source: """
// 这个脚本运行在隔离的 content world 中
// 不会与网页自身的 JavaScript 冲突
var appBridge = {
sendToApp: function(message) {
window.webkit.messageHandlers.nativeBridge.postMessage(message);
},
getVersion: function() {
return "1.0.0";
}
};
""",
injectionTime: .atDocumentStart,
forMainFrameOnly: true,
in: .defaultClient // 关键:使用隔离的 content world
)
config.userContentController.addUserScript(script)
// 注册消息处理器(也在 defaultClient world 中)
config.userContentController.add(
self,
name: "nativeBridge",
contentWorld: .defaultClient
)
webView = WKWebView(frame: view.bounds, configuration: config)
view.addSubview(webView)
}
}
// 处理来自 JavaScript 的消息
extension WebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard message.name == "nativeBridge" else { return }
if let body = message.body as? String {
print("收到来自 JS 的消息: \(body)")
}
}
}
// 调用异步 JavaScript 函数
extension WebViewController {
func callJSAsyncFunction() {
// 先注入一个异步函数
let asyncScript = """
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return JSON.stringify(data);
}
"""
let script = WKUserScript(
source: asyncScript,
injectionTime: .atDocumentEnd,
forMainFrameOnly: true,
in: .page // 这个函数需要在 page world 中执行
)
webView.configuration.userContentController.addUserScript(script)
// 调用异步函数并获取结果
webView.callAsyncJavaScript(
"return await fetchUserData('123')",
arguments: [:],
in: .page,
in: webView
) { result in
switch result {
case .success(let value):
print("异步函数返回: \(value)")
case .failure(let error):
print("调用失败: \(error)")
}
}
}
}
// 配置 WKWebpagePreference 控制渲染策略
extension WebViewController {
func loadURLWithPreferences() {
let url = URL(string: "https://example.com")!
var request = URLRequest(url: url)
// 创建网页偏好配置
let preferences = WKWebpagePreferences()
// 根据域名设置不同的内容策略
if let host = url.host, host == "trusted.example.com" {
preferences.preferredContentMode = .recommended // 推荐模式
} else {
preferences.preferredContentMode = .desktop // 强制桌面模式
}
webView.load(request, preferences: preferences) { [weak self] navigation in
// 页面加载开始
print("开始加载: \(navigation?.request?.url?.absoluteString ?? "")")
}
}
// 使用 WKNavigationDelegate 处理导航决策
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
preferences: WKWebpagePreferences,
decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
// 根据目标 URL 动态调整偏好
if let host = navigationAction.request.url?.host {
if host.contains("mobile.example.com") {
preferences.preferredContentMode = .mobile
}
}
decisionHandler(.allow, preferences)
}
}
最佳实践
- 所有应用注入的 JavaScript 都使用
.defaultClientcontent world,避免与网页脚本冲突 - 通过
WKScriptMessageHandler在 content world 之间传递消息,不要直接操作 DOM - 使用
callAsyncJavaScript调用网页中的异步函数,比手动 Promise 桥接简洁得多 - 为不同信任级别的域名配置不同的
WKWebpagePreferences,提升安全性 - 在 dealloc 时记得调用
WKUserContentController.removeAllScriptMessageHandlers(),防止内存泄漏
还有什么值得关注
WKWebView现在支持了更多的 Web API,包括Payment Request API和WebAuthn- Session 提到了
WKURLSchemeHandler的改进,让你可以拦截自定义 URL scheme 并返回本地内容 - macOS 版本的
WKWebView新增了对findString方法的支持,方便实现页面内搜索