IE 模式之后换成 WebView2 就够了吗 ── ActiveX 无法运行的限制与现实的迁移设计

· · WebView2, Windows, .NET, WPF, Windows Forms, IE 模式, ActiveX, 遗留系统迁移, 内部系统, 技术咨询

在之前的文章〈如何延续并摆脱依赖 IE 模式的内部 Web 系统〉中,我提到 IE 模式终究只是有期限的延命措施,应该同时设计好退出路径。而在接受「出口」相关咨询的过程中,经常出现的选项就是 WebView2。「想在专用应用程序中显示内部 Web 系统」「想只用 Web 技术做桌面应用程序中的部分界面」「听说 Electron 很重,有没有替代方案」──这些场景都会让 WebView2 成为候选方案。

另一方面,WebView2 在引入之前也有一些必须先了解的特性:运行时该怎么分发、用户数据文件夹该放在哪里、原生端和 Web 端该怎么互相通信。而最重要的是,原本在 IE 模式下运行的 ActiveX,在 WebView2 中无法运行,这是关系到迁移计划根本的限制。本文将梳理 WebView2 的基本结构、分发方式、设计、安全性,一直谈到与摆脱 IE 模式现实结合的方式。

1. 先说结论

  • WebView2 是把基于 Chromium 的 Microsoft Edge 嵌入到 Windows 应用程序中的控件。可以从 WinForms / WPF / WinUI / Win32 C++ 调用,适合用在既有桌面应用程序中「只加入部分 Web UI」的场景。1
  • 运行时原则上使用 Evergreen(自动更新的共享运行环境)。虽然 Windows 11 已经标准内置,但不应假设「一定已经安装」,官方建议在安装程序中内置存在性检测与启动安装(bootstrap)流程。2
  • 针对离线环境或希望固定已验证配置的工厂、生产线,可以使用 Fixed Version(随应用程序一起发布),但内置文件会超过 250MB,并且必须自行负责发布安全更新。请不要轻易选用。3
  • 最先容易踩到的是用户数据文件夹(UDF)的问题。UDF 默认会创建在 exe 旁边,因此安装在 Program Files 下的应用程序会启动失败。请把明确指定 %LOCALAPPDATA% 下的文件夹当作固定做法。4
  • 原生端与 Web 端的联动,基本上以 PostWebMessageAsJson / WebMessageReceived 的消息交换为主AddHostObjectToScript(暴露 COM 对象)则应限定于可信任的内容。1
  • WebView2 中无法运行 ActiveX。 含有 ActiveX 的 IE 模式依赖页面,不能仅靠「换成 WebView2」了事,必须把原本由 ActiveX 负责的处理迁移到原生端重新设计。这正是摆脱 IE 模式计划的核心所在。5

2. WebView2 的基本结构

WebView2 由「SDK(嵌入到应用程序中的 API)」与「运行时(安装在客户端的 Edge 基础运行环境)」两部分组成。这与 Visual C++ 运行时或 .NET 运行时的架构相同:应用程序引用 NuGet 包 Microsoft.Web.WebView2 进行构建,运行时则使用客户端上的运行环境来运行。3

支持的平台相当广泛,可以在 .NET Framework 4.6.2 以上/.NET Core 3.1 以上的 WinForms、WPF、WinUI、Win32 C++ 中使用。像是只把既有 WinForms 业务应用程序中的某一个界面换成 WebView2 这种渐进式用法,正是相对于整体切换框架(例如 Electron)的一大优势。UI 框架本身的选型,也请参考〈WinForms / WPF / WinUI 该如何选择 - 判断表〉。

最小化的嵌入方式大致如下面的代码(WPF/WinForms 通用的思路)。

var env = await CoreWebView2Environment.CreateAsync(
    browserExecutableFolder: null,   // 使用 Evergreen 运行环境
    userDataFolder: Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "KomuraSoft", "MyApp", "WebView2"));
await webView.EnsureCoreWebView2Async(env);
webView.CoreWebView2.Navigate("https://internal.example.co.jp/app/");

重点在于不依赖默认值,而是自行创建 CoreWebView2Environment。理由会在接下来的两章中说明。

