CefSharp
CEF全称:Chromium Embedded Framework。
CefSharp是什么?官网上它是这么写的:CefSharp是在C#或VB.NET应用程序中嵌入全功能标准兼容web浏览器的最简单方法。CefSharp有WinForms和WPF应用程序的浏览器控件,也有自动化项目的无标题(屏幕外)版本。CefSharp基于Chromium嵌入式框架,这是Google Chrome的开源版本。
说白了,就是基于C#或VB语言的可编程浏览器(当然CEF也有其他语言的,如Java,Go)。
本文环境:
- CefSharp版本:75.1.143
- VS版本:2015
- 操作系统:Windows 10专业版
WPF引入CefSharp
CefSharp有现成的NuGet包,先引入到项目中,然后在XAML中添加响应控件:
1
| <cefSharp:ChromiumWebBrowser Name="myChrome" Loaded="myChrome_Loaded"/>
|
添加cefSharp
命名空间:
1
| xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
|
在myChrome_Loaded
事件中,我们让浏览器打开百度首页:
1
2
3
4
5
| private void myChrome_Loaded(object sender, RoutedEventArgs e)
{
String url = "https://www.baidu.com";
myChrome.Load(url);
}
|
运行程序,我们就可以看到百度首页了。
截断请求
根据文档,我们可以看到RequestHandler
类中的方法GetResourceRequestHandler
会在每次发请求前被调用:
GetResourceRequestHandler
Called on the CEF IO thread before a resource request is initiated.
RequestHandler
类是IRequestHandler
接口的默认实现,我们自定义请求可以继承这个类:
Default implementation of IRequestHandler.
This class provides default implementations of the methods from IRequestHandler, therefore providing a convenience base class for any custom request handler.
所以我们可以创建一个继承RequestHandler
的类
1
2
3
4
5
6
7
8
|
class CustomRequestHandler : RequestHandler
{
protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
{
return new CustomResourceRequestHandler();
}
}
|
GetResourceRequestHandler
是我们要重点关注的方法,里头我们返回了一个类实例,在这个类中我们就可以自定义请求。
新版的CefSharp(75版本之后)把OnBeforeResourceLoad
方法移动到了IResourceRequestHandler
接口里(文档),同样的CefSharp也提供了这个接口的默认实现:ResourceRequestHandler
,所以我们还需要一个继承ResourceRequestHandler
的类(也就是上面代码中的CustomResourceRequestHandler
类):
1
2
3
4
5
6
7
8
9
10
11
| public class CustomResourceRequestHandler : ResourceRequestHandler
{
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
{
var headers = request.Headers;
headers["Custom-Header"] = "My Custom Header";
request.Headers = headers;
return CefReturnValue.Continue;
}
}
|
最后,把自定义请求类设置到CefSharp实例中
1
| myChrome.RequestHandler = new CustomRequestHandler();
|
通过Fiddler这样的抓包工具,我们就会发现,自定义的Custom-Header
头已经加上了
添加自定义查询参数
上面的例子中,我们添加了自定义的header,如果我们想改写URL
添加一些自定义的查询参数呢,譬如name=foo
?这里有个坑,如果我们简单地把request.Url += "?name=foo"
,这样会导致无限重定向(因为改了Url就会重定向)。解决方法也很简单,就是判断一下我们想要的查询参数是否已经在Url
里了:
1
2
3
4
5
6
7
8
9
10
11
12
13
| protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
{
var headers = request.Headers;
headers["Custom-Header"] = "My Custom Header";
request.Headers = headers;
if (!request.Url.Contains("name=foo"))
{
request.Url += "?" + "name=foo";
}
return CefReturnValue.Continue;
}
|
添加自定义Body
根据IRequest的文档,我们可以利用PostData
属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
{
var headers = request.Headers;
headers["Custom-Header"] = "My Custom Header";
request.Headers = headers;
string body = "name=foo";
byte[] byteArray = System.Text.Encoding.UTF8.GetBytes(body);
request.InitializePostData();
var element = request.PostData.CreatePostDataElement();
element.Bytes = byteArray;
request.PostData.AddElement(element);
return CefReturnValue.Continue;
}
|
通过Fiddler这样的抓包工具,我们就会发现,POST 数据已经加上了:
加载本地HTML字符串
有时候,我们可能需要渲染一个内存中的HTML字符串,CefSharp也提供这样的接口,代码很简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| private void myChrome_Loaded(object sender, RoutedEventArgs e)
{
string html = @"<!DOCTYPE html>
<html>
<head>
<title>这是个标题</title>
<meta charset='utf-8' />
<meta name = 'viewport' content = 'width=device-width, initial-scale=1' />
</head>
<body>
<h1>这是一个一个简单的HTML</h1>
<p>Hello World!</p >
</body>
</html>";
String url = "https://www.baidu.com";
myChrome.LoadHtml(html, url);
}
|
截断响应
这里的关键在于GetResourceResponseFilter
方法,它的签名如下:
1
2
3
4
5
6
7
| IResponseFilter GetResourceResponseFilter(
IWebBrowser chromiumWebBrowser,
IBrowser browser,
IFrame frame,
IRequest request,
IResponse response
)
|
它返回了一个IResponseFilter
接口,在这个接口中,我们可以截取到请求响应的内容。在CefSharp最新版本中,GetResourceResponseFilter
已经被放入到IResourceRequestHandler
接口中,最新文档。
下面我放了一个截断网页XHR请求的例子:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
| public class TestJsonFilter : IResponseFilter
{
public List<byte> DataAll = new List<byte>();
public FilterStatus Filter(System.IO.Stream dataIn, out long dataInRead, System.IO.Stream dataOut, out long dataOutWritten)
{
try
{
if (dataIn == null || dataIn.Length == 0)
{
dataInRead = 0;
dataOutWritten = 0;
return FilterStatus.Done;
}
dataInRead = dataIn.Length;
dataOutWritten = Math.Min(dataInRead, dataOut.Length);
dataIn.CopyTo(dataOut);
dataIn.Seek(0, SeekOrigin.Begin);
byte[] bs = new byte[dataIn.Length];
dataIn.Read(bs, 0, bs.Length);
DataAll.AddRange(bs);
dataInRead = dataIn.Length;
dataOutWritten = dataIn.Length;
return FilterStatus.NeedMoreData;
}
catch (Exception ex)
{
dataInRead = dataIn.Length;
dataOutWritten = dataIn.Length;
return FilterStatus.Done;
}
}
public bool InitFilter()
{
return true;
}
public void Dispose()
{
}
}
public class FilterManager
{
private static Dictionary<string, IResponseFilter> dataList = new Dictionary<string, IResponseFilter>();
public static IResponseFilter CreateFilter(string guid)
{
lock (dataList)
{
var filter = new TestJsonFilter();
dataList.Add(guid, filter);
return filter;
}
}
public static IResponseFilter GetFileter(string guid)
{
lock (dataList)
{
if (dataList.ContainsKey(guid)) // 这里要检测key存在,不然会报异常,会导致ContextSwitchDeadlock
{
return dataList[guid];
}
else
{
return null;
}
}
}
}
public class CustomResourceRequestHandler : ResourceRequestHandler
{
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
{
// 截断请求的代码...
return CefReturnValue.Continue;
}
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
{
if (!(request.ResourceType == ResourceType.Xhr)) // 不是XHR类型就不去过滤
{
return null;
}
var filer = FilterManager.CreateFilter(request.Identifier.ToString());
return filer;
}
protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength)
{
var filer = FilterManager.GetFileter(request.Identifier.ToString()) as TestJsonFilter;
if (filer != null)
{
Console.WriteLine(ASCIIEncoding.UTF8.GetString(filer.DataAll.ToArray())); // 打印body内容
}
}
}
private void myChrome_Loaded(object sender, RoutedEventArgs e)
{
String url = "https://github.com/salamander-mh"; // github首页上有ajax请求,可以看效果
myChrome.Load(url);
}
|
运行程序,在输出
视图就可以看到Ajax请求的body数据。
截取cookie
建立Cookie读取对象,继承接口 ICookieVisitor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class CookieVisitor : CefSharp.ICookieVisitor
{
public event Action<CefSharp.Cookie> SendCookie;
public bool Visit(Cookie cookie, int count, int total, ref bool deleteCookie)
{
deleteCookie = false;
if (SendCookie != null)
{
SendCookie(cookie);
}
return true;
}
public void Dispose()
{
}
}
|
在browser事件中进行处理
1
2
3
4
5
6
7
8
| private void browser_FrameLoadEnd(object sender, CefSharp.FrameLoadEndEventArgs e)
{
var cookieManager = myChrome.GetCookieManager();
CookieVisitor visitor = new CookieVisitor();
visitor.SendCookie += visitor_SendCookie;
cookieManager.VisitAllCookies(visitor);
}
|
回调事件
1
2
3
4
| private void visitor_SendCookie(CefSharp.Cookie obj)
{
Console.WriteLine("获取cookie:" + obj.Domain.TrimStart('.') + "^" + obj.Name + "^" + obj.Value + "$");
}
|
设置CefSharp实例事件:
1
2
3
4
5
6
| private void myChrome_Loaded(object sender, RoutedEventArgs e)
{
String url = "https://www.baidu.com";
myChrome.Load(url);
myChrome.FrameLoadEnd += browser_FrameLoadEnd;
}
|
运行程序,在输出
视图就可以看到cookie数据了。
Javascript交互
C#执行js方法
1
| myChrome.GetBrowser().MainFrame.ExecuteJavaScriptAsync("document.getElementById('testid').click();");
|
以上代码就会触发id为testid
的元素的click
事件。
注意:脚本是在 Frame 级别执行,页面永远至少有一个Frame( MainFrame )。
获取Javascript方法结果
这里需要使用Task<JavascriptResponse> EvaluateScriptAsync(string script, TimeSpan? timeout)
方法。 JavaScript代码是异步执行的,因此使用.NET Task 类返回一个响应,其中包含错误消息,结果和一个成功(bool)标志。
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());
|
资源清理
关闭应用,发现CefSharp.BrowserSubprocess.exe
进程会发现没有结束,其实在退出事件中,我们需要调用Cef.Shutdown()
方法
1
2
3
4
5
6
7
8
9
| try
{
if (browser != null)
{
browser.Dispose();
Cef.Shutdown();
}
}
catch { }
|
示例代码下载
参考: