CEF JS與browser程序間的非同步通訊

基於CEF開發時經常需要在JS和C 程式碼間通訊,我們在CEF中JavaScript與C 互動中討論了常見的互動方式,不過都是在Renderer程序中,這次來看看如何在JS和Browser程序間通訊,基本介紹可以看這裡:
https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage#markdown-header-asynchronous-javascript-bindings

具體做法,上面給出的連結裡有大概說明,另外在cef_message_router.h中有詳細說明,它分了9步,有需要的可以去檢視。我試驗了下可行。按照我的實驗,從三個方面來說說:

  • browser程序
  • renderer程序
  • html中的js

foruok原創,轉載請關注微信訂閱號“程式視界”聯絡foruok。

browser程序

  • 1)配置browser程序這一側的Message Router

使用CefMessageRouterConfig來定義可以在html中使用的方法名字。程式碼片段可能如下:

...
CefMessageRouterConfig g_messageRouterConfig;
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPTSTR    lpCmdLine,
_In_ int       nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// Enable High-DPI support on Windows 7 or newer.
CefEnableHighDPISupport();
g_messageRouterConfig.js_query_function = "cefQuery";
g_messageRouterConfig.js_cancel_function = "cefQueryCancel";
CefMainArgs main_args(hInstance);
...
}

注意我在main函式之外定義了一個CefMessageRouterConfig的例項,然後在main函式中設定了query和cancel這兩個函式。它們定義了可以在html的js程式碼裡使用的方法,比如根據上面程式碼,你可以用window.cefQuery({…})。

  • 2)建立CefMessageRouterBrowserSide的例項

browser程序這邊需要一個CefMessageRouterBrowserSide的例項來處理renderer程序發過來的訊息。建議把這個例項作為成員變數放在CefClient的派生類中,因為它CefMessageRouterBrowserSide定義了一些方法,需要在指定的地方呼叫。

宣告和初始化的程式碼片段:

CefRefPtr<CefMessageRouterBrowserSide> m_browser_side_router;
...
extern CefMessageRouterConfig g_messageRouterConfig;
m_browser_side_router = CefMessageRouterBrowserSide::Create(g_messageRouterConfig);
...
  • 3)呼叫預定義方法

CefMessageRouterBrowserSide預定義了下列介面,需要在指定地方呼叫:

// call from CefClient::OnProcessMessageReceived
bool OnProcessMessageReceived(...)
// call from CefLifeSpanHandler::OnBeforeClose()
void OnBeforeClose(CefRefPtr<CefBrowser> browser)
// call from CefRequestHandler::OnRenderProcessTerminated()
void OnRenderProcessTerminated(CefRefPtr<CefBrowser> browser)
// call from CefRequestHandler::OnBeforeBrowse()
void OnBeforeBrowse(...)

所以,我們的這個ClientHandler類,宣告如下:

class ClientHandler : public CefClient,
public CefDisplayHandler,
public CefLifeSpanHandler,
public CefLoadHandler,
public CefRequestHandler
{
...
}

ClientHandler要實現CefClient的OnProcessMessageReceived、CefLifeSpanHandler的OnBeforeClose、CefRequestHandler的OnRenderProcessTerminated和OnBeforeBrowse方法,在這些方法裡呼叫CefMessageRouterBrowserSide的同名方法即可。程式碼片段如下:

void ClientHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser)
{
CEF_REQUIRE_UI_THREAD();
m_browser_side_router->OnBeforeClose(browser);
...
}
bool ClientHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message)
{
if (m_browser_side_router->OnProcessMessageReceived(browser, source_process, message)) return true;
return false;
}
bool ClientHandler::OnBeforeBrowse(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefRequest> request, bool is_redirect)
{
m_browser_side_router->OnBeforeBrowse(browser, frame);
return false;
}
void ClientHandler::OnRenderProcessTerminated(CefRefPtr<CefBrowser> browser, TerminationStatus status)
{
m_browser_side_router->OnRenderProcessTerminated(browser);
}
  • 4) 在browser程序裡實現處理JS查詢的handler

CefMessageRouterBrowserSide定義了一個內部類Handler,繼承它:

class BrowserSideJSHandler : public CefMessageRouterBrowserSide::Handler
{
public:
BrowserSideJSHandler();
virtual bool OnQuery(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64 query_id,
const CefString& request,
bool persistent,
CefRefPtr<Callback> callback);
virtual void OnQueryCanceled(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64 query_id);
};

實現 OnQuery 和 OnQueryCanceled兩個方法:

