设计
这份文档概述了库的设计,同时提供了通用规则,以帮助你理解如何与其进行交互。
对象
所有库对象可分为以下类别:
- 服务对象;
- 不可变数据对象。
服务对象允许在数据对象只保存数据时执行一些操作。 服务对象可以使用数据对象。
像 IEngine, IBrowser, IProfile IBrowserSettings, IFrame, IDocument, IJsObject 实例都是 服务对象。 而 EngineOptions, Size, Rectangle 则是 不可变数据对象。
实例化
要创建不可变数据对象或服务对象,请使用其构造函数、构建器或其静态方法之一。 以下是使用构建器的示例:
EngineOptions options = new EngineOptions.Builder
{
RenderingMode = RenderingMode.HardwareAccelerated,
Language = Language.EnglishUs
}.Build();
IEngine engine = EngineFactory.Create(options);
Dim options As EngineOptions = New EngineOptions.Builder With
{
.RenderingMode = RenderingMode.HardwareAccelerated,
.Language = Language.EnglishUs
}.Build()
Dim engine As IEngine = EngineFactory.Create(options)
销毁
每个必须手动处理的服务对象都实现了 IDisposable 接口。 要处理服务对象并释放所有已分配的内存和资源,请调用 IDisposable.Dispose()方法。 例如:
engine.Dispose();
engine.Dispose()
诸如 IFrame 这样的一些服务对象可以自动释放,比如当网页被卸载时。 这些对象实现了 IAutoDisposable 接口。
如果您使用已处理的对象,则会发生ObjectDisposedException 。
关系
服务对象的生命周期可以依赖于另一个对象的生命周期。 当该服务对象被处理时,依赖于该服务对象的所有服务对象将被自动释放。 例如:
- 当您处理
IEngine时,它的所有IBrowser实例都会自动释放; - 当您处理
IBrowser时,它的所有IFrame实例都会自动处理。
方法
返回 Task<T> 实例的方法是异步执行的。 如果该方法返回某个值,它会同步执行,阻塞当前线程执行,直到收到返回值。
处理程序
每个允许注册处理程序的对象都具有 IHandler<in T> 或 IHandler<in T, out TResult> 接口类型的属性。 要注册和注销处理程序,请使用属性设置器和获取器。
IHandler<in T, out TResult> 接口有默认实现:
Handler<T, TResult>类允许包装 lambda 和方法组;AsyncHandler<T, TResult>类允许包装异步 lambda 和方法组或返回Task<TResult>的 lambda 和方法组。
异步
下面的示例演示了如何注册一个返回Task 以异步提供响应的异步处理程序:
browser.ShowContextMenuHandler =
new AsyncHandler<ShowContextMenuParameters, ShowContextMenuResponse
>(ShowContextMenu);
// 在相同类中声明的异步方法。
private async Task<ShowContextMenuResponse> ShowContextMenu(ShowContextMenuParameters p)
{
// ...
}
browser.ShowContextMenuHandler =
New AsyncHandler(Of ShowContextMenuParameters, ShowContextMenuResponse)(
AddressOf ShowContextMenu)
' 在相同类中声明的异步方法。
Private Async Function ShowContextMenu(p As ShowContextMenuParameters) As Task(Of ShowContextMenuResponse)
' ...
End Function
任务结果可以从不同的线程异步提供。
通过返回的 Task 参数提供响应,否则 IEngine 将等待响应直到终止。
同步
下面的示例演示了如何注册和注销通过返回值返回响应的常规 Handler :
browser.CreatePopupHandler =
new Handler<CreatePopupParameters, CreatePopupResponse>(p =>
{
return CreatePopupResponse.Create();
});
browser.CreatePopupHandler =
New Handler(Of CreatePopupParameters, CreatePopupResponse)(Function(p)
Return CreatePopupResponse.Create()
End Function)
处理程序如何运行
实现处理程序时,请注意:
- Chromium 会等待处理程序的响应,然后才继续处理程序所响应的请求、导航或对话框,因此运行缓慢的处理程序会延迟它所响应的操作。
- 处理程序在线程池线程上调用,而不是在 UI 线程上调用。多个处理程序可能同时运行,例如在并行网络请求时,因此处理程序代码必须是线程安全的。
IBrowser.Keyboard、IBrowser.Mouse和IBrowser.Touch的键盘、鼠标和触摸处理程序是例外。在离屏渲染模式下以及在 macOS 上,它们在传递输入的线程上运行,该线程通常是 UI 线程。请确保它们快速返回,否则 UI 会冻结。- 如果处理程序抛出异常,DotNetBrowser 会将其写入日志,而日志默认处于禁用状态。请在处理程序内部捕获异常并返回明确的响应。
- 有些处理程序在返回之前会阻塞浏览器或引擎。它们的 API 参考会说明不允许在处理程序内部调用浏览器或引擎的方法。例如,
INavigation.StartNavigationHandler不允许调用浏览器方法,INavigation.ShowHttpErrorPageHandler不允许调用引擎方法。请仅根据处理程序的参数决定响应。
事件
DotNetBrowser 通过常规的 .NET 事件报告通知,例如 IEngine.Disposed 或 INavigation.FrameLoadFinished。
事件处理程序在内部事件线程上运行,而不是在 UI 线程上运行。DotNetBrowser 使用多个事件线程,例如每个浏览器的导航事件各使用一个线程。每个线程逐个传递其事件,因此运行缓慢的事件处理程序会延迟同一线程上排在它之后的事件。来自不同线程的事件可能同时运行,即使是同一对象的事件也是如此:例如,IBrowser.TitleChanged 和 IBrowser.FrameCreated 使用不同的线程。请对多个事件处理程序共享的数据进行同步访问。
大多数对象的 Disposed 事件会在调用 Dispose() 的线程上同步触发。IEngine.Disposed 则不同:它在 Chromium 主进程退出时触发,包括崩溃之后,因此它可能在任意线程上触发。
线程安全
该库具备线程安全性:可在不同线程中安全使用 DotNetBrowser 对象。
从处理程序和事件更新 UI
除了上面所述的输入处理程序之外,处理程序和事件都在 UI 线程以外的线程上运行。Disposed 事件也可能在调用 Dispose() 的线程上运行,而该线程可能是 UI 线程。UI 框架只允许在 UI 线程上更改控件。否则,WPF 和 Avalonia UI 会抛出 InvalidOperationException,WinUI 3 会抛出 COMException。Windows Forms 只有在调试器下才会可靠地抛出 InvalidOperationException;没有调试器时,调用可能以不可预测的方式失败。请使用非阻塞调用将 UI 更新发送到 UI 线程:
- WPF 中的
Dispatcher.BeginInvoke(); - Windows Forms 中的
Control.BeginInvoke(); - WinUI 3 中的
DispatcherQueue.TryEnqueue(); - Avalonia UI 中的
Dispatcher.UIThread.Post()。
有关示例,请参阅跨线程操作无效。
不要在处理程序中阻塞等待 UI 线程,例如使用 Dispatcher.Invoke() 或 Control.Invoke()。如果此时 UI 线程正在等待一个必须在处理程序返回后才能完成的 DotNetBrowser 调用,两个线程就会互相等待。UI 会冻结,直到该调用超时并抛出 TimeoutException。在事件处理程序中,阻塞调用还会延迟排在它之后的事件,直到 UI 线程空闲。