目录
为 WPF 或 WinForms 应用程序选择浏览器组件,对于那些搜索基于Chrome的解决方案的人来说, DotNetBrowser[1]和CefSharp[2]是最明显的选择。
本文是我们的客户在考虑其项目的开源库和商业库时提出的最常见比较点的汇编。
引擎
CefSharp 实际上是 Chromium Embedded Framework[3] (CEF) 的 .NET 包装器。包装通过 C++/CLI 完成。
DotNetBrowser 在底层不使用 CEF 或 C++/CLI。相反,它采用了自己的方法直接与 Chromium 集成。它启动一个功能齐全的 Chromium 引擎,并通过进程间通信 (IPC) 与其进行通信。
架构
在 CefSharp 中,Chromium 引擎直接在您的 .NET 进程中初始化[4]。初始化和关闭都必须在主应用程序线程(通常是 UI 线程)中执行。在不同的线程中调用它们通常会导致冻结。
此外,每个进程可以执行一次初始化和关闭。这个限制来自 CEF 本身。在执行关闭后尝试重新初始化 CefSharp 将导致错误。
CefSharp architecture
在 DotNetBrowser 中,Chromium 引擎在单独的本机进程中进行初始化。不需要在主 UI 线程上执行此操作——即使在工作线程中也可以执行此操作。
您可以同时初始化和使用具有不同配置的多个 Chromium 引擎,这在 CefSharp 中是不可能的。您可以在不再需要 Chromium 时将其关闭并随时重新初始化。
DotNetBrowser architecture
稳定性和内存使用
在单独的进程中运行 Chromium 有更多优点:
-
在这种情况下,内存消耗要低得多,这对于 32 位应用程序来说似乎很关键。
-
在 CefSharp 中,如果 CEF 或 C++/CLI 绑定内部出现问题,这将导致整个 .NET 应用程序崩溃而无法处理这种情况。这就不太妙了,因为 .NET 应用程序可能会丢失或损坏用户的数据。
对于 DotNetBrowser,Chromium 内部的错误不会导致 .NET 应用程序崩溃。此外,甚至可以在托管代码中正确检测和处理这一切。例如,如果发生这种情况,那么您可以重新初始化 Chromium 并恢复用户会话。
应用程序域
由于其架构,CefSharp 不能在非默认 AppDomain 中使用[5]。因此,它不能用于通过 VSTO 插件或 Excel-DNA 将 Chromium 嵌入到 Office 应用程序中[6]。Office VSTO 将加载项加载到单独的 AppDomain 中以进行隔离。DotNetBrowser 在非默认 AppDomain 中运行。事实上,可以在不同的 AppDomain 中创建多个 Chromium 引擎并同时使用它们。因此,DotNetBrowser 可用于创建 VSTO 加载项。
AnyCPU
在针对 AnyCPU 的应用程序中使用 CefSharp 时,您会发现它在这些应用程序的 64 位环境中无法正常工作。
这儿有几个选项[7]可以解决这个问题。其中之一是让您的应用程序始终在 32 位模式下运行,另一个更复杂,需要修改项目文件(.csproj
或 .vbproj
)和代码。
在 DotNetBrowser 中,AnyCPU 支持开箱即用。因此,不需要类似的调整。
H.264, AAC
视频和音频通常使用专有编解码器进行编码,例如 H.264 和 AAC。此媒体无法在 CefSharp 中播放。
要在 CefSharp 中启用这些编解码器,您需要在启用专有编解码器的情况下自行重建 CEF。这是一项相当复杂的任务,可能需要长达一个月的时间[8]。
在 DotNetBrowser 中默认禁用专有编解码器。可以通过编程方式启用它们,而无需重建库:
InitializeCodecs.cs
IEngine engine = EngineFactory.Create(new EngineOptions.Builder
{
ProprietaryFeatures = ProprietaryFeatures.H264 | ProprietaryFeatures.Aac
}.Build());
安全
Chromium 通过利用操作系统为它们提供的安全性来限制其渲染器和实用程序进程。此功能称为 Chromium沙箱[9]。其主要目的是防止第三方代码对计算机进行持久更改或访问机密信息。
CefSharp 不支持 Chromium 沙箱[10]。这个限制来自 CEF 本身。
DotNetBrowser 支持沙箱并默认启用。如有必要,可以在初始化期间将其禁用[11]。
CefSharp 在 .NET 进程中启动 Chromium。这使您的应用程序容易受到 CEF 和 Chromium 中的漏洞的影响。如果恶意软件获得了对 Chromium 内存的访问权,它也会获得对 .NET 内存的访问权。
DotNetBrowser 在单独的进程中启动 Chromium。
Chromium 漏洞保留在 Chromium 中。
Visual Studio设计器
现代 WPF 和 Windows 窗体应用程序通常是在设计器的帮助下在 Visual Studio 中创建的。这种方法总体上简化了 UI 创建并节省了大量时间和精力。
CefSharp 提供有限的设计器支持[12]。如果应用程序本身以 x86 为目标,则其控件将在设计器中正确处理。AnyCPU 可能会工作,但尚未经过彻底测试。
DotNetBrowser 控件是纯 UI 控件,它们在代码中显式初始化。您可以在设计器中不受任何限制地使用它们。安装 NuGet 包或 VSIX 扩展后,BrowserView 控件出现在工具箱中。它可以像任何其他常规 UI 控件一样被拖到窗体或窗口上。
嵌入应用程序 UI
CefSharp 提供 WPF 和 Windows 窗体支持。但是,它的 WPF 实现只能在离屏渲染模式[13]下工作。此实现具有有限的触摸屏和 IME[14] 支持。
DotNetBrowser 在两种渲染模式下同时支持 WPF 和 Windows 窗体。在硬件加速模式下,触摸、手势和 IME 由 Chromium 自行处理,因此它们开箱即用。在离屏模式下,存在一些已知的限制[15]。
以下是将 CefSharp 嵌入 WPF 窗口的方法:
<Window x:Class="CefSharpWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
Title="MainWindow" Height="450" Width="800">
<Grid>
<wpf:ChromiumWebBrowser Address="https://www.google.com"/>
</Grid>
</Window>
就是这样,在最简单的情况下,不再需要编写代码。但是,在这种情况下,CefSharp 初始化和关闭是隐式执行的,很难确定它是否已经在某个点初始化。
将 DotNetBrowser 嵌入 WPF 窗口的过程需要额外的步骤。
例如:
MainWindow.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WPF="clr-namespace:DotNetBrowser.Wpf;assembly=DotNetBrowser.Wpf"
x:Class="Embedding.Wpf.MainWindow"
Title="MainWindow" Height="480" Width="800" Closed="Window_Closed">
<Grid>
<WPF:BrowserView Name="browserView" />
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private const string Url = "https://www.google.com";
private readonly IBrowser browser;
private readonly IEngine engine;
public MainWindow()
{
// Create and initialize the IEngine instance.
EngineOptions engineOptions = new EngineOptions.Builder
{
RenderingMode = RenderingMode.HardwareAccelerated
}.Build();
engine = EngineFactory.Create(engineOptions);
// Create the IBrowser instance.
browser = engine.CreateBrowser();
InitializeComponent();
// Initialize the WPF BrowserView control.
browserView.InitializeFrom(browser);
browser.Navigation.LoadUrl(Url);
}
private void Window_Closed(object sender, EventArgs e)
{
browser?.Dispose();
engine?.Dispose();
}
}
在这里,大部分代码都与 Chromium 实例和 IBrowser
实例的显式初始化和关闭有关。UI 控件初始化是通过调用 InitializeFrom()
显式执行的。这种方法可以更好地控制初始化和关闭过程,并且更容易自定义初始 Chromium 配置。
高DPI
在 CefSharp 中,浏览器子进程的 默认 DPI 感知[16] 是 Per-Monitor。因此,桌面应用程序应具备 DPI 感知功能,才能在高 DPI 显示器(DPI 比例设置大于 100% 的显示器)上正确运行。在其他情况下,浏览器内容可能无法正确呈现,例如:
DotNetBrowser 以不同的方式支持高 DPI。在初始化过程中,它会检查当前进程的 DPI 感知,并为相应的 Chromium 引擎设置匹配的 DPI 感知。因此,无需让您的应用程序显式识别 DPI 以避免在高 DPI 显示上呈现伪影。
Headless
DotNetBrowser 和 CefSharp 都可以在没有 UI 的应用程序中使用。
在 CefSharp 中,CefSharp.OffScreen.ChromiumWebBrowser
用于此目的。初始化过程通常保持不变。但是,如果您的代码使用 async/await 模式,则需要使用同步上下文来确保在主线程上而不是在不同的工作线程上执行初始化和关闭。
要在没有 UI 的应用程序中使用 DotNetBrowser,您需要像往常一样执行初始化。在这种情况下,没有需要初始化的 BrowserView
。即使您的代码使用async/await模式,也无需创建和使用同步上下文。
API和功能
这两种产品都有许多可用的功能。在本文中,我将比较几个最重要的,以展示 API 的不同之处。
DOM访问
在 CefSharp 中,您只能通过执行 JavaScript 调用来访问 DOM。
例如:
CefSharpDom.cs
var script = @"
document.getElementsByName('question')[0].value = 'CefSharp Example';
document.getElementsByName('btn')[0].click();
";
browser.ExecuteScriptAsync(script);
DotNetBrowser 提供了丰富的 DOM API,可用于直接从 .NET 执行以下操作:
-
访问和修改 DOM 树;
-
更改 HTML 元素属性;
-
订阅 DOM 事件并从 .NET 代码中调度它们。
例如,以下是如何在 DotNetBrowser 中的网页上执行相同的操作:
DotNetBrowserDom.cs
IDocument document = browser.MainFrame.Document;
(document.GetElementByName("question") as IInputElement).Value = "DotNetBrowser Example";
document.GetElementByName("btn").Click();
因此,在 DotNetBrowser 中与网页执行复杂的交互要方便得多。无需编写难以调试和支持的复杂 JavaScript 代码。DotNetBrowser 中的 DOM API 不是一组 JavaScript 调用的包装器。它直接对 Blink 引擎进行 IPC 调用。
与JavaScript交互
执行JavaScript并处理结果
CefSharp 和 DotNetBrowser 都提供了在网页上执行 JavaScript 的能力。
在 CefSharp 中,有两种方法可用于此目的,ExecuteJavaScriptAsync
和 EvaluateScriptAsync
。两者都可用于浏览器本身(通过扩展方法)或其中的一个框架:
CefSharpExecuteJs.cs
// Execute JavaScript without returning a result. The method returns
// before the script has actually been executed.
browser.ExecuteJavaScriptAsync("alert('All Resources Have Loaded');");
// Evaluate some Javascript code. The script will be executed asynchronously
// and the method returns a Task encapsulating the response from the
// JavaScript.
JavascriptResponse response = await browser.EvaluateScriptAsync(script);
然后使用 JavascriptResponse.Result
获取执行结果。
可能的结果类型有 bool
, int
, long
, double
, string
, List<object>``,
IDictionary<string, object>``, 和 IJavascriptCallback
。这里的集合是 JavaScript 集合的快照表示,而 IJavascriptCallback
是一种 JavaScript 函数表示,可用于从 .NET 端执行它。
在 DotNetBrowser 中,有 IFrame.ExecuteJavaScript()
用于此目的。此方法的通用版本可用于显式指定预期的返回类型:
DotNetBrowserJavaScript.cs
string title = await browser.MainFrame.ExecuteJavaScript<string>("document.title");
IJsObject window = await browser.MainFrame.ExecuteJavaScript<IJsObject>("window");
IElement body = await browser.MainFrame.ExecuteJavaScript<IElement>("document.body");
这里的主要区别是可以将 JavaScript 对象表示为IJsObject
。使用此接口,您可以访问和修改 JavaScript 对象的属性并调用其方法。在 .NET 端对 IJsObject
所做的所有更改都将立即反映在 JavaScript 端。此外, ExecuteJavaScript
调用可以返回一个 IElement
,这是一个 DOM 元素的表示,您可以使用它来访问和修改 DOM 属性或订阅 DOM 事件。
从JavaScript调用.NET
CefSharp 和 DotNetBrowser 都可以使网页上的 JavaScript 可以访问 .NET 对象,但是,CefSharp 存在一些特定的限制。
CefSharp JavaScript 绑定可用于 JavaScript 和 .NET 之间的通信。但是,CefSharp 不允许[17]将Form, Window 或任何Control注入 JavaScript。另外,CefSharp 只支持调用注入对象的方法。如果需要设置属性,则必须修改类并创建 Get/Set 方法。
在 DotNetBrowser 中,您可以将任何对象注入 JavaScript,包括 Form, Window 和 Control 对象。执行注入后,您可以访问注入的 .NET 对象的公共字段、属性和方法。此外,DotNetBrowser 支持从 JavaScript 访问索引属性(使用字符串或数字索引器)。如果您需要从 JavaScript 访问 .NET 集合,这会很有帮助。
截屏
两种解决方案都支持在浏览器不可见时进行截屏。但是,API 有明显不同。以下是代码片段:
CefSharpScreenshot.cs
// Take a screenshot
var bitmapAsByteArray = await browser.CaptureScreenshotAsync();
// Save the screenshot as PNG
var screenshotPath = Path.GetFullPath("screenshot.png");
File.WriteAllBytes(screenshotPath, bitmapAsByteArray);
DotNetBrowserScreenshot.cs
// Take a screenshot
DotNetBrowser.Ui.Bitmap image = browser.TakeImage();
// Convert the screenshot to System.Drawing.Bitmap and save it as PNG
System.Drawing.Bitmap bitmap = image.ToBitmap();
bitmap.Save("screenshot.png", ImageFormat.Png);
主要的 DotNetBrowser DLL 不使用 System.Drawing 中的类型,因为它的限制[18],因此,它提供了自己的类型。然后可以通过 DotNetBrowser.Wpf
或 DotNetBrowser.WinForms
中提供的扩展方法将此类型转换为常规 System.Drawing.Bitmap。
分发和部署
CefSharp 需要 Microsoft Visual C++ 运行时存在于环境中[19]。Visual C++ 2015 是最低版本,但所需的确切版本取决于 Chromium 版本。因此,需要在您希望运行基于 CefSharp 的应用程序的每台机器上预安装 Microsoft Visual C++ Redistributable Package,将其设置为安装程序的依赖项,或将其 DLL 打包为应用程序的一部分,并确保 CefSharp 正确找到它们。
在 DotNetBrowser 中,所有必需的 Chromium 二进制文件和 DLL 都已打包到 DotNetBrowser DLL 中,并且可以在执行期间自动提取。您无需预先安装 Microsoft Visual C++ Runtime 即可使用 DotNetBrowser。
支持和更新
CefSharp 是一个开源项目。如果您发现错误或缺少功能,您可以提出建议[20]。
DotNetBrowser是为使用.NET开发软件的商业公司设计和创建的商业产品,对集成第三方解决方案的质量和支持有很高的要求。自 2015 年以来,TeamDev 开发并支持 DotNetBrowser。
我们所有已订阅有效标准支持[21]的客户都可免费使用所有DotNetBrowser 新版本并获得技术支持。如果您发现错误或缺少功能,我们将应用修复程序,实施所需功能,并根据任务的复杂性在几天或几周内为您提供新版本的库。
几乎每个月都会发布一个新版本的 DotNetBrowser。我们会在 Chromium 正式发布后的 3-4 周内将 Chromium 升级到最新的稳定版本(带有最新的安全补丁和修复的漏洞)。
概括
因为开源和免费,CefSharp 被广泛使用。它很容易为基本案例进行配置,并且拥有广泛的文档和活跃的开源开发者社区。
但是,它具有来自其设计和架构的限制。缺少沙盒支持使其安全性降低,并且进程内方法会影响稳定性并增加应用程序的内存使用量。
它也不能用于暗示在单独的 AppDomain(如 VSTO)中运行代码的环境。对于其他一些情况,例如播放使用专有编解码器编码的内容,您必须自己构建、更新和维护 CEF。这需要大量额外的工作和基础设施。
与加载的网页的复杂交互会通过 JavaScript 注入执行,这使得生成的代码更难调试和支持。
DotNetBrowser 简化了所有这些案例的开发过程——由于它的进程外架构,它可以用于创建更稳定和安全的解决方案。使用 DotNetBrowser,您可以与需要在单独的 AppDomain 中运行代码的应用程序集成,并在网页上执行复杂的操作,而无需进行大量的 JavaScript 注入。
除此之外,如果您觉得缺少哪些功能、发现问题或有其他任何疑问,您可以随时与我们联系并获得帮助[22]。
参考资料
[1]
DotNetBrowser: https://www.teamdev.com/dotnetbrowser?utm_campaign=dotnetbrowser-articles&utm_medium=article&utm_source=medium
[2]
CefSharp: http://cefsharp.github.io/
[3]
Chromium Embedded Framework: https://bitbucket.org/chromiumembedded/cef/src/master/
[4]
引擎直接在您的 .NET 进程中初始化: https://github.com/cefsharp/CefSharp/wiki/General-Usage#initialize-and-shutdown
[5]
CefSharp 不能在非默认 AppDomain 中使用: https://github.com/cefsharp/CefSharp/wiki/General-Usage#need-to-knowlimitations
[6]
将 Chromium 嵌入到 Office 应用程序中: https://docs.microsoft.com/en-us/visualstudio/vsto/architecture-of-vsto-add-ins
[7]
几个选项: https://github.com/cefsharp/CefSharp/issues/1714
[8]
可能需要长达一个月的时间: https://greenlightstudionet.wordpress.com/2019/10/09/stream-netflix-in-your-c-sharp-program/amp/
[9]
Chromium沙箱: https://www.google.com/googlebooks/chrome/med_26.html
[10]
不支持 Chromium 沙箱: https://github.com/cefsharp/CefSharp/wiki/General-Usage#need-to-knowlimitations
[11]
初始化期间将其禁用: https://dotnetbrowser-support.teamdev.com/docs/guides/gs/chromium.html#windows?utm_campaign=dotnetbrowser-articles&utm_medium=article&utm_source=medium
[12]
提供有限的设计器支持: https://github.com/cefsharp/CefSharp/wiki/General-Usage#need-to-knowlimitations
[13]
离屏渲染模式: https://github.com/cefsharp/CefSharp/wiki/General-Usage#offscreen-rendering-osr
[14]
IME: https://github.com/cefsharp/CefSharp/issues/1262
[15]
已知的限制: https://dotnetbrowser-support.teamdev.com/docs/guides/gs/browser-view.html#mouse-keyboard-touch-drag-and-drop?utm_campaign=dotnetbrowser-articles&utm_medium=article&utm_source=c-sharpcorner
[16]
默认 DPI 感知: https://github.com/cefsharp/CefSharp/wiki/General-Usage#high-dpi-additional-info
[17]
不允许: https://github.com/cefsharp/CefSharp/wiki/General-Usage#binding-an-async-object-in-javascript
[18]
它的限制: https://docs.microsoft.com/en-us/dotnet/api/system.drawing?view=netstandard-2.0#remarks
[19]
Microsoft Visual C++ 运行时存在于环境中: https://github.com/cefsharp/CefSharp/wiki/Output-files-description-table-%28Redistribution%29#Requirements
[20]
提出建议: https://github.com/cefsharp/CefSharp
[21]
标准支持: https://dotnetbrowser-support.teamdev.com/getting-help/#standard-support?utm_campaign=dotnetbrowser-articles&utm_medium=article&utm_source=medium
[22]
联系并获得帮助: https://dotnetbrowser-support.teamdev.com/getting-help?utm_campaign=dotnetbrowser-articles&utm_medium=article&utm_source=medium