我正在尝试为以下(原型)方法编写实现:
var result = browser.GetHtml(string url);
我需要这个的原因是因为有许多页面将一堆Javascript推送到浏览器,然后Javascript渲染页面。 可靠地检索此类页面的唯一方法是允许 Javascript 在检索生成的 HTML 之前在浏览器环境中执行。
我目前的尝试是使用CefGlue。 下载此项目并将其与此答案中的代码相结合后,我想出了以下代码(为了完整起见,此处包含):
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Printing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Xilium.CefGlue;
namespace OffScreenCefGlue
{
internal class Program
{
private static void Main(string[] args)
{
// Load CEF. This checks for the correct CEF version.
CefRuntime.Load();
// Start the secondary CEF process.
var cefMainArgs = new CefMainArgs(new string[0]);
var cefApp = new DemoCefApp();
// This is where the code path divereges for child processes.
if (CefRuntime.ExecuteProcess(cefMainArgs, cefApp) != -1)
{
Console.Error.WriteLine("CefRuntime could not create the secondary process.");
}
// Settings for all of CEF (e.g. process management and control).
var cefSettings = new CefSettings
{
SingleProcess = false,
MultiThreadedMessageLoop = true
};
// Start the browser process (a child process).
CefRuntime.Initialize(cefMainArgs, cefSettings, cefApp);
// Instruct CEF to not render to a window at all.
CefWindowInfo cefWindowInfo = CefWindowInfo.Create();
cefWindowInfo.SetAsOffScreen(IntPtr.Zero);
// Settings for the browser window itself (e.g. should JavaScript be enabled?).
var cefBrowserSettings = new CefBrowserSettings();
// Initialize some the cust interactions with the browser process.
// The browser window will be 1280 x 720 (pixels).
var cefClient = new DemoCefClient(1280, 720);
// Start up the browser instance.
string url = "http://www.reddit.com/";
CefBrowserHost.CreateBrowser(cefWindowInfo, cefClient, cefBrowserSettings, url);
// Hang, to let the browser do its work.
Console.Read();
// Clean up CEF.
CefRuntime.Shutdown();
}
}
internal class DemoCefApp : CefApp
{
}
internal class DemoCefClient : CefClient
{
private readonly DemoCefLoadHandler _loadHandler;
private readonly DemoCefRenderHandler _renderHandler;
public DemoCefClient(int windowWidth, int windowHeight)
{
_renderHandler = new DemoCefRenderHandler(windowWidth, windowHeight);
_loadHandler = new DemoCefLoadHandler();
}
protected override CefRenderHandler GetRenderHandler()
{
return _renderHandler;
}
protected override CefLoadHandler GetLoadHandler()
{
return _loadHandler;
}
}
internal class DemoCefLoadHandler : CefLoadHandler
{
public string Html { get; private set; }
protected override void OnLoadStart(CefBrowser browser, CefFrame frame)
{
// A single CefBrowser instance can handle multiple requests
// for a single URL if there are frames (i.e. <FRAME>, <IFRAME>).
if (frame.IsMain)
{
Console.WriteLine("START: {0}", browser.GetMainFrame().Url);
}
}
protected override async void OnLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode)
{
if (frame.IsMain)
{
Html = await browser.GetSourceAsync();
Console.WriteLine("END: {0}, {1}", browser.GetMainFrame().Url, httpStatusCode);
}
}
}
internal class DemoCefRenderHandler : CefRenderHandler
{
private readonly int _windowHeight;
private readonly int _windowWidth;
public DemoCefRenderHandler(int windowWidth, int windowHeight)
{
_windowWidth = windowWidth;
_windowHeight = windowHeight;
}
protected override bool GetRootScreenRect(CefBrowser browser, ref CefRectangle rect)
{
return GetViewRect(browser, ref rect);
}
protected override bool GetScreenPoint(CefBrowser browser, int viewX, int viewY, ref int screenX, ref int screenY)
{
screenX = viewX;
screenY = viewY;
return true;
}
protected override bool GetViewRect(CefBrowser browser, ref CefRectangle rect)
{
rect.X = 0;
rect.Y = 0;
rect.Width = _windowWidth;
rect.Height = _windowHeight;
return true;
}
protected override bool GetScreenInfo(CefBrowser browser, CefScreenInfo screenInfo)
{
return false;
}
protected override void OnPopupSize(CefBrowser browser, CefRectangle rect)
{
}
protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects, IntPtr buffer, int width, int height)
{
// Save the provided buffer (a bitmap image) as a PNG.
var bitmap = new Bitmap(width, height, width*4, PixelFormat.Format32bppRgb, buffer);
bitmap.Save("LastOnPaint.png", ImageFormat.Png);
}
protected override void OnCursorChange(CefBrowser browser, IntPtr cursorHandle)
{
}
protected override void OnScrollOffsetChanged(CefBrowser browser)
{
}
}
public class TaskStringVisitor : CefStringVisitor
{
private readonly TaskCompletionSource<string> taskCompletionSource;
public TaskStringVisitor()
{
taskCompletionSource = new TaskCompletionSource<string>();
}
protected override void Visit(string value)
{
taskCompletionSource.SetResult(value);
}
public Task<string> Task
{
get { return taskCompletionSource.Task; }
}
}
public static class CEFExtensions
{
public static Task<string> GetSourceAsync(this CefBrowser browser)
{
TaskStringVisitor taskStringVisitor = new TaskStringVisitor();
browser.GetMainFrame().GetSource(taskStringVisitor);
return taskStringVisitor.Task;
}
}
}
相关的代码位在这里:
protected override async void OnLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode)
{
if (frame.IsMain)
{
Html = await browser.GetSourceAsync();
Console.WriteLine("END: {0}, {1}", browser.GetMainFrame().Url, httpStatusCode);
}
}
这实际上看起来有效;您可以使用调试器检查 Html 变量,并且那里有一个 HTML 页面。 问题是,Html 变量在该回调方法中对我没有好处;它深埋在类层次结构中的三层,我需要在我尝试编写的方法中返回它,而无需创建 Schroedinbug。
(尝试从该 string Html
属性获取结果,包括尝试使用调试器中的 Html 可视化工具查看它,似乎会导致死锁,这是我真正想避免的,特别是因为此代码将在服务器上运行)。
如何安全可靠地实现var result = browser.GetHtml(string url);
?
奖励问题:可以使用这种技术将上述代码中的回调机制转换为任务吗? 那会是什么样子?
,当前的 CefGlue 版本没有提供任何同步上下文,因此大多数情况下您不应该在回调中使用 async/await,除非您确定自己要做什么。
"可靠"代码应该是异步的,因为大多数 CEF 调用都是异步的(提供或不提供回调)。Async/await 大大简化了这项任务,所以我假设这个问题可以简化为:"如何正确编写 GetSourceAsync 方法?这也依赖于你的奖励问题,简单的答案当然是否定的,这种技术应该被认为是有害的,因为不了解底层代码会导致不同的效果。
因此,无论 GetSourceAsync 方法如何,尤其是 TaskStringVisitor 我只建议您永远不要直接执行 TaskCompletionSource 的方法,因为它同步执行延续(在 .NET 4.6 中,它可以选择异步执行延续,但我个人没有检查它在 4.6 内部是如何完成的)。这是尽快释放一个 CEF 线程所必需的。否则最终你可以获得大的延续树,循环或等待,实际上是永远阻止浏览器的线程。另外,请注意,这种扩展也是有害的,因为它们具有上述相同的问题 - 要处理的唯一选择是具有真正的异步延续。
protected override void Visit(string value)
{
System.Threading.Tasks.Task.Run(() => taskCompletionSource.SetResult(value));
}
一些 CEF API 是混合的:如果我们不在必需线程上,它们会将任务排队到所需的线程,或者同步执行。对于这种情况,应该简化处理,在这种情况下最好避免异步的东西。同样,只是为了避免同步延续,因为它们可能导致重入问题和/或只是您获得不必要的堆栈帧(希望只是在短时间内,并且代码不会卡在某个地方)。
最简单的示例之一是,但对于其他一些 API 调用也是如此:
internal static class CefTaskHelper
{
public static Task RunAsync(CefThreadId threadId, Action action)
{
if (CefRuntime.CurrentlyOn(threadId))
{
action();
return TaskHelpers.Completed();
}
else
{
var tcs = new TaskCompletionSource<FakeVoid>();
StartNew(threadId, () =>
{
try
{
action();
tcs.SetResultAsync(default(FakeVoid));
}
catch (Exception e)
{
tcs.SetExceptionAsync(e);
}
});
return tcs.Task;
}
}
public static void StartNew(CefThreadId threadId, Action action)
{
CefRuntime.PostTask(threadId, new CefActionTask(action));
}
}
更新:
这实际上看起来有效;您可以使用 调试器,其中有一个 HTML 页面。问题是, Html 变量在该回调方法中对我没有好处;它被埋葬了 类层次结构中的三层,我需要在 方法我试图在不创建 Schroedinbug 的情况下编写。
你只需要实现CefLifeSpanHandler,然后你可以在CefBrowser被创建(异步创建)后直接访问它。存在 CreateBrowserSync 调用,但不是首选方式。
PS:我正在开发下一代CefGlue,但现在还没有准备好使用。计划更好的异步/等待集成。我个人在服务器端环境中大量使用异步/等待的东西。