在 .NET Framework 中集成 CefSharp
参考文档:
本文索引:
- 快速开始
- 进程 Process
- 线程 Threads
- Initialize 和 Shutdown
- 配置
- IBrowser,IFrame 和 IBrowserHost 对象
- Handlers
- 配置代理
- 请求上下文
- 高 DPI 显示器支持
- Javascript 集成
快速开始
创建一个新的 WPF 项目,目标框架为 .NET Framework 4.5.2,本项目将使用 CefSharp 3 库,该库仅支持 x86 和 x64 应用程序,这意味着项目必须指定编译目标,不支持 Any Cpu。创建项目后,首先打开配置管理器,Solution -> Configuration Manager:
由默认 Debug 配置创建新的 x86 和 x64 的编译配置:
完成配置创建后,引入 CefSharp.Wpf Nuget 包:
现在,保存所有更改并关闭 Visual Studio,因为 CefSharp 需要完全重启以加载其依赖项。重启打开 Visual Studio 项目,编译。
进程 Process
CEF 运行时包含多个进程
browser进程: 主进程,负责创建窗口,渲染和网络访问,该进程大部分情况下等同于「宿主应用程序」所在的进程,且执行大部分逻辑。render进程: 渲染和与Javascript交互(如 JS 对象绑定)由另一个render进程处理,进程模型将会为每一个单独的Origin([scheme] + [domain]) 开辟一个render进程。plugin进程: 负责处理诸如Flash等插件gpu进程: 处理渲染加速等
线程 Threads
不同级别的进程可管理多个线程,browser 进程包含了以下线程:
- UI 线程:
browser进程的主线程,默认情况下CefSharp使用setting.MultiThreadedMessageLoop = true设置,这使得CEF的 UI 线程与宿主应用程序的 UI 线程使用不同的线程。 - IO 线程: 在
browser进程中负责处理 IPC 和网络消息 - File 线程: 在
browser进程中负责与「文件系统」交互 - Renderer 线程:
render进程的主线程
Initialize 和 Shutdown
Initialize
Initialize 方法仅允许每个应用程序调用一次,该方法用于初始化底层 CEF 库,可以显式或隐式的调用该方法:
- 隐式调用: 首次创建
ChromiumWebBrowser实例时,将检查CEF是否已被初始化,如果没有,则使用默认配置调用初始化方法 - 显式调用: 当希望指定自定义配置时,可显式调用
CEF.Initialize(CefSettings settings)方法
Shutdown
ChromiumWebBrowser 的 WPF 和 Winform 的实现都在对应的 Exit 事件中默认调用了 Shutdown 方法。要禁用这一行为,可在任一 ChromiumWebBrowser 实例创建之前设置 CefSharpSettings.ShutdownOnExit = false。
Initialize和Shutdown都必须在应用程序的主线程中调用,如果在另外的线程中调用它们,应用程序将会挂起。
在 CefSharp.OffScreen 应用中,在应用程序退出前必须显式调用 Cef.Shutdown() 方法。
配置
CefSettings
CefSettings 覆盖了应用程序级别的配置,一些常见的配置包括:
BrowserSubprocessPath: 启用子进程的路径,通常不建议更改MultiThreadedMessageLoop: 默认为true,也可以设置为false并将Cef集成至现有应用程序的消息泵中,参考 https://github.com/cefsharp/CefSharp/issues/1748CommandLineArgsDisabled: 设置为true以禁用使用标准的Chromium命令行参数控制浏览器行为CachePath: 缓存数据在本机上存放的路径,若为空,则使用临时缓存和内存缓存。仅当该设置不为空时,HTML5 数据库(如localStorage)才会被持久化Locale: 传给 Blink 的本地化信息,en-US为默认值,可由命令行参数lang控制LogFile: Debug 日志使用的持久化目录与文件名。./debug.log为默认值。可由命令行参数log-file控制LogSeverity: 类似于 LogLevel,仅当等于或高于该级别的日志将会记录,可由log-severity命令行参数控制,可接收verbose、info、warning、error、error-report和disabled值。ResourceDirPath: 资源路径,可由resources-dir-path命令行参数控制LocalesDirPath: 本地化信息路径,可由locales-dir-path命令行参数控制RemoteDebuggingPort: 可在 1024 - 65535 之间取值,用以启用远程调试,可由另一个 CEF 或谷歌浏览器访问,可由remote-debugging-port命令行参数控制。
BrowserSettings
BrowserSettings 覆盖 ChromiumWebBrowser 实例级别的配置,具体参考 BrowserSettings 类型
IBrowser,IFrame 和 IBrowserHost 对象
IBrowser 和 IFrame 对象用于向浏览器发送命令及从回掉函数中接收状态信息,每一个 IBrowser 对象都至少包含一个 IFrame 对象代表顶层窗口。即,如果一个浏览器窗口加载了两个 <iframe> 元素将会包含 3 个 IFrame 对象(一个顶层 IFrame 和两个 <iframe>)。
在一个 IBrowser 对象中加载 url:
1 | browser.MainFrame.LoadUrl(url); |
IBrowserHost 代表更底层的浏览器方法,例如:
Print()ShowDevTools()CloseDevTools()StartDownload(string url)
Handlers
CefSharp 提供了一系列 Handler 对象,这些对象以 .NET 实现封装了浏览器的常见行为和事件。例如,当需要知道一个页面是否加载完成时,可侦听 LoadingStateChanged 事件。多数 Handler 的方法都提供了异步实现,所有 Handler 都遵循一个实现模式: 返回 bool 的方法意味着询问你是否需要自行处理,返回 false 表示使用默认实现,返回 true 表示由开发人员提供自定义实现。IWebBrowser 定义了以下常见的 IXXXHandler:
IDownloadHandler: 下载文件,进度通知,暂停,取消等IRequestHandler: 处理导航、重定向、资源加载通知等IDialogHandler: 文件对话框通知IDisplayHandler: 地址栏变更,状态信息,控制台信息,全屏模式更改通知等ILoadHandler: 加载状态信息、事件,弹出框通知信息ILifeSpanHandler: 弹出框弹出及关闭事件IKeyboardHandler: 键盘事件IJsDialogHandler: javascript 消息框/弹出框IDragHandler: 拖拽事件IContextMenuHandler: 定制右键菜单IFocusHandler: 焦点相关通知IResourceHandlerFactory: 拦截资源请求相关IGeolocationHandler: 地理位置请求相关IRenderProcessMessageHandler: 从render进程发送的自定义CefSharp消息相关IFindHandler: 与查找通知相关
通常,使用定制化的 Handler 需要在创建 ChromiumWebBrowser 之后立即对相应的 Handler 赋值:
1 | browser.DownloadHandler = new DownloadHandler(); |
各个 Handler 的具体用法参见 Handlers
配置代理
CEF 遵循使用同样的命令行命令来设置代理,如果代理有认证要求,可通过 IRequestHandler.GetAuthCredentials() 方法提供。
请求上下文
请求上下文(RequestContext)用于隔离 IBrowser 实例,包括可提供单独的缓存路径、单独的代理配置、单独的 Cookie 管理器及其他相关设置。RequestContext 有以下关键点:
- 默认情况下,提供一个全局
RequestContext,由多个IBrowser实例共享设置 - 可在运行时通过
Preferences更改某些设置,这种情况下不要使用命令行工具 - Winform 在创建
IBrowser实例之后立即设置RequestContext;OffScreen则须将RequestContext作为构造函数参数传入;WPF 则在 InitialComponent() 方法之后设置 - Plugin 加载通知通过
IRequestContextHandler接口处理 - 设置
RequestContextSettings.CachePath以持久化Cookie,localStorage等数据
高 DPI 显示器支持
如果应用程序经常显示为黑屏,则很有可能需要开启「高 DPI」支持。要启用「高 DPI」支持,需要在对应应用程序的 app.manifest 文件中加入相应的节点以通知 Windows 该程序支持「高 DPI」,
- WinForm: 加入
app.manifest并在第一时间调用Cef.EnableHighDPISupport() - WPF: 加入
app.manifest - OffScreen: 加入
app.manifest
app.manifest 类似于以下内容:
1 | <asmv3:application> |
Javascript 集成
从 .NET 中调用 Javascript 方法
Javascript 方法仅能在一个 V8Context 中执行,IRenderProcessMessageHandler.OnContextCreated 和 IRenderProcessMessageHandler.OnContextReleased 为 Javascript 代码的执行划定了一个清晰的边界。通常:
Javascript在Frame级别执行,每个页面至少包含一个FrameIWebBrowser.ExecuteScriptAsync方法保留下来用于向后兼容,该方法可作为快速执行Javascript方法的入口OnFrameLoadStart事件触发时DOM还没有加载完成IRenderProcessMessageHandler.OnContextCreated和IRenderProcessMessageHandler.OnContextReleased仅在主Frame上触发。
1 | browser.RenderProcessMessageHandler = new RenderProcessMessageHandler(); |
从 .NET 中调用带有返回结果的 Javascript 方法
如果希望调用一个带有返回结果的 Javascript 方法,使用 Task<JavascriptResponse> EvaluateScriptAsync(string script, TimeSpan? timeout),Javascript 异步执行并最终返回一个 JavascriptResponse 类型的实例,包含错误消息,执行结果和是否成功的标志。
1 | // Get Document Height |
返回结果的类型仅支持基元类型,如
int,bool,string等,目前还没有一个合适的方法将Javascript类型映射为一个 .NET 类型,但可借助JSON.toStringify()返回 JSON 字符串的形式来组织复杂类型,之后在 .NET 中利用 JSON.NET 将该字符串反序列化为相应的类型。
将一个 .NET 类型暴露给 Javascript
Javascript 绑定(JSB)实现了 Javascript 和 .NET 之间的通信,目前该方法提供了异步和同步版本的实现
异步 Javascript Binding API
- 利用
Native Chromium IPC在browser进程和render进程之间传递数据,非常快速 - 仅支持 .NET 方法,属性的
Get/Set不能以异步模型完成绑定 - 方法可返回基元类型,结构和复杂类型,这些类型仅属性被传递自
Javascript,可将其看作 DTO - 通过
IJavascriptCallback接口支持Javascript回调函数 - 异步模型返回一个标准的
Javascript Promise
CefSharp 提供了 Javascript 对象 CefSharp 以支持 JSB,CefSharp.BindObjectAsync() 方法返回一个 Promise 对象,并在绑定对象可用时解析该对象,绑定的对象在全局上下文(window 对象的属性)创建。
CefSharp.BindObjectAsync
CefSharp.BindObjectAsync(objectName, settings) 绑定对象:
1 | async function bindBridgeObject(){ |
settings 由以下两个属性:
NotifyIfAdlreadyBound: 若为true则触发 .NETIJavascriptObjectRepository.ObjectBoundInJavascript事件,默认值 trueIgnoreCache: 若为 true,则忽略本地缓存
返回一个 JavaScript Promise 对象。
CefSharp.DeleteBoundObject
CefSharp.DeleteBoundObject(objectName),删除匹配名称匹配的绑定对象:
1 | CefSharp.DeleteBoundObject("boundAsync"); |
返回 true/false
CefSharp.RemoveObjectFromCache
CefSharp.RemoveObjectFromCache(objectName),从缓存中移除匹配名称的绑定对象:
1 | CefSharp.RemoveObjectFromCache("boundAsync"); |
CefSharp.IsObjectCached
CefSharp.IsObjectCached(objectName) 检查指定名称的对象是否被缓存:
1 | CefSharp.IsObjectCached("boundAsync") === true; |
返回 true/false
集成示例
- 在 .NET 中创建一个将要暴露给
Javascript的类型:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class BoundObject
{
public class AsyncBoundObject
{
public void Error()
{
throw new Exception("This is an exception coming from C#");
}
public int Div(int divident, int divisor)
{
return divident / divisor;
}
}
} - 将上述类型的实例注册至
IBrowser.JavascriptObjectRepository:1
2
3
4
5
6
7
8
9
10
11
12
13// set ConcurrentTaskExecution to true if you want do concurrent jobs at one time.
CefSharpSettings.ConcurrentTaskExecution = true;
browser.JavascriptObjectRepository.ResolveObject += (sender, e) =>
{
var repo = e.ObjectRepository;
if (e.ObjectName == "bridge")
{
var boundObject = new ServiceBridge();
var bindingOptions = new BindingOptions();
repo.Register("bridge", boundObject, isAsync: true, options: bindingOptions);
}
}; - 在
Javascript中调用CefSharp.BindObjectAsync()方法解析绑定对象:1
2
3
4
5
6await CefSharp.BindObjectAsync("boundAsync");
boundAsync.div(16, 2).then(function (actualResult)
{
alert("Calculated value from calling .NET Object: " + actualResult);
});
在 .NET 中接收绑定成功事件:
1 | browser.JavascriptObjectRepository.ObjectBoundInJavascript += (sender, e) => |