引入步骤本身很直观,WinForms / WPF 的话大致是下面的流程。

  1. 通过 NuGet 把 Microsoft.Web.WebView2 添加到项目中(设计器的工具箱中也会出现 WebView2 控件)
  2. 在表单/窗口中放置控件
  3. 如上面的代码一样,先用 EnsureCoreWebView2Async 初始化,再调用 Navigate

这里有一个一开始就务必掌握的 API 注意事项:webView.CoreWebView2 在初始化完成之前是 null。在表单构造函数中尝试订阅事件,比如 CoreWebView2.WebMessageReceived += ...,结果变成 NullReferenceException,是常见的坑,初始化相关的处理应统一放在「EnsureCoreWebView2Async 的 await 之后」,这是基本做法。对 Source 属性赋值也会隐式启动初始化,但如果是想指定环境(比如 UDF 的位置)的应用程序,统一先明确调用 EnsureCoreWebView2Async(env) 会更不容易出问题。

另外,WebView2 的事件全部都在 UI 线程上触发。如果在 WebMessageReceived 中直接写入繁重的处理(比如设备访问或文件 I/O),连带 Web 端的操作体验也会一起卡住,因此原生端的处理应该用 async/await 让出线程。这方面的原则,可以直接套用〈WPF/WinForms 的 UI 线程与 async/await〉一文中写的内容。

3. 运行时分发 ── Evergreen 与 Fixed Version

3.1 Evergreen(推荐)

Evergreen 是所有 WebView2 应用程序共享安装在客户端上的运行时,并且该运行时会自动更新的方式。因为安全补丁会自动应用、磁盘占用也较小,官方明确推荐使用这种方式。6

实务中要注意的重点有 3 个。

  • 实现存在性检测。 虽然 Windows 11 已经标准内置,Windows 10 也已经广泛分发,但仍然存在「没有安装」的终端设备。在 .NET 中可以用 CoreWebView2Environment.GetAvailableBrowserVersionString() 确认,但在尚未安装运行时的环境中,这个调用本身就会以异常(WebView2RuntimeNotFoundException)失败,因此要用 try/catch 包起来,把「发生异常=尚未安装」当作判断依据,并在安装流程中接续执行启动安装程序(bootstrapper,一个小型的在线安装程序)或独立安装程序。2
  • 设计对运行时更新的跟进。 即使运行时已经更新,正在运行中的应用程序仍会继续使用旧版本。推荐的做法是捕获 NewBrowserVersionAvailable 事件,建立「重启后应用更新」的引导流程。2
  • 与内部的更新管控保持一致。 Edge 浏览器与 WebView2 运行时的更新策略是各自独立的。在通过组策略停止运行时更新的环境中,Evergreen 的前提(始终是最新版本)会不成立,因此若要使用较新的 API,应加入功能检测。6

3.2 Fixed Version(限定用途)

Fixed Version 是把特定版本的运行时随应用程序一起发布的方式。因为可以固定已完成验证的配置,所以在离线的生产设备,或变更管理极其严格的环境中,会是合理的选择。不过,

  • 内置的二进制文件会超过 250MB,发布物也会因此变大2
  • 由于运行时不会自动更新,因此必须自行负责通过自己的发布版本,发送浏览器引擎的漏洞修复
  • 一旦疏于更新,公司内部就会持续留下「搭载旧版 Chromium 的业务应用程序」

这些都是相当沉重的代价。只有在显示的内容完全封闭、由自家管理,并且发布周期中已经纳入运行时更新的体制时,才应该选用 Fixed Version。

4. 最先容易踩到的坑 ── 用户数据文件夹

WebView2 会把 Cookie、缓存、权限等内容存储在用户数据文件夹(UDF)中。如果不指定 UDF 的位置,默认会尝试在默认位置(多数配置下会是 exe 旁边)创建,因此安装在 Program Files 下的应用程序会写入失败,出现初始化错误。这是一种典型的「发布之后才发现」的问题:在开发机(调试运行、可写入的文件夹)上运行正常,一旦安装就启动不了。4

