windows-rs 之 windows-webview 实战:用 Rust 在桌面应用中托管 Microsoft Edge WebView2
【免费下载链接】windows-rsRust for Windows项目地址: https://gitcode.com/GitHub_Trending/wi/windows-rs
导读
本文围绕 windows-rs 工作区中的windows-webviewcrate(源码位于 crates/libs/webview)展开,讲解如何在 Windows 桌面应用中用 Rust 安全地封装并托管 WebView2(基于 Chromium 的 Microsoft Edge)浏览器控件。读完本文,你将掌握从"创建一个 Environment 并导航到网址"的最小示例,到窗口尺寸同步、事件订阅、宿主与 JavaScript 双向通信、本地内容拦截、Profile/Cookie/下载/DevTools 等完整能力,并了解其异步初始化在 UI 线程消息泵上的底层原理。
一、crate 定位:何时使用 windows-webview
windows-webview是对 WebView2 与官方指南 docs/crates/windows-webview.md,当你的 Windows 桌面应用需要以下能力时应选择本 crate:
- 在窗口(
HWND)中托管 Web 内容; - 与页面中的 JavaScript 交换消息;
- 使用浏览器的 Profile、Cookie、下载、Chrome DevTools Protocol(CDP)等设施。
crate 有两大托管路径:
- 默认路径:在原生
HWND窗口中托管 WebView2; - reactor 路径:启用
reactorfeature 后,可将 WinUI XAML 的 WebView2 控件放入windows-reactor的视图树中。
需要强调的是,本 crate 是"精选 WebView2 API 表面"的封装,而非完整 SDK 的暴露层。若应用需要未在此处体现的 API,应改用原始 WebView2 绑定(ICoreWebView2*直接绑定)。
二、前置条件与依赖配置
2.1 运行时与环境要求
- WebView2 Runtime:
HWND托管方式要求系统已安装 Microsoft Edge WebView2 运行时(大多数现代 Windows 10/11 系统已内置 Edge 并附带该运行时)。 - 活着的父窗口 + 消息循环:宿主必须提供一个存活的父窗口,并在其 UI 线程上持续分发消息。
windows-window是一个适合此用途的小型宿主窗口 crate。 - COM STA 线程:必须在 COM 单线程单元(STA)上创建环境。
Environment::new与Environment::with_options在必要时会把调用线程初始化为 STA;如果该线程已被初始化为多线程单元(MTA),它们会返回错误。从 environment.rs 的实现可以看到,这是通过CoInitializeEx(COINIT_APARTMENTTHREADED)完成的,当返回RPC_E_CHANGED_MODE时会抛出明确的错误说明。 - 生命周期顺序:父窗口必须活得比它的
Controller更久。
2.2 依赖配置
在Cargo.toml中添加:
[dependencies] windows-webview = "0.100" windows-window = { workspace = true }windows-webview本身依赖windows-core与windows-window(见 Cargo.toml);reactor为可选 feature,开启时会引入windows-reactor:
[dependencies] windows-webview = { version = "0.100", features = ["reactor"] }crate 的rust-version为 1.95,采用 2024 edition,默认目标平台为x86_64-pc-windows-msvc。
三、最小托管示例:从零到完成导航
readme 给出的最小示例(见 readme.md)演示了完整的最小闭环——创建环境、创建控制器、取得 WebView、导航:
use windows_webview::*; use windows_window::Window; fn host(window: &Window) -> Result<()> { let environment = Environment::new()?; let controller = environment.create_controller(window)?; let webview = controller.webview()?; webview.navigate("https://example.com")?; Ok(()) }对应的完整可运行示例见 crates/samples/webview/samples/examples/minimal.rs。
两个关键注意点:
Environment与Controller的创建在 WebView2 中是异步操作,本 crate 通过在 UI 线程上泵送消息队列、直到回调完成,将其呈现为同步调用(细节见下文"异步初始化与消息泵")。因此这些调用应发生在设置阶段、进入应用主消息循环之前。- 在浏览器被托管期间必须保持
Controller存活。Controller被 drop 后,其持有的WebView将不可用。
四、首个完整工作流:托管一个可缩放页面
官方指南给出了从最小示例延伸出去的完整工作流(docs/crates/windows-webview.md):
- 在 UI 线程上创建父窗口;
- 创建一个
Environment,再为该窗口创建一个Controller; - 把控制器边界设置为当前客户区大小,并在窗口的 resize 回调中重复该操作;
- 取得
WebView,注册应用所需的事件,并保留每一个返回的EventRegistration; - 导航,然后进入宿主消息循环;
- 若关闭顺序由应用控制,则在销毁父窗口之前先关闭控制器。
其中关键的 resize 与事件订阅代码如下:
use windows_webview::*; fn configure(controller: &Controller, webview: &WebView, width: i32, height: i32) -> Result<EventRegistration> { controller.set_bounds(0, 0, width, height)?; let navigation = webview.on_navigation_completed(|args| { println!("navigation succeeded: {}", args.is_success()); })?; webview.navigate("https://learn.microsoft.com/windows/apps/")?; Ok(navigation) }陷阱:如果立刻 drop 掉
navigation,就会立即退订该处理器。WebView 示例在消息循环的整个生命周期内,把注册保存在Vec<EventRegistration>中。可参考 crates/samples/webview/samples/src/lib.rs 中关于 resize、注册、控制器与消息循环生命周期的正确写法。
五、对象模型与回调生命周期
5.1 三个核心对象的分工
| 对象 | 职责 |
|---|---|
Environment | 拥有用户数据目录与浏览器进程上下文。复用它创建多个Controller,即可共享同一上下文(浏览器进程与数据)。 |
Controller | 拥有托管在父窗口中的浏览器,控制边界、可见性、焦点与显示属性。 |
WebView | 表示页面本身,提供导航、脚本、消息、Profile 与事件 API。 |
5.2 异步初始化与消息泵(pump)原理
Environment与Controller的创建是异步 WebView2 操作。crate 通过"泵送调用线程的消息队列直到回调完成"把它们包装成同步调用。其底层实现在 pump.rs 中:一个Rc<Cell<Option<Result<T>>>>的一次性结果槽位,配合GetMessageW/TranslateMessage/DispatchMessageW循环不断分发消息,直到完成回调写入结果。
pub(crate) fn wait<T>(slot: &Slot<T>) -> Result<T> { let mut message = MSG::default(); loop { if let Some(value) = slot.take() { return value; } match GetMessageW(&mut message, std::ptr::null_mut(), 0, 0).0 { -1 => return Err(Error::from_thread()), 0 => return Err(Error::empty()), // WM_QUIT 在完成回调执行前结束了嵌套泵 _ => { let _ = TranslateMessage(&message); DispatchMessageW(&message); } } } }这个机制之所以安全,是因为创建与完成都发生在同一个 STA 线程上。而运行时操作(如execute_script、Cookie 枚举、Profile 清理、DevTools 调用)则保持回调式、在 UI 线程上完成,避免嵌套的消息泵送。同一规则也适用于add_script_to_execute_on_document_created——它同样泵送消息,因此应在设置阶段调用。
5.3 显式关闭与安全边界
- 需要显式关闭时调用
Controller::close(底层对应ICoreWebView2Controller::Close)。 - 任何情况下,使用
WebView期间都要保持Controller存活。 - 通过
unsafe create_*_for_hwnd传入的原始父窗口句柄,必须在控制器的整个生命周期内保持有效。
六、导航与页面状态
6.1 基础导航 API
WebView支持:
navigate(uri):导航到指定 URI;navigate_to_string(html):把给定 HTML 内容作为文档导航;reload()/stop()/go_back()/go_forward():历史与加载控制;source()/document_title():读取当前顶层文档的 URL 与标题。
这些方法在 webview.rs 中直接映射到ICoreWebView2的对应 COM 调用。
6.2 自定义导航请求(NavigationRequest)
普通navigate无法携带自定义方法、请求头或请求体,此时使用NavigationRequest。默认是"GET + 无头 + 无体",可通过 fluent 方法定制:
use windows_webview::{NavigationRequest, Result, WebView}; fn submit(webview: &WebView) -> Result<()> { let request = NavigationRequest::new("https://example.test/session") .method("POST") .header("Content-Type", "application/json") .body(br#"{"active":true}"#.to_vec()); webview.navigate_with_request(&request) }从源码看,navigate_with_request会把 headers 拼装为"Name: Value\r\n"格式,并通过SHCreateMemStream把 body 转成IStream,最终调用CreateWebResourceRequest+NavigateWithWebResourceRequest(webview.rs)。
6.3 导航事件与故障处理
导航生命周期通过三个事件观察:
on_navigation_starting:导航开始前触发,可检查目标并通过NavigationStartingArgs::set_cancel(true)取消;on_content_loading:新文档内容开始加载时触发;on_navigation_completed:导航完成时触发,args.is_success()表示是否成功。
各事件的参数均带navigation_id(),用于关联同一导航的不同阶段。on_process_failed用于处理进程故障:ProcessFailedKind::RenderProcessExited(渲染进程崩溃)之后可以reload恢复;而BrowserProcessExited(浏览器进程退出)则必须创建新的WebView。完整的事件参数类型定义见 event.rs。
七、事件、决策与清理:EventRegistration 与 Deferral
7.1 RAII 风格的事件注册
每个on_*方法都返回一个#[must_use]的EventRegistration(event.rs)。丢弃它、或显式调用remove(),都会自动退订回调——这符合 Rust 的 RAII 惯例,避免了忘记退订导致的内存泄漏。
事件回调运行在 UI 线程上,且不是Send。因此回调内部工作要短小精悍,耗时工作应移出回调,避免阻塞消息分发。
7.2 事件覆盖范围
事件集合覆盖(对应源码中的subscription!宏与手写订阅方法):
- 导航:
on_navigation_starting、on_content_loading、on_navigation_completed; - 页面状态:
on_document_title_changed、on_contains_fullscreen_element_changed; - 窗口行为:
on_window_close_requested、on_new_window_requested; - 权限:
on_permission_requested; - 下载:
on_download_starting; - 进程故障:
on_process_failed; - 消息:
on_web_message_received; - 资源请求:
on_web_resource_requested; - 焦点:
on_got_focus、on_lost_focus、on_move_focus_requested、on_accelerator_key_pressed; - DevTools:
on_dev_tools_protocol_event。
7.3 Deferral:事件返回后继续决策
NewWindowRequestedArgs与PermissionRequestedArgs可以在事件返回之后通过defer()取得一个Deferral来完成决策(例如等待用户在弹出的对话框中作答)。Deferral 在 drop 时自动完成;必须保持它存活直到决策被应用——过早 drop 会告诉 WebView2 处理已结束。
下载进度订阅遵循同样的 RAII 规则:不仅外层on_download_starting的注册要保留,DownloadOperation::on_bytes_received_changed与on_state_changed返回的注册也必须保留。
八、宿主集成:尺寸、DPI、焦点与控制器选项
8.1 尺寸、可见性与 DPI
Controller::set_bounds(left, top, right, bottom)使用父窗口客户区像素坐标;- 在父窗口的
WM_MOVE处理中调用notify_parent_window_position_changed(),让浏览器弹出的窗口与对话框跟随宿主移动; set_visible(false)隐藏控制器;隐藏期间可把 WebView 内存目标设为MemoryUsageTargetLevel::Low,显示时恢复Normal(webview.rs);zoom_factor控制页面缩放(1.0即 100%),rasterization_scale控制渲染缩放;- 默认会自动检测显示器 DPI 变化;若缩放由应用自己管理,先调用
set_should_detect_monitor_scale_changes(false)关闭自动检测; set_default_background_color控制页面绘制前的区域颜色。WebView2 仅支持完全不透明或完全透明的 alpha:使用Color::TRANSPARENT可以让宿主窗口在页面透明处透出(controller.rs)。
8.2 焦点与键盘
- 父窗口收到
WM_SETFOCUS时调用move_focus(MoveFocusReason::Programmatic),把键盘焦点移入浏览器; on_move_focus_requested让宿主能继续把 Tab 导航带入另一个原生控件:移动焦点并调用MoveFocusRequestedArgs::set_handled(true);on_accelerator_key_pressed可以在页面处理之前消费应用快捷键(如 F 键、Ctrl/Alt 组合键);相关浏览器加速键设置决定 WebView2 内置快捷键是否保持可用。
8.3 控制器创建选项(ControllerOptions)
ControllerOptions用于在创建时选择 Profile 名称、隐私模式与初始背景色:
let options = ControllerOptions::new() .profile_name("app-profile") .in_private_mode(false) .default_background_color(Color::TRANSPARENT); let controller = environment.create_controller_with_options(&window, &options)?;这些选项只在控制器创建时生效,无法事后通过WebView修改。源码实现(controller.rs)通过CreateCoreWebView2ControllerOptions创建 COM 选项对象,再调用CreateCoreWebView2ControllerWithOptions。
九、环境与浏览器设置
9.1 EnvironmentOptions
EnvironmentOptions通过 fluent 风格构建,配置浏览器可执行文件夹、用户数据文件夹、浏览器命令行参数、语言、最低兼容浏览器版本、操作系统账户单点登录、浏览器扩展与滚动条样式:
use windows_webview::EnvironmentOptions; let options = EnvironmentOptions::new() .user_data_folder(r"C:\MyApp\WebView2") .additional_browser_arguments("--disable-features=msSmartScreenProtection") .language("en-US"); let environment = Environment::with_options(&options)?;各配置项说明(见 options.rs):
| 方法 | 说明 |
|---|---|
browser_executable_folder | 指定包含 WebView2 浏览器(Edge)二进制的文件夹,替代已安装的运行时 |
user_data_folder | WebView2 存放用户数据(缓存、Cookie 等)的文件夹 |
additional_browser_arguments | 传递给浏览器进程的额外命令行参数 |
language | 默认显示语言,如"en-US" |
target_compatible_browser_version | 环境要求的最低兼容浏览器版本;未设置时使用 WebView2 GA 基线86.0.616.0(WebView2 拒绝空值,源码中的默认常量见 options.rs) |
allow_single_sign_on_using_os_primary_account | 使用操作系统主账户的单点登录 |
are_browser_extensions_enabled | 允许环境加载并运行浏览器扩展 |
scrollbar_style | 滚动条样式:Default(浏览器默认)或FluentOverlay(Fluent 风格细覆盖条) |
数据目录建议:默认位置不合适时,选择可写、由应用拥有的用户数据文件夹。用同一 Environment 创建的多个 Controller 共享其浏览器进程与数据上下文;在该上下文中,命名 Profile 可以隔离 Cookie、缓存与存储。
9.2 WebView 设置(Settings)
WebView::settings返回一组开关(settings.rs),包括:
- 脚本:
is_script_enabled / set_script_enabled; - 消息:
is_web_message_enabled / set_web_message_enabled; - 对话框:
are_default_script_dialogs_enabled / set_default_script_dialogs_enabled; - 状态栏、DevTools、上下文菜单、宿主对象、缩放控件、错误页、加速键、自动填充、密码保存、捏合缩放、滑动导航、非客户区等开关;
- 用户代理覆盖(user-agent override)。
注意:Settings 的 setter 在下次导航时生效。
十、宿主与 JavaScript 双向通信
10.1 页面 → 宿主
页面调用window.chrome.webview.postMessage(...),宿主通过on_web_message_received接收。收到消息后:
- 先检查
WebMessageReceivedArgs::source()(消息来源文档的 URI),不要信任来自可导航内容的任意消息; - 用
web_message_as_json()获取任意 JavaScript 值的 JSON 序列化结果; - 当协议要求字符串时,用
try_web_message_as_string()(若页面发送的不是字符串则返回错误)。
10.2 宿主 → 页面
post_web_message_as_json(json):以 JSON 值发送,页面通过window.chrome.webview.addEventListener("message", ...)接收,event.data为解析后的 JSON;post_web_message_as_string(message):以字符串发送;execute_script(javascript, handler):在页面上下文异步执行 JavaScript,回调在 UI 线程收到 JSON 编码的结果;add_script_to_execute_on_document_created(javascript):注册在每个新文档中、先于页面脚本执行的注入脚本,返回ScriptId;若后续需要移除,保留该 ID 并调用remove_script_to_execute_on_document_created(&id)。
ipc示例(crates/samples/webview/samples/examples/ipc.rs)与script示例(script.rs)演示了消息注入、收发与脚本增删的完整流程。
十一、本地内容与请求拦截
11.1 磁盘文件:虚拟主机映射
对于磁盘上的文件,使用set_virtual_host_name_to_folder_mapping(host_name, folder_path, access_kind)把虚拟主机名映射到本地文件夹,然后导航到 HTTPS 源,例如https://app.example/index.html。HostResourceAccessKind控制跨源访问:
| 取值 | 行为 |
|---|---|
Deny | 其他源的资源不能访问映射内容 |
Allow | 任意源的资源都可以访问映射内容 |
DenyCors | 与Deny类似,但允许通过 CORS 的跨源请求 |
访问结束时调用clear_virtual_host_name_to_folder_mapping(host_name)清除映射。示例见 crates/samples/webview/samples/examples/local_files.rs。
11.2 内存内容:资源请求拦截
对于动态生成或内嵌的字节内容,使用on_web_resource_requested(uri_filter, handler)。其通配符过滤器限制哪些请求到达处理器。处理器返回:
Some(WebResourceResponse):提供状态码、请求头、内容类型与响应体;None:继续浏览器默认处理。
处理器在 UI 线程上同步运行,因此昂贵内容应提前准备好。从源码看(webview.rs),注册会同时添加请求过滤器,注册被 drop 时会连同过滤器一起移除。示例见 crates/samples/webview/samples/examples/custom_protocol.rs,它从内存中提供 HTML 与 CSS。
十二、Profile、Cookie、下载与 DevTools
- Cookie:
CookieManager(通过WebView::cookie_manager()取得)支持创建、更新、枚举与删除 Cookie;枚举是回调式的。 - Profile:
Profile(WebView::profile())暴露名称、路径、隐私状态、首选配色方案、下载文件夹,以及回调式的浏览数据清理。 - 下载:
on_download_starting可以修改结果路径、取消操作,或保留DownloadOperation用于暂停、恢复、取消、进度、状态与中断原因查询。示例见 crates/samples/webview/samples/examples/downloads.rs。 - DevTools/CDP:
call_dev_tools_protocol_method(method, params_json, handler)发送 CDP 方法与 JSON 参数,无需打开远程调试端口。通过on_dev_tools_protocol_event(event_name, handler)订阅的多数 CDP 事件,需要先用 CDP 方法启用其所属域(domain)才会触发。示例见 crates/samples/webview/samples/examples/devtools.rs。
十三、Reactor 集成(XAML 路径)
当浏览器应属于 Reactor 视觉树时,启用reactorfeature:
webview()返回一个View,并把就绪的WebView提供给其回调;原生初始化出错时它会 panic;- 若组件需要自行处理
IntegrationError,改用webview_result(); - 返回的
WebView与HWND路径支持完全相同的导航、消息、设置与事件 API。
重要限制:XAML 控件只有在进入活着的视觉树之后才会初始化,因此不要在组件构造期间期待它的回调。自包含的 Reactor 应用还必须部署Microsoft.Web.WebView2.Core.dll,windows-reactor-setup负责暂存它。组件与部署布局可参考 crates/samples/reactor/webview 示例。
底层机制上,WinUI XAML 控件暴露的是 WinRT 的CoreWebView2,而本 crate 封装的是 COM 的ICoreWebView2,两者不能通过普通接口转换互相转换,必须通过官方支持的桥接接口ICoreWebView2Interop2::GetComICoreWebView2获取 COM 内核(见 reactor.rs 与 docs/crates/windows-webview.md)。
十四、示例程序一览
使用以下命令运行 WebView 示例:
cargo run -p webview_samples --example <name>| 示例 | 工作流 |
|---|---|
minimal | 创建HWND宿主、设置控制器尺寸并导航 |
events | 观察导航、弹窗、权限、关闭与进程故障事件 |
ipc | 注入脚本、交换消息、执行 JavaScript |
custom_protocol | 从内存中提供 HTML 与 CSS |
local_files | 把文件夹映射为 HTTPS 虚拟主机 |
downloads | 跟踪下载进度与状态 |
cookies | 添加与枚举 Cookie |
profile | 使用隐私模式、配色方案与浏览数据清理 |
script | 添加、执行与移除文档创建时脚本 |
devtools | 调用 CDP 方法并订阅 CDP 事件 |
示例源码位于 crates/samples/webview/samples/examples;共享辅助代码 crates/samples/webview/samples/src/lib.rs 演示了正确的 resize、注册、控制器与消息循环生命周期。
十五、内部实现速览(面向贡献者)
15.1 绑定生成
WebView2 只提供 C/C++ 头文件而没有 Windows 元数据,因此tool_webview分三阶段生成提交到仓库的绑定:
| 阶段 | 实现 | 输出 |
|---|---|---|
| 头文件 → RDL | windows_clang::clang() | target/webview/WebView2.rdl |
| RDL → winmd | windows_rdl::reader() | target/webview/WebView2.winmd |
| winmd → Rust | windows_bindgen | crates/libs/webview/src/bindings.rs |
工具下载固定的Microsoft.Web.WebView2NuGet 包,分别解析WebView2.h与WebView2Interop.h两个输入再合并翻译单元,目标平台为x86_64-pc-windows-msvc(启用 Microsoft 扩展)。重新生成用cargo run -p tool_webview;切勿手改src/bindings.rs。过滤规则在 crates/tools/webview/src/webview.txt。
15.2 运行时与字符串
handler.rs中的完成处理器与事件适配器使用implement_decl!宏,避免引入windows-core的 proc-macro 依赖;pump.rs的一次性结果槽与消息泵保证了创建流程的同步观感;- 事件适配器把 COM 的 add/remove token 转换为
EventRegistration,资源拦截在注册 drop 时同时移除请求过滤器; string.rs统一处理借用的 UTF-16 输入、借用的回调字符串、需要CoTaskMemFree释放的LPWSTR,以及由实现的接口返回的任务分配器输出字符串。
由于 WebView2 需要运行时、窗口与消息泵,仓库没有无头集成测试套件,示例应用本身就是端到端的托管与功能覆盖测试。
结语
windows-webview以不到数百行的安全封装,把 WebView2 最常用的能力——导航、脚本、消息、Profile、Cookie、下载、DevTools、资源拦截、事件订阅——以符合 Rust 习惯(RAII、Result、回调)的方式呈现给桌面应用开发者。理解"创建期同步泵送、运行期回调式"的线程模型,并牢记EventRegistration与Deferral的生命周期纪律,即可在项目中稳定、高效地集成 Chromium 内核的 Web 托管能力。
【免费下载链接】windows-rsRust for Windows项目地址: https://gitcode.com/GitHub_Trending/wi/windows-rs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考