bool BrowserSideJSHandler::OnQuery(CefRefPtr<CefBrowser> browser, 
CefRefPtr<CefFrame> frame, int64 query_id, 
const CefString& request, bool persistent, CefRefPtr<Callback> callback)
{
if (request == "HelloCefQuery")
{
callback->Success("HelloCefQuery Ok");
return true;
}
else if (request == "GiveMeMoney")
{
callback->Failure(404, "There are none thus query!");
return true;
}
return false; // give other handler chance
}
void BrowserSideJSHandler::OnQueryCanceled(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int64 query_id)
{
//cancel our async query task...
}
  • 5)給CefMessageRouterBrowserSide的例項新增handler

我在ClientHandler類中定義了一個BrowserSideJSHandler的例項,類似下面:

class ClientHandler : public CefClient,
public CefDisplayHandler,
public CefLifeSpanHandler,
public CefLoadHandler,
public CefRequestHandler
{
...
CefRefPtr<CefMessageRouterBrowserSide> m_browser_side_router;
BrowserSideJSHandler m_jsHandler;
...
}

然後在ClientHandler建構函式中加入了下列程式碼:

m_browser_side_router->AddHandler(&m_jsHandler, true);

上面的程式碼把BrowserSideJSHandler加入到browser程序裡的訊息路由器中。

好了,到這裡browser程序配置完成。接下來看renderer程序。

renderer程序

  • 1)配置renderer程序這一側的Message Router

renderer程序的配置和browser必須一致!

因為我browser和renderer使用一個程序,所以前面的程式碼可以共用,不說了。

  • 2)建立CefMessageRouterRendererSide的例項

renderer程序這邊需要一個CefMessageRouterRendererSide的例項來處理browser程序發過來的訊息和js傳入的資訊。建議把這個例項作為成員變數放在CefApp的派生類中。

我的定義類似下面:

class ClientAppRenderer : public CefApp,
public CefRenderProcessHandler 
{
...
CefRefPtr<CefMessageRouterRendererSide> m_renderer_side_router;
...
};

建立類似下面:

m_renderer_side_router = CefMessageRouterRendererSide::Create(g_messageRouterConfig);
  • 3) 呼叫預定義方法

CefMessageRouterRendererSide定義了下面三個預定義方法:

void OnContextCreated(...)
void OnContextReleased(...)
bool OnProcessMessageReceived(...)

這三個方法需要在CefRenderProcessHandler的派生類的同名方法裡呼叫,我的程式碼片段:

void ClientAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context)
{
m_renderer_side_router->OnContextCreated(browser, frame, context);
}
void ClientAppRenderer::OnContextReleased(CefRefPtr<CefBrowser> browser, 
CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
m_renderer_side_router->OnContextReleased(browser, frame, context);
}
bool ClientAppRenderer::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message)
{
if (m_renderer_side_router->OnProcessMessageReceived(browser, source_process, message)) return true;
return false;
}

好啦,renderer程序這邊的事兒幹完了。接下來就可以在html裡寫js程式碼呼叫cefQuery方法了。

html中的js

html這一側很簡單,我的測試程式碼如下:

<!DOCTYPE html>
<html>
<!--
Copyright (c) 2016 [email protected]微信訂閱號“程式視界”(programmer_sight). 
All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<head>
<script type="text/javascript">
function doQuery() {
window.cefQuery({
request: document.getElementById("query").value,
persistent: false,
onSuccess: function(response) { alert(response); },
onFailure: function(code, msg) { alert(code   " - "   msg); }
}
);
}
</script>
<meta charset="UTF-8">
<title>Test Cef Query</title>
</head>
<body>
<form>
Query String: <input type="text" id="query" value="HelloCefQuery"/>&nbsp;&nbsp;<input  type="button" value="SendQuery" onclick="doQuery()"/>
</form>
</body>
</html>

關鍵的程式碼起始就一個函式:

function doQuery() {
window.cefQuery({
request: document.getElementById("query").value,
persistent: false,
onSuccess: function(response) { alert(response); },
onFailure: function(code, msg) { alert(code   " - "   msg); }
}
);
}

cefQuery方法接受一個JS物件,如你所見,request、persistent可以對應到BrowserSideJSHandler::OnQuery的引數上,onSuccess和onFailure則在renderer程序裡做了轉換,到browser程序裡對應到了CefMessageRouterBrowserSide::Callback的Success和Failure方法上。

我的實現,對HelloCefQuery呼叫success,對GiveMeMoney呼叫failure,其它的都沒處理。

成功時的效果:

失敗時的效果:

查詢未被處理時的效果:

Cef Generic Message Router的實現

Generic Message Router內部實現有兩個關鍵點:

  1. CefMessageRouterRendererSide在renderer程序內,在OnContextCreated中向JS匯出了cefQuery和cefQueryCancel兩個方法,具體見cef_message_router.cc
  2. renderer和browser程序間使用SendProcessMessage通訊,在OnProcessMessageReceived中處理訊息

就這樣吧。

其他參考文章詳見我的專欄:【CEF與PPAPI開發】。