对策很简单,就是像前面的代码示例一样,始终明确指定 %LOCALAPPDATA% 下的应用专用文件夹。同时也要在设计中纳入以下几点。

  • UDF 按用户、按应用程序分开(不要在多个应用程序间共享)
  • 不要放在网络磁盘上(会导致速度下降、损坏、数据丢失)4
  • 事先确定在卸载或「清除登录信息」功能中删除 UDF 的步骤(如果运维人员不知道 Cookie 或站点数据存放的位置,在整理离职员工的终端设备等场合就容易遗漏)

可以把这件事看作前一篇文章〈业务用 Windows 应用程序的本地数据存储〉中提到「不要写入 exe 旁边」原则的 WebView2 版本。

5. 原生 ⇔ Web 的联动设计

要让 WebView2 不只是一个「单纯的浏览器画框」,关键就在于原生代码与 Web 内容之间的相互联动。手段主要有两种。1

5.1 Web 消息(基本形式)

原生端用 PostWebMessageAsJson 发送 JSON,Web 端用 window.chrome.webview.addEventListener("message", ...) 接收;反方向则是 window.chrome.webview.postMessage(...)WebMessageReceived 事件。因为是松耦合,而且可以在单一处检查对外暴露的操作,所以应先把这个方式当作默认的联动手段

// 原生 → Web
webView.CoreWebView2.PostWebMessageAsJson(
    JsonSerializer.Serialize(new { type = "deviceStatus", connected = true }));

// Web → 原生
webView.CoreWebView2.WebMessageReceived += (s, e) =>
{
    // 不处理非预期来源(例如页面已跳转到外部站点后)的消息
    if (!e.Source.StartsWith("https://internal.example.co.jp/", StringComparison.Ordinal))
        return;

    AppMessage? msg;
    try { msg = JsonSerializer.Deserialize<AppMessage>(e.WebMessageAsJson); }
    catch (JsonException) { msg = null; }
    if (msg?.Type is null)
        return;  // 格式不正确的在这里丢弃(需要的话记录日志)

    // 按 type 只执行允许的操作
};

Web 端(JavaScript)就这样接收。不需要特别的库,只要使用 WebView2 注入的 window.chrome.webview 对象即可。

// 接收来自原生端的消息
window.chrome.webview.addEventListener("message", (e) => {
    if (e.data.type === "deviceStatus") {
        updateStatusBadge(e.data.connected);
    }
});

// 向原生端发送请求
document.getElementById("print-label").addEventListener("click", () => {
    window.chrome.webview.postMessage({ type: "printLabel", copies: 2 });
});

接收端应如上面的代码一样,先检查 e.Source(发送消息的页面 URI),然后严格执行「用白名单检查消息的 type,非预期的情况一律忽略并记录日志」。由于 Web 端只要一个链接、一次重定向就可能跳转到外部站点,因此请不要因为「现在显示的应该是自己的页面」这种假设而省略来源检查。反过来说,在 Web 端也可以加入 window.chrome.webview 不存在时(例如用普通浏览器打开时)的降级逻辑,这样就能单独用浏览器调试 Web UI 部分,提升开发效率。

5.2 暴露宿主对象(功能强大但应限定使用)

使用 AddHostObjectToScript,就能让 .NET/COM 对象直接被 JavaScript 调用。内部机制其实是 COM,本公司长期经手的 COM 技术在这种地方依然活跃,是相当有趣的一点,但让 Web 内容直接握有原生对象,也意味着一旦页面被入侵,影响范围也会同样大。暴露的对象应限定于自家管理的内容,暴露的方法也应压缩到必要的最小限度。原则是:可能显示不受信任页面的 WebView,不应注册宿主对象。

5.3 加载本地内容

如果要把 HTML/JS 随应用程序一起发布并显示,固定做法不是直接读取 file://,而是用 SetVirtualHostNameToFolderMapping 把文件夹映射到虚拟主机名。这样一来,内容就会拥有类似 https://appassets.example/ 的来源(origin),因此像 localStorage 这类以来源为前提的 Web API 就能正常使用,也能指定跨域访问的允许级别。主机名应使用实际上不可能存在的保留域名(例如 .example),访问类型则从必要的最小限度(先设置为 DenyCors)开始。7

6. 摆脱 IE 模式与 WebView2 ── ActiveX 无法运行

