CefSharp 提供了多種執行CDP(Chrome DevTools Protocol)方式,有高度封裝的DevToolsClient.Page、DevToolsClient.DOM等等,也有完全手動執行的IBrowserHost下的SendDevToolsMessage,這里我們只討論手動執行方式。
手動執行CDP方式目前我知道的有兩種:
只傳入CDP方法名稱、參數,返回結果(Cefsharp維護 發送消息ID、接收消息ID; 有些方法也提供了消息ID入參),使用方便,但是由於消息id是Cefsharp維護,頻繁發送時有時會拋出消息ID不匹配異常;
手動控制發送json,監聽返回結果(完全控制,就剩下websocket鏈接等基本信息cefsharp維護),但是操作比較麻煩
手動執行CDP方法
DevToolsClient
Cefsharp提供的CDP封裝類,封裝了CDP各種方法模塊直接調用方法,但是使用姿勢不對可能會執行后程序卡死,具體各種卡死情況請跳轉stackoverflow。
可以通過chromiumWebBrowser.GetBrowser().GetDevToolsClient() 獲得DevToolsClient實例。
最好不要頻繁調用GetDevToolsClient() 獲取DevToolsClient,因為據說每次獲取會重置消息ID,頻繁獲取可能會導致 發送/接收消息ID沖突,所以最好聲明全局變量在ChromiumWebBrowser實例初始化完成時獲取一次:
DevToolsClient devTool = null; private void Form1_Load(object sender, EventArgs e){ //.... ChromiumWebBrowser chromiumWebBrowser1 = new ChromiumWebBrowser(); chromiumWebBrowser1.IsBrowserInitializedChanged+= new EventHandler(delegate { devTool = chromiumWebBrowser1.GetBrowser().GetDevToolsClient(); }); }
DevToolsClient.ExecuteDevToolsMethodAsync
我感覺相對比較簡單的手動調用CDP方式,CefSharp維護發送消息ID,Cefsharp已經簡單封裝了消息結果類型
方法原型:
public class DevToolsClient : IDevToolsMessageObserver, IDisposable, IDevToolsClient { //.... public Task<DevToolsMethodResponse> ExecuteDevToolsMethodAsync(string method, IDictionary<string, object> parameters = null); }
method: CDP 方法名稱
parameters: 方法參數
返回結果DevToolsMethodResponse:
public class DevToolsMethodResponse { public DevToolsMethodResponse(); public int MessageId { get; set; } public string ResponseAsJsonString { get; set; } public bool Success { get; set; } }
MessageId: 消息ID
ResponseAsJsonString: 返回消息內容(消息的result內容)
Success: 是否執行成功
比如執行刷新頁面:
private void button8_Click(object sender, EventArgs e) { devTool.ExecuteDevToolsMethodAsync("Page.reload").ContinueWith(delegate(Task<DevToolsMethodResponse> result) { Console.WriteLine(result.Result.ResponseAsJsonString); }); }
獲取頁面結構:
private void button8_Click(object sender, EventArgs e) { devTool.ExecuteDevToolsMethodAsync("DOM.enable").ContinueWith(delegate(Task<DevToolsMethodResponse> result) { Dictionary<string, object> param = new Dictionary<string, object>() { { "depth", 10 }, { "pierce", true } }; devTool.ExecuteDevToolsMethodAsync("DOM.getDocument", param).ContinueWith(delegate(Task<DevToolsMethodResponse> resultA) { Console.WriteLine(resultA.Result.ResponseAsJsonString); }); }); }
DOM.enable: 開啟DOM代理
DOM.getDocument: 獲取頁面結構(包含嵌套的iframe內容),有兩個可選參數(depth: 獲取結構深度,pierce: 是否遞歸向下查詢iframes)
但是別使用Wait()奧- -,像這樣:
private void button8_Click(object sender, EventArgs e) { devTool.ExecuteDevToolsMethodAsync("DOM.enable").Wait(); }
會發現程序卡死了...當初這個問題困擾好久,上邊的overflow上的問題就是我提出的,截止到現在,還沒有大佬關注...o(╥﹏╥)o
DevToolsExtensions
另一個執行CDP方法的靜態類,主要用來擴展實現IBrowserHost、IWebBrowser、IBrowser接口的實例可以直接執行CDP方法,因為ChromiumWebBrowser實現了IWebBrowser和IBrowser,所以可以在ChromiumWebBrowser實例中直接調用ExecuteDevToolsMethodAsync方法。
上邊chromiumWebBrowser1.GetBrowser().GetDevToolsClient()中GetDevToolsClient方法就是使用的此類中的擴展函數。
DevToolsExtensions.ExecuteDevToolsMethod
執行CDP方法,執行成功返回消息id,失敗則返回0。
和上邊不同的是,上邊執行CDP方法后,會異步返回方法執行結果,此方法沒有異步執行,而是返回了傳入的消息id,並且此方法必須在cefsharp線程中調用
public static class DevToolsExtensions { public static int ExecuteDevToolsMethod(this IBrowserHost browserHost, int messageId, string method, JsonString parameters); }
browserHost: 此處傳入 chromiumWebBrowser1.GetBrowserHost();
messageId: 消息ID
method: 方法名稱
parameters: 方法參數,傳入方法參數json字符串
比如獲取頁面結構:
int i = 1;
Cef.UIThreadTaskFactory.StartNew(delegate { chromiumWebBrowser1.GetBrowserHost().ExecuteDevToolsMethod(i, "DOM.enable"); i++; Console.WriteLine(chromiumWebBrowser1.GetBrowserHost().ExecuteDevToolsMethod(i, "DOM.getDocument", new JsonString("{\"pierce\": true, \"depth\": 1}"))); });
上邊方法只是發送指令,如果想要拿到指令對應的結果,就需要實現IDevToolsMessageObserver接口,相當於添加了一個監聽(官方取名叫觀察者),監聽websocket發送過來的消息:
class DevToolsMessageObserverHandler : IDevToolsMessageObserver { public void Dispose() { } public void OnDevToolsAgentAttached(IBrowser browser) { } public void OnDevToolsAgentDetached(IBrowser browser) { } public void OnDevToolsEvent(IBrowser browser, string method, Stream parameters) { } public bool OnDevToolsMessage(IBrowser browser, Stream message) { return false; } public void OnDevToolsMethodResult(IBrowser browser, int messageId, bool success, Stream result) { byte[] bytes = new byte[result.Length]; result.Read(bytes, 0, bytes.Length); StringBuilder sb = new StringBuilder(); foreach (byte item in bytes) { sb.Append((char)item); } Console.WriteLine(sb.ToString()); } }
這里主要關注OnDevToolsMessage和OnDevToolsMethodResult方法,服務器發送一條消息時,先到OnDevToolsMessage方法,在到OnDevToolsMethodResult方法。
如果OnDevToolsMessage方法返回true,表示消息已處理,不會在執行后續的OnDevToolsMethodResult.
OnDevToolsMethodResult方法的messageId就是發送指令時傳入的消息ID.
把監聽類注冊到BrowserHost中:
//全局變量
IRegistration reg = null;
chromiumWebBrowser1.IsBrowserInitializedChanged += new EventHandler(delegate { reg = chromiumWebBrowser1.GetBrowserHost().AddDevToolsMessageObserver(new DevToolsMessageObserverHandler()); });
這樣每一條瀏覽器端的發送過來的消息,都會監聽到,但是這時就需要我們自己來實現根據消息ID匹配CDP方法的返回結果了。
這里需要注意AddDevToolsMessageObserver方法返回的IRegistration對象,這個對象控制監聽器是否繼續監聽,如果調用reg.Dispose(),監聽器將再也不起作用。
不要寫成這樣:
chromiumWebBrowser1.IsBrowserInitializedChanged += new EventHandler(delegate { chromiumWebBrowser1.GetBrowserHost().AddDevToolsMessageObserver(new DevToolsMessageObserverHandler()); });
如果寫成這樣,將在第一次監聽到消息后,會隨着GC空閑時,把AddDevToolsMessageObserver返回的IRegistration對象回收,監聽器也就無效了。
IBrowserHost.SendDevToolsMessage
完全手動控制發送json,雖然自由度高但使用起來跟上邊比起來確實有些繁瑣
方法原型很簡單,只傳入一個json,CefSharp會直接給瀏覽器端發送這個json字符串,返回true表示執行成功,false表示執行失敗
bool SendDevToolsMessage(string messageAsJson);
發前需要我們記一下消息id,發送后需要使用上邊監聽瀏覽器端消息內容方式匹配每一條消息ID。
和DevToolsExtensions.ExecuteDevToolsMethod函數一樣,需要在cefsharp線程中使用:
Cef.UIThreadTaskFactory.StartNew(delegate { IBrowserHost browserHose = chromiumWebBrowser1.GetBrowserHost(); browserHose.SendDevToolsMessage("{\"id\": 1, \"method\": \"DOM.enable\"}"); browserHose.SendDevToolsMessage("{\"id\": 2, \"method\": \"DOM.getDocument\", \"params\": {\"pierce\": true, \"depth\": 40}}"); });
以上根據個人理解總結,如有錯誤的地方,歡迎前輩指出,非常感謝!