快速开始.
进程 Process
线程 Threads
Initialize 和 Shutdown.
Initialize.
Shutdown.
配置
CefSettings
BrowserSettings
IBrowser,IFrame 和 IBrowserHost 对象.
Handlers
配置代理.
请求上下文..
高 DPI 显示器支持..
Javascript 集成..
从 .NET 中调用 Javascript 方法..
从 .NET 中调用带有返回结果的 Javascript 方法..
将一个 .NET 类型暴露给 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/1748
- CommandLineArgsDisabled: 设置为 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 对象代表顶层窗口。即,如果一个浏览器窗口加载了两个 元素将会包含 3 个 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 2 3 4 5 |
"http://schemas.microsoft.com/SMI/2005/WindowsSettings"> true/PM
|
Javascript 集成
从 .NET 中调用 Javascript 方法
Javascript 方法仅能在一个 V8Context 中执行,IRenderProcessMessageHandler.OnContextCreated 和 IRenderProcessMessageHandler.OnContextReleased 为 Javascript 代码的执行划定了一个清晰的边界。通常:
- Javascript 在 Frame 级别执行,每个页面至少包含一个 Frame
- IWebBrowser.ExecuteScriptAsync 方法保留下来用于向后兼容,该方法可作为快速执行 Javascript 方法的入口
- OnFrameLoadStart 事件触发时 DOM 还没有加载完成
- IRenderProcessMessageHandler.OnContextCreated 和 IRenderProcessMessageHandler.OnContextReleased 仅在主 Frame 上触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
browser.RenderProcessMessageHandler = new RenderProcessMessageHandler();
public class RenderProcessMessageHandler : IRenderProcessMessageHandler { // Wait for the underlying JavaScript Context to be created. This is only called for the main frame. // If the page has no JavaScript, no context will be created. void IRenderProcessMessageHandler.OnContextCreated(IWebBrowser browserControl, IBrowser browser, IFrame frame) { const string script = "document.addEventListener('DOMContentLoaded', function(){ alert('DomLoaded'); });";
frame.ExecuteJavaScriptAsync(script); } }
//Wait for the page to finish loading (all resources will have been loaded, rendering is likely still happening) browser.LoadingStateChanged += (sender, args) => { //Wait for the Page to finish loading if (args.IsLoading == false) { browser.ExecuteJavaScriptAsync("alert('All Resources Have Loaded');"); } }
//Wait for the MainFrame to finish loading browser.FrameLoadEnd += (sender, args) => { //Wait for the MainFrame to finish loading if(args.Frame.IsMain) { args.Frame.ExecuteJavaScriptAsync("alert('MainFrame finished loading');"); } }; |
从 .NET 中调用带有返回结果的 Javascript 方法
如果希望调用一个带有返回结果的 Javascript 方法,使用 Task EvaluateScriptAsync(string script, TimeSpan? timeout),Javascript 异步执行并最终返回一个 JavascriptResponse 类型的实例,包含错误消息,执行结果和是否成功的标志。
1 2 3 4 5 6 7 8 9 10 11 |
// Get Document Height var task = frame.EvaluateScriptAsync("(function() { var body = document.body, html = document.documentElement; return Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight ); })();", null);
task.ContinueWith(t => { if (!t.IsFaulted) { var response = t.Result; EvaluateJavaScriptResult = response.Success ? (response.Result ?? "null") : response.Message; } }, TaskScheduler.FromCurrentSynchronizationContext()); |
返回结果的类型仅支持基元类型,如 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 2 3 |
async function bindBridgeObject(){ await CefSharp.BindObjectAsync(objectName, settings); } |
settings 由以下两个属性:
- NotifyIfAdlreadyBound: 若为 true 则触发 .NET IJavascriptObjectRepository.ObjectBoundInJavascript 事件,默认值 true
- IgnoreCache: 若为 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 15 |
public 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 6 |
await CefSharp.BindObjectAsync("boundAsync");
boundAsync.div(16, 2).then(function (actualResult) { alert("Calculated value from calling .NET Object: " + actualResult); }); |
在 .NET 中接收绑定成功事件:
1 2 3 4 5 6 |
browser.JavascriptObjectRepository.ObjectBoundInJavascript += (sender, e) => { var name = e.ObjectName;
Debug.WriteLine($"Object {e.ObjectName} was bound successfully."); }; |