JS 能调用window.desktop.openFile()——
并不是 Web 突然变强了,而是浏览器内核在背后完成了一整套跨进程绑定体系。
一、核心问题:JS 是怎么“看到” C++ 的?
我们从最终现象开始:
window.desktop.openFile("C:\\test.txt", callback)
这行 JS 的背后,是这样一条链:
JS ↓ V8 Native Function ↓ Renderer C++ (DesktopApiHelper) ↓ IPC ↓ Browser C++ (DesktopCmdHandler) ↓ Win32 Shell API
而WebHostView正是这个链路的“宿主容器入口”。
二、第一层:WebHostView 的真实职责
WebHostView 并不直接参与 JS 调用,它负责三件底层大事:
① 持有独立 WebContents
它不是 Tab,不是 WebUI,而是:
class WebHostView : public views::View { std::unique_ptr<content::WebContents> web_contents_; }
这意味着:
它拥有一个完整渲染环境,但不属于浏览器标签体系。
这为 JS API 注入提供了“私有运行时”。
② 控制 RenderFrame 生命周期
JS 绑定只能发生在:
RenderFrame 创建 → ScriptContext 建立
而 WebHostView 正是这个 RenderFrame 的创建触发者。
③ 它定义了“信任域”
浏览器不会把系统能力暴露给所有页面。
WebHostView 作为宿主,天然就是:
Trusted Web Runtime
只有这个运行时里的页面,才允许注入 desktop API。
三、真正的 JS 绑定发生在哪里?
很多人误以为 JS 绑定在 Browser 进程,实际上:
JS 能力注入发生在 Renderer 进程,通过 V8 Extension 完成。
关键类:
class DesktopApiExtension : public extensions_v8::Extension
这里定义了注入给页面的 JS:
const char kDesktopApiSource[] = R"( native function SendDesktopCmd(); native function SetCallback(); if (!window.desktop) window.desktop = {}; window.desktop.openFile = function(path, cb) { var id = SetCallback(cb); SendDesktopCmd(id, "openFile", path, "", "", ""); }; )";
注意:
这段 JS不是网页写的,而是浏览器内核塞进去的。
四、V8 是如何把 JS 调用转到 C++ 的?
JS 中的:
native function SendDesktopCmd();
会被绑定到:
void DesktopApiExtension::GetNativeFunction(...)
此时:
JS 调用 → V8 → C++ 回调函数
这一步是:
JavaScript 世界 → Renderer C++ 世界
五、Renderer 如何把请求送去 Browser?
Renderer 并没有权限操作系统,所以:
void DesktopApiHelper::DesktopApiCmd(...) { Send(new ExtensionHostMsg_DesktopApiCmd(...)); }
通过 IPC 发送到 Browser 进程。
这是安全模型的核心:
| 进程 | 权限 |
|---|---|
| Renderer | 沙箱内 |
| Browser | 可调用系统 API |
六、Browser 进程如何执行桌面操作?
Browser 收到 IPC 后进入:
DesktopCmdHandler::OnDesktopApiCmd(...)
然后调用:
| JS API | Windows API |
|---|---|
| openFile | ShellExecute |
| showSysMenu | IContextMenu |
| getIcon | SHGetFileInfo |
这里浏览器本质上:
成为了 Web 的“系统代理进程”
七、结果如何回到 JS?
Browser:
ExtensionMsg_DesktopApiCmdResponse
Renderer:
DesktopApiHelper::OnDesktopApiCmdResponse(...) → DesktopApiExtension::HandleResponse(...) → JS callback
完整闭环形成。
八、WebHostView 在这套链路中的真实定位
很多人问:
“既然绑定在 Renderer,WebHostView 作用是什么?”
答案是:
| 组件 | 角色 |
|---|---|
| WebHostView | JS 运行时宿主 |
| RenderFrame | JS 执行环境 |
| DesktopApiExtension | API 注入器 |
| DesktopApiHelper | IPC 桥接器 |
| DesktopCmdHandler | 系统能力执行者 |
WebHostView 是容器,不是桥,但没有它桥就无处可架。
九、为什么必须设计成这样?
❌ 不能让 JS 直接访问系统
这会打破浏览器沙箱安全模型。
❌ 不能在 Renderer 执行系统 API
Renderer 被沙箱限制。
✅ 必须走 Browser 代理
这就是:
浏览器 = 安全的桌面能力网关
十、这一机制的工程意义
WebHostView 不是 UI 组件,而是:
✔ 浏览器向“桌面运行时”演进的关键节点
✔ Web App 本地化的基础设施
✔ 多进程安全模型下的能力注入范式
这也是:
Chrome Apps、Electron、WebView2 背后的底层哲学
十一、用一句话总结
WebHostView 并不直接绑定 JS,它提供的是一个“可信 Web 运行时容器”,在该容器创建的 RenderFrame 中,浏览器通过 V8 Extension 动态注入 JS API,再经 Renderer → Browser IPC 桥接,最终让 JavaScript 能够安全地调用操作系统能力。
结尾
当你再看到:
window.desktop.openFile()
请记住:
这不是一个 JS API,
而是一整套浏览器内核级跨进程架构在工作。