这是本文最重要的一节。IE 模式之所以能够延续使用,是因为 Edge 内部运行着真正的 IE11(Trident 引擎),ActiveX 与浏览器辅助对象(BHO)能够照常运行5 另一方面,WebView2 基于 Chromium,没有承载 ActiveX 的机制。也就是说,

「把在 IE 模式下运行的内部系统,换成用 WebView2 制作的外壳」,只有在不依赖 ActiveX 的界面上才能成立。

必须在这样的限制之下规划迁移计划。现实可行的迁移顺序如下。

  1. 盘点现状:把依赖 IE 模式的页面分类为「使用 ActiveX 等 IE 专属技术的界面」与「只是架构老旧的界面」(可以直接沿用〈IE 模式文章〉中的站点列表盘点方法)。
  2. 非 IE 专属的界面:改造为支持现代浏览器,直接用 Edge 显示;若想集成到业务终端设备的应用程序中,则改用 WebView2 外壳承载。
  3. 依赖 ActiveX 的界面:把原本由 ActiveX 负责的功能(串口通信、文件访问、专用设备控制等)迁移到原生端(WebView2 的宿主应用程序),重新设计为通过 Web 消息调用的形式。可以想象成把「浏览器里的 ActiveX」翻转成「应用程序里的 Web UI +原生处理」。
  4. 至于 ActiveX 本身要保留、封装,还是替换,可以直接套用〈ActiveX/OCX 该保留、封装,还是替换 - 判断表〉的判断标准。

第 3 点的重新设计正是引入 WebView2 的实质工作量所在,在规划阶段就必须先调整好预期:这并不是「引入 WebView2 就能摆脱 IE 模式」这么简单的事。反过来说,只要完成把 ActiveX 功能迁移到宿主应用程序的设计,UI 就能改用 Web 技术自行开发、维护,而分发方式又能以桌面应用程序的形式统一管控,可以两者兼得。

7. 内部系统一定会被问到的实现课题

在评估是否采用 WebView2 时,业务端一定会提出几个问题。这里先梳理一下。

7.1 打印与报表

「用 IE 的时候,点击打印按钮就会打印出报表」这样的需求,在 WebView2 中可以用两种方式满足。

  • 直接打印界面:用 CoreWebView2.ShowPrintUI() 显示打印对话框,或是用 PrintAsync 进行无人值守打印。因为效果等同于浏览器的打印功能,CSS 的打印设置(@media print)可以直接生效。
  • 输出为 PDF:用 PrintToPdfAsync 可以把当前显示的页面转存为 PDF 文件。对于「报表输出为 PDF 后保存到共享文件夹」这类业务流程,因为原生端能统一管理文件名与保存位置,比较适合采用这个方式。1

像是需要像素级对齐的复写式单据这类报表,与其用 Web 打印硬调整到位,也应该考虑改用原生端的报表输出方式(也就是〈Excel 报表制作方法〉一文中整理的方式)。

7.2 文件的下载与上传

下载功能即使不做任何设置,默认也会像浏览器一样运行,但在业务应用程序中,固定做法是通过 DownloadStarting 事件介入处理。可以做到固定保存位置、按扩展名判断允许或拒绝、把默认的下载 UI 换成应用程序自己的通知等管控。1 上传(<input type="file">)则不需要特别实现,会直接打开操作系统的文件选择对话框。

7.3 认证与 SSO

如果内部 Web 系统采用 Windows 集成身份验证(NTLM/Kerberos),在 WebView2 上大致也能像浏览器一样通过。对于使用 Basic 认证的旧系统,可以通过 BasicAuthenticationRequested 事件提供凭据,甚至可以做到不显示登录界面──不过这个凭据要存储在哪里,正是〈DPAPI 文章〉中提到的话题。如果想让 Microsoft Entra ID(原 Azure AD)的 SSO 通过操作系统的登录信息通过验证,可以考虑启用环境选项 AllowSingleSignOnUsingOSPrimaryAccount

由于 Cookie 存储在 UDF 中,即使重启应用程序,登录状态也会保持。如果希望「通过注销功能确实清除会话」,请加入用 CookieManager 明确删除 Cookie 的实现。

7.4 开发时的调试

