Qt WebChannel Impliment based chromium
在開始介紹之前, 我們先看一段常用的一個應用的一段代碼:
QWebEnginePage *page = new QWebEnginePage(this);
ui->webEngineView->setPage(page);
QWebChannel *channel = new QWebChannel(this);
channel->registerObject(QStringLiteral("content"), &MsgHandler); //MsgHandler是交互類對象
page->setWebChannel(channel);
ui->webEngineView->setUrl(QUrl("you_html_page_uri.html")); //加載一個頁面
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
window.onload = function() {
new QWebChannel(qt.webChannelTransport, function(channel) {
var content = channel.objects.content; //把對象賦值到JS中
content.updateText("a new msg string"); //updateText 是一個c++中的slot 函數;
}
//連接QT中發出的信號,裏面的message是參數;只需要在QT中調用此函數也就是sendText就可以調用JS函數
content.sendText.connect(function(message) {
alert("Received message: " + message);
});
}
有編程經驗的人應該很容易看出來這其中的意思, C++中初始化了一個webengine(qt的web引擎組件已經基於chromium了)將ui組件和WebEngineView和WebEngine綁定, 之後創建了一個QWebChannel對象, 並通過WebChannel對象註冊了一個名字爲content的對象, 並綁定一個MsgHandler的Qobject的對象用來處理和頁面的交互;待會的分析就是從WebEnginePage的setWebChannel(QwebChannel*)
函數來開始分析,
因爲這裏是一切的開始;
而在js端, 當頁面load完成時, 就可以通過獲取這個註冊的js對象;有了這個對象,配合Qt 的MetaObject 引入的signal 和 slot機制, 來完成頁面的消息傳遞和函數調用;
當頁面初始化的時候,qwebchannel.js會將所有的c++端交互對象的信號和槽在js中生成對應的對象和函數,當c++觸發某個信號的時候, 會通過WebChannel內部的一個對象觸發一個Chromium的IPC消息, 這個消息以json字符串的形式打包並傳送到頁面的渲染進程;在render進程收到這個消息的時候,通過調用一個WebChannelIPCTransport::dispatchWebChannelMessage
函數將這個消息給到V8的運行環境中,
藉助於qwebchannel.js
中的全局對象qt
拿到一個js對象中的webChannelTransport
對象;通過這個對象的onmessage
函數,將這個打包好的ipc數據按照一定的格式解析出來,調用到對應的消息處理函數上;
另一方面, 通過在一個創建一個頁面的時候, 通過向當前頁面註冊webChannelTransport
這個C++中的包裝對象的時候,同樣通過google自己實現的一套包裝模版函數將c++中WebChannelTransport的成員函數NativeQtSendMessage
註冊爲頁面中的send函數;這樣, 頁面也同樣可以發送消息給到主進程當中;之後通過qwebchannel類來與應用程序或者對應的QML接口的封裝類;
官方另外一個例子是藉助於qtwebchannel建立一個簡單的websocket server的例子;see here
在Qt包裝Webcontents的時候,通過一個Adaptor類WebContentsAdapter來包裝整個webcontents必要的接口, 裏面提供了下面兩個接口:
QWebChannel *webChannel() const;
void setWebChannel(QWebChannel *, uint worldId);
通過這兩個接口, 可以向頁面註冊一個QWebChannel的對象, Adaptor對象通過調用其WebContentsAdapterPrivate對象創建並初始化其webChannelTransport對象;
void WebContentsAdapter::setWebChannel(QWebChannel *channel, uint worldId) {
Q_D(WebContentsAdapter);
if (d->webChannel == channel && d->webChannelWorld == worldId)
return;
if (!d->webChannelTransport.get())
d->webChannelTransport.reset(new WebChannelIPCTransportHost(d->webContents.get(), worldId));
...
d->webChannel = channel;
d->webChannelWorld = worldId;
...
channel->connectTo(d->webChannelTransport.get());
}
當調用setWebChannel
的時候, 如果沒有建立相應的IPC通信類, 就創建一個WebChannelIPCTransportHost
對象; 之後將channel和world id保存在webcontntsadapterprivate對象內部, 做爲附加在這個頁面上的一個channel對象;之後所有的消息、以及映射的信號與槽函數的處理, 都是通過IPC消息轉換到channel對象進行處理;在函數最後,
調用了channel的connectTo函數, 將這個IPCtransport 和 channel進行綁定;這個稍後分析, 先來看看WebChannelIPCTransportHost
構造函數,其實我們可以想想, 基於chromium這種多進程架構, 如果要與網頁環境所在的Render Process建立聯繫,那麼我們必然要通過content API 去setup這個IPC通信環境;
WebChannelIPCTransportHost::WebChannelIPCTransportHost(content::WebContents *contents, uint worldId, QObject *parent)
: QWebChannelAbstractTransport(parent)
, content::WebContentsObserver(contents)
, m_worldId(worldId)
{
Send(new WebChannelIPCTransport_Install(routing_id(), m_worldId));
}
通過上面的類圖可以知道, WebChannelIPCTransportHost類是繼承了QWebChannelAbstractTransport
content::WebContentsObserver
對象; 實現前者的sendMessage
函數來向頁面發送信號與消息, 後者利用content的接口對象WebContentsObserver來實現chromium多進程之間的ipc消息的發送和接收;在上面的構造函數中,
裏面簡單的就只有一句話“發送一個Install 的消息給render進程,用來向Render進程的V8執行環境註冊安裝一些私有函數和全局對象”
IPC_MESSAGE_HANDLER(WebChannelIPCTransport_Install, installWebChannel)
void WebChannelIPCTransport::installWebChannel(uint worldId)
{
blink::WebView *webView = render_view()->GetWebView();
if (!webView)
return;
WebChannelTransport::Install(webView->mainFrame(), worldId);
m_installed = true;
m_installedWorldId = worldId;
}
這裏說明一下,WebChannelIPCTransport對象的創建是在render進程中的renderview對象初始化的時候通過content_render_client.h 裏的接口RenderViewCreated
創建的;它繼承了RenderViewObserver對象;看到這裏應該不難看出, chromium在多進程設計上的一個邏輯對稱性; 如果對chromium代碼熟悉的同學應該深有感觸;
上面的code調用了一個靜態函數WebChannelTransport::Install
來像V8的context註冊一些全局的對象與函數對象;
void WebChannelTransport::Install(blink::WebFrame *frame, uint worldId)
{
v8::Isolate *isolate = v8::Isolate::GetCurrent();
v8::HandleScope handleScope(isolate);
v8::Handle<v8::Context> context;
if (worldId == 0)
context = frame->mainWorldScriptContext();
else
context = frame->toWebLocalFrame()->isolatedWorldScriptContext(worldId, 0);
v8::Context::Scope contextScope(context);
gin::Handle<WebChannelTransport> transport = gin::CreateHandle(isolate, new WebChannelTransport);
v8::Handle<v8::Object> global = context->Global();
v8::Handle<v8::Object> qt = global->Get(gin::StringToV8(isolate, "qt"))->ToObject();
if (qt.IsEmpty()) {
qt = v8::Object::New(isolate);
global->Set(gin::StringToV8(isolate, "qt"), qt);
}
qt->Set(gin::StringToV8(isolate, "webChannelTransport"), transport.ToV8());
}
最後附上一張整個調用的時序圖: