WKWebView 里 H5 的 window 到底是什么?一文讲清 iOS 与 H5 的通信本质
最近在整理一套 WKWebView Demo,里面实现了一个很典型的双向通信场景:
- H5 点击按钮
- 调用 Native
- Native 组装一份 JSON
- 再把 JSON 回传给 H5
- 最后 H5 把 JSON 字符串展示到页面上
代码跑通后,我发现一个很适合给客户端同学"扫盲"的问题是:
H5 里经常出现的 window,到底是什么?为什么它能调到 Native?
这篇文章就从 iOS 客户端视角,把这件事讲清楚。
一、先说结论:window 是网页 JS 的全局对象
如果你是客户端同学,可以先记住一句话:
window = 当前网页 JavaScript 运行时的全局对象。
你可以把它理解成:
- JS 世界里的"最外层对象"
- 当前网页运行环境的全局上下文
- 网页里很多能力的统一入口
比如这些前端常见能力,很多都挂在 window 上:
js
window.location
window.alert()
window.setTimeout()
window.localStorage
而在 WKWebView 环境里,iOS 还会通过 WebKit 给 H5 注入额外能力,比如:
js
window.webkit.messageHandlers
这就是 H5 能调用 Native 的关键入口。
二、客户端同学最常见的一句代码,到底是什么意思?
在 WKWebView 场景里,我们经常会看到这样的 H5 代码:
js
window.webkit.messageHandlers.nativeBridge.postMessage({
action: "getNativeJSON",
from: "h5"
});
如果第一次看到,很容易一脸懵。
其实它可以拆成 5 层:
window- 当前网页的全局对象
webkit- WebKit 注入给当前页面的一层对象
messageHandlers- Native 注册给 H5 的消息通道集合
nativeBridge- 其中一条具体通道,名字由 Native 注册时决定
postMessage(...)- H5 发消息给 Native 的调用方式
所以整句话翻译成客户端语言就是:
"H5 通过 WebKit 提供的 nativeBridge 通道,把一段消息发给 iOS。"
三、为什么 H5 能直接写 window.webkit.messageHandlers?
因为这个对象不是 H5 自己凭空写出来的,而是 Native 提前注册好的。
在 iOS 侧,我们通常会这样注册:
swift
userContentController.add(
WeakScriptMessageHandler(delegate: self),
name: pageConfiguration.jsBridgeName
)
如果 pageConfiguration.jsBridgeName = "nativeBridge",那么 H5 就能在 JS 环境里访问:
js
window.webkit.messageHandlers.nativeBridge
也就是说:
- Native 注册了一个名为
nativeBridge的 handler - WebKit 把这个 handler 暴露给了 JS 环境
- H5 就能通过
postMessage调用 Native
这就是 H5 -> Native 的通信本质。
四、window 不是"桥",它只是"全局对象"
这里很容易有个误区:
很多客户端同学会把 window 直接理解成"JSBridge"。
这个说法不够准确。
更准确的理解应该是:
window是网页全局对象window.webkit.messageHandlers.xxx才是 Native 暴露给 H5 的桥接入口
也就是说:
window 是容器,桥接能力只是挂在这个容器上的一部分属性。
就像你在 Swift 里写:
swift
app.network.request()
你不会说 app 就等于网络层本身。
同理:
js
window.webkit.messageHandlers.nativeBridge.postMessage(...)
也不是说 window 就等于 Native Bridge,而是说 Bridge 被挂在 window 这棵对象树下面。
五、从 iOS 客户端角度,怎么类比最好理解?
如果用客户端同学更熟悉的方式来类比,可以这么记:
1. window 类似 JS 世界的根对象
类似于你有一个全局入口对象:
swift
app
然后一层层往下找:
swift
app.webkit.messageHandlers.nativeBridge.postMessage(...)
只是 JS 里这个根对象叫 window。
2. messageHandlers 类似 Native 暴露的能力列表
你可以把它想成一个"功能注册中心"。
比如 Native 注册了:
nativeBridgeloginBridgerouterBridge
那么 H5 就可以访问:
js
window.webkit.messageHandlers.nativeBridge
window.webkit.messageHandlers.loginBridge
window.webkit.messageHandlers.routerBridge
3. postMessage 类似一次 RPC 调用
H5 通过 postMessage 发送一个消息对象,Native 收到后再根据 action 去分发:
js
{ action: "getNativeJSON", from: "h5" }
这跟客户端里常见的"路由分发"或者"协议分发"其实很像。
六、H5 调 Native 的本质,其实就是"发消息"
我们这次 Demo 里,H5 请求 Native JSON 的代码大概是这样:
js
function requestNativeJSON() {
window.webkit.messageHandlers.nativeBridge.postMessage({
action: "getNativeJSON",
from: "h5"
});
}
这里不是"直接调用 Swift 方法"。
H5 做的事情本质上是:
- 往 Native 发一条消息
- 消息里带上 action 和参数
- Native 收到后自己决定怎么处理
iOS 侧会在 WKScriptMessageHandler 里接收:
swift
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == pageConfiguration.jsBridgeName else { return }
if let body = message.body as? [String: Any], let action = body["action"] as? String {
switch action {
case "getNativeJSON":
sendNativeJSONToH5()
default:
break
}
}
}
所以从本质上说:
H5 -> Native 不是"函数直调",而是"消息通信 + Native 分发处理"。
七、那 Native 又是怎么把结果回传给 H5 的?
这就是另一条方向:
Native -> H5
在 iOS 里最常见的方式是:
swift
webView.evaluateJavaScript(script)
比如这次 Demo 里,Native 会执行这样一段脚本:
swift
let script = "window.renderNativeJSONFromNative(\(jsonObjectString));"
webView.evaluateJavaScript(script)
它的含义是:
- Native 主动执行一段 JS
- 调用页面里的
window.renderNativeJSONFromNative(...) - 把 JSON 数据作为参数传给 H5
而 H5 这边提前定义了这个函数:
js
function renderNativeJSONFromNative(payload) {
var resultNode = document.getElementById('nativeJSONResult');
resultNode.textContent = JSON.stringify(payload, null, 2);
}
这就形成了完整闭环:
- H5 发消息给 Native
- Native 收到并处理
- Native 再执行 JS 回调 H5
- H5 更新页面展示结果
八、所以双向通信可以记成两句话
这个特别适合团队分享时说:
H5 调 Native
js
window.webkit.messageHandlers.xxx.postMessage(...)
意思是:
H5 通过 WebKit 注入的消息通道,把消息发给 Native。
Native 回调 H5
swift
webView.evaluateJavaScript("window.xxx(...)")
意思是:
Native 主动执行页面里的 JS 方法,把结果传回给 H5。
如果再浓缩一下,就是:
postMessage:H5 调 NativeevaluateJavaScript:Native 调 H5
九、为什么很多 JSBridge 设计都会围绕 window 展开?
因为 window 是网页运行时最自然的全局入口。
前端代码如果想暴露一个全局函数,最简单的方式就是挂到 window 上:
js
window.renderNativeJSONFromNative = function(payload) { ... }
而 Native 想让 H5 使用某些能力,也通常会借助 WebKit 注入到这个全局环境里。
所以你会看到很多 JSBridge 方案,不管名字怎么变化,底层都绕不开两件事:
- H5 侧通过
window找到桥接入口 - Native 侧通过执行 JS 调用
window上的方法
window 不是桥本身,但它往往是桥接能力的"落点"。
十、客户端同学在理解 window 时,最容易踩的几个误区
误区 1:window 就是 iOS 注入的对象
不是。
window 本来就是浏览器 / WebView 里的全局对象。
iOS 注入的是它下面的某些属性,例如:
js
window.webkit.messageHandlers
误区 2:H5 是在直接调 Swift 方法
也不是。
H5 只是通过 postMessage 发消息给 Native,Native 收到之后再自己分发。
误区 3:Native 回调 H5 只能传字符串
不完全对。
本质上是 Native 拼一段 JS 代码给 WebView 执行。你可以传字符串,也可以把 JSON 序列化后拼进 JS 调用里。
不过在工程实践里,要特别注意:
- 转义
- 注入安全
- 数据格式统一
- 大对象传输成本
十一、如果站在工程实践角度,这套机制还能怎么升级?
这次 Demo 是一个"扫盲版最小闭环",但线上业务一般会继续升级成标准协议。
比如从:
js
{ action: "getNativeJSON" }
升级成:
js
{
module: "user",
method: "getProfile",
params: { userId: "10001" },
callbackId: "cb_123"
}
然后 Native 统一返回:
json
{
"callbackId": "cb_123",
"code": 0,
"message": "success",
"data": {
"id": "10001",
"name": "Oliver"
}
}
这样就能支持:
- 多模块分发
- 异步回调
- Promise 封装
- 错误码体系
- 统一埋点与日志
这时候你再回头看 window,就会更清楚:
它只是 JS 世界的入口,真正的重点是桥接协议怎么定义。
十二、最后用一句最容易记住的话收尾
如果你是客户端同学,只要记住下面这三句,基本就不会再被 window 绕晕:
window是网页 JS 的全局对象window.webkit.messageHandlers.xxx.postMessage(...)是 H5 调 NativeevaluateJavaScript("window.xxx(...)")是 Native 回调 H5
结尾
以前我第一次看 window.webkit.messageHandlers.nativeBridge.postMessage(...) 的时候,也会觉得这玩意儿像"黑魔法"。
但把它拆开看,本质并不复杂:
window是 JS 全局对象webkit.messageHandlers是 Native 暴露给 H5 的消息通道postMessage是 H5 发消息evaluateJavaScript是 Native 回调
一旦把这四层关系理顺,WKWebView 里的绝大多数桥接代码,读起来都会清晰很多。
如果你最近也在整理 WKWebView、JSBridge 或者混合栈相关知识,希望这篇扫盲随笔能帮你少绕几个弯。