因为 WebView2 内部就是 Chromium,开发时可以直接使用 F12 的 DevTools(CoreWebView2Settings.AreDevToolsEnabled 默认启用)。此外,如前面所述,只要把 Web UI 端设计成「单独用浏览器也能运行」,就能把 UI 的开发、调试当成普通的 Web 开发来进行,只在 WebView2 上确认原生联动部分,做到分工。也就是说,即使把 Web 资产封装进应用程序里,开发体验依然能保持 Web 开发的样子。

8. 安全设计的要点

由于 WebView2 应用程序是「内置浏览器的应用程序」,因此应以相当于浏览器的威胁模型来思考。

  • 限定显示的内容:在 NavigationStarting 中用内部域名的白名单检查跳转目标,非预期的 URL 就转交给默认浏览器打开(NewWindowRequested 也做同样的处理)。
  • 缩小跨越信任边界的功能:宿主对象的暴露范围、Web 消息允许的操作都应压缩到最小限度。把可能显示外部站点的 WebView 与具有原生联动的 WebView 分开,也是有效的做法。
  • 按环境调整提供给用户的功能:通过 CoreWebView2Settings,可以只保留必要程度的「浏览器特性」。在信息一体机终端或现场终端上提前收紧设置,能减少事故发生。
  • 若使用 Fixed Version,须自行负责更新计划:如前所述,漏洞修复的发布会变成应用程序端的责任。6

以下列出 CoreWebView2Settings 中经常调整的项目。建议把「正式版构建要怎么设置」纳入发布前的检查清单中。

设置 默认值 现场终端・正式环境的典型做法
AreDevToolsEnabled(F12 开发者工具) 启用 正式环境中禁用
AreDefaultContextMenusEnabled(右键菜单) 启用 不想让用户使用「后退」「重新加载」的界面中禁用
AreBrowserAcceleratorKeysEnabled(Ctrl+F5 等快捷键) 启用 信息一体机用途中禁用
IsStatusBarEnabled(显示链接目标) 启用 按喜好决定
IsZoomControlEnabled(Ctrl+滚轮缩放) 启用 布局会错乱的业务界面中禁用
AreHostObjectsAllowed(宿主对象) 启用 不使用的话就禁用

与其说这些是「禁用就安全」,不如说它们是用来「关闭应用程序预期操作之外的入口」的工具。若要全面提升 Windows 应用程序的安全性,也请一并参考〈Windows 应用程序安全最低检查清单〉。

9. 采用判断汇总

架构 适合的场景 注意事项
用 Edge(浏览器)显示 普通的内部 Web 系统 无法集成应用程序、无法进行原生联动
既有应用程序+WebView2 承载部分 Web UI 以界面为单位进行现代化改造、复用 Web 资产 UDF、运行时分发、联动设计(本文)
WebView2 外壳+原生功能移植 依赖 ActiveX 的 IE 模式资产的出口 重新实现 ActiveX 功能是核心工作
Electron 等 必须跨平台的场景 若只用于 Windows 会偏重,发布物与内存占用增加
用纯原生(WPF 等)重写 没有 Web 资产、以离线为前提 需要与开发成本权衡

如果是「仅限 Windows 使用的内部应用程序,想用 Web 技术制作 UI」,那么 WebView2 会比 Electron 更适合作为默认选项。因为可以与操作系统共享运行时,所以发布负担较轻,与既有 .NET 资产的集成也更直观。

10. 总结

WebView2 是一项可以把基于 Chromium 的 Web UI,作为组件嵌入到 Windows 应用程序中的技术,与内部系统的现代化改造相当契合。引入时的实务要点有三个:Evergreen 运行时的存在性检测与更新跟进、明确指定用户数据文件夹,以及以 Web 消息为基础的联动设计。而在规划层面,则要正视 ActiveX 无法运行的限制,把将 ActiveX 功能迁移到原生端的重新设计,放在工作量的核心位置

如果一边留意着 IE 模式的期限,一边在想「差不多该找出口了」,那么从盘点目标系统、拆解依赖 ActiveX 部分的功能开始,会是最踏实的做法。这方面的推进方式,有不少地方需要结合实际的系统架构才容易判断,如果拿不定主意,欢迎与我们咨询。

相关文章

相关咨询领域

小村软件有限公司承接依赖 IE 模式、ActiveX 的内部系统出口设计、运用 WebView2 进行分阶段现代化改造,以及将 Web UI 集成到既有 Windows 应用程序的相关咨询。

参考链接

  1. Microsoft Learn, Overview of WebView2 APIs。关于导航管理、本地内容加载、宿主⇔Web 间通信(Web 消息、宿主对象)等 WebView2 功能的整体概览。  2 3 4 5

  2. Microsoft Learn, Distribute your app and the WebView2 Runtime。关于通过启动安装程序/独立安装程序进行分发、检测是否已安装、用 NewBrowserVersionAvailable 跟进更新,以及 Fixed Version 的内置步骤(超过 250MB)。  2 3 4

  3. Microsoft Learn, Evergreen vs. fixed version of the WebView2 Runtime。关于运行时两种分发模式的差异、Windows 11 的标准内置,以及 Fixed Version 的优缺点。  2

  4. Microsoft Learn, Manage user data folders。关于用户数据文件夹的作用、自定义 UDF 所需的读写权限,以及放在网络磁盘上会导致速度下降、崩溃、数据丢失的原因。  2 3

  5. Microsoft Learn, What is Internet Explorer (IE) mode?。关于 IE 模式以 Trident (MSHTML) 引擎运行,并支持 ActiveX 控件与浏览器辅助对象(也就是基于 Chromium 的 WebView2 并不具备这项支持)。  2

  6. Microsoft Learn, Development best practices for WebView2 apps。关于推荐使用 Evergreen、运行时更新的处理方式、功能检测,以及使用 Fixed Version 时定期更新的必要性。  2 3

  7. Microsoft Learn, Using local content in WebView2 apps。关于通过虚拟主机名映射加载本地内容、赋予来源(origin)带来的优点,以及访问类型(DenyCors 等)的指定方式。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

WebView2 能运行 ActiveX 吗?
不能。IE 模式是在 Edge 内部运行真正的 IE11(Trident 引擎),因此 ActiveX 能照常运行;而 WebView2 基于 Chromium,没有承载 ActiveX 的机制。因此,依赖 ActiveX 的界面不能仅靠「换成 WebView2」了事,必须把原本由 ActiveX 负责的串口通信、文件访问、专用设备控制等功能迁移到原生端(宿主应用程序),重新设计为通过 Web 消息调用的架构。这个重新设计正是引入 WebView2 的实质工作量所在。
WebView2 的运行时应该选 Evergreen 还是 Fixed Version?
原则上应选择 Evergreen(自动更新的共享运行环境),因为安全补丁会自动应用、磁盘占用也较小,官方也明确建议使用。虽然 Windows 11 已经标准内置,但仍有部分尚未安装的终端设备,因此需要在安装程序中内置存在性检测与启动安装(bootstrap)流程。Fixed Version(应用程序内置)的内置文件会超过 250MB,而且需要自行负责发布浏览器引擎的漏洞修复,因此只应在离线的生产设备或变更管理极其严格的环境等有限场景下选用。
为什么 WebView2 应用程序安装后启动不了?
最典型的入门陷阱就是用户数据文件夹(UDF)的问题。WebView2 会把 Cookie、缓存、权限存放在 UDF 中,如果不指定位置,默认会尝试在 exe 旁边创建,导致安装在 Program Files 下的应用程序写入失败而出现初始化错误。这是一种典型的「在开发机上运行正常,一旦安装就启动不了」的问题。对策是在创建 `CoreWebView2Environment` 时,始终明确指定 `%LOCALAPPDATA%` 下的应用专用文件夹。放在网络磁盘上也会导致速度下降、数据损坏,应当避免。
WebView2 中原生代码与 Web 页面要怎么联动?
基本方式是通过 Web 消息进行松耦合的联动。原生端使用 `PostWebMessageAsJson` 发送 JSON,Web 端通过 `window.chrome.webview` 的 message 事件接收;反方向则是 `postMessage` 与 `WebMessageReceived` 事件。接收端应通过 `e.Source` 检查消息来源的 URI,并用白名单检查消息的 type,这一点非常重要。此外也可以用 `AddHostObjectToScript` 直接暴露 .NET/COM 对象,但一旦页面被入侵,影响范围会相当大,因此应限定于自家管理的内容,并把暴露的方法压缩到最小限度。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表