本篇简介
上一节中, 我们完成了 CEF 各基本组件的封装, 并完成了浏览器基本功能的实现.>> 点这里回顾上节内容
本节我们将尝试扩展所实现的各组件, 实现浏览器与页面的双向通信.
本篇的小目标:
实现浏览器与页面的双向通信
原理简述
上一节曾提到过, CEF 应用在默认情况下包含很多子进程, 这些进程会共享同一个执行入口. 除了主进程的各类处理接口外, CEF 还提供了各类子进程的处理接口. 而页面到浏览器的消息通道就可以借助对渲染进程的控制来实现, 整体流程如下:
重载渲染进程的上下文初始化监听接口, 获取 V8 上下文引用
从 V8 上下文中获取所加载的窗口对象
借助 V8 处理器定义消息通道函数
向窗口对象中注册消息通道函数
完成上述步骤后, 在页面调用对应的消息通道函数时, V8 处理器则会相应地进行处理, 从而完成消息的发送.
另一方面, 实现浏览器到页面的消息通道和第二节中基于 Qt webEngine 的方法类似, CEF 也提供了执行 JS 脚本的方法, 只需在页面中定义好对应的消息接口, 并通过执行脚本方法执行该接口即可完成消息的发送.
因此, 实现双向通道主要的问题集中在针对渲染进程处理和 JS 脚本执行的扩展上. 接下来先就渲染进程处理进行说明.
渲染进程处理
为了实现对渲染进程的处理, 我们首先需要向上一节中封装的 QCefContext 中添加对渲染进程入口的解析和处理. 具体实现如下:
- int QCefContext::initCef(CefMainArgs& mainArgs)
- {
- CefRefPtr<CefApp> app;
- // 创建一个正确类型的 App Client
- if (!m_cmdLine->HasSwitch("type"))
- {
- app = new QCefApp();
- m_cefApp = CefRefPtr<QCefApp>((QCefApp*)app.get());
- }
- else
- {
- CefString procType = m_cmdLine->GetSwitchValue("type");
- bool typeJudge = (procType == "renderer");
- #ifdef CEF_LINUX
- typeJudge |= (procType == "zygote");
- #endif
- if (typeJudge)
- {
- app = new QCefRenderHandler();
- m_cefRenderer = CefRefPtr<QCefRenderHandler>((QCefRenderHandler*)app.get());
- }
- }
- // 后续处理与上一小节相同, 略过
- ...
- }
上面的实现除了处理了 CEF 主进程外, 还判断了子进程是否为渲染进程 (Windows 环境下的 renderer 进程和 Linux 环境下的 zygote 进程), 如果发现当前处理的是渲染进程, 则创建一个渲染进程处理器 QCefRenderHandler 的实例. QCefRenderHandler 的声明如下:
- class QCefRenderHandler : public CefApp,
- public CefRenderProcessHandler
- {
- public:
- QCefRenderHandler();
- ~QCefRenderHandler();
- CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() OVERRIDE
- {
- return this;
- }
- virtual void OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) OVERRIDE;
- private:
- CefRefPtr<QCefV8Handler> m_v8Handler;
- IMPLEMENT_REFCOUNTING(QCefRenderHandler)
- };
和主进程 CefApp 的实现类似, 这里也实现了 CefApp 接口, 此外额外实现了 CefRenderProcessHandler 接口的 OnContextCreated 方法, 来获取 V8 上下文的引用, 具体实现如下:
- void QCefRenderHandler::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
- {
- qDebug() <<"V8 Context Created!";
- // 取回 V8 上下文的 window 对象
- CefRefPtr<CefV8Value> object = context->GetGlobal();
- // 创建 "sendMessage" 函数, 作为消息通道使用.
- m_v8Handler = CefRefPtr<QCefV8Handler>(new QCefV8Handler(browser));
- CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction("sendMessage", m_v8Handler);
- // 将消息通道注册到 window 对象
- object->SetValue("sendMessage", func, V8_PROPERTY_ATTRIBUTE_NONE);
- }
上面的实现将 sendMessage 函数定义为消息通道, 并注册到了 window 对象上. sendMessage 函数的具体实现则放在 v8Handler 的实现中. QCefV8Handler 声明如下:
- class QCefV8Handler : public CefV8Handler
- {
- public:
- QCefV8Handler(CefRefPtr<CefBrowser> browser);
- ~QCefV8Handler();
- virtual bool Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception) OVERRIDE;
- private:
- CefRefPtr<CefBrowser> m_browser;
- IMPLEMENT_REFCOUNTING(QCefV8Handler)
- };
QCefV8Handler 通过实现 CEF V8 处理器的 Execute 执行方法, 完成对所加载的 JS 函数的过滤, 并进行相应的处理, 实现如下:
- bool QCefV8Handler::Execute(const CefString& name,
- CefRefPtr<CefV8Value> object,
- const CefV8ValueList& arguments,
- CefRefPtr<CefV8Value>& retval,
- CefString& exception)
- {
- if (name == "sendMessage")
- {
- if (arguments.size() == 1)
- {
- CefString msgStr = arguments.at(0)->GetStringValue();
- // 消息会被发送到 CefClient 的 OnProcessMessageReceived 接口方法
- m_browser->SendProcessMessage(PID_BROWSER, CefProcessMessage::Create(msgStr));
- retval = CefV8Value::CreateInt(0);
- }
- return true;
- }
- return false;
- }
这里首先对函数名和参数进行了校验, 之后调用 CefBrowser 的 IPC 方法 SendProcessMessage 向主进程的 CefClient 发送消息, 从而完成页面向浏览器主进程消息的传递.
实现消息通道
页面 -> 浏览器
要实现页面到浏览器的消息通道, 除了完成了上面渲染进程的控制扩展, 我们还需要在 QCefClient 中添加接收 IPC 消息的接口实现. 首先在 QCefClient 头文件中声明对 CefClient 接口的重载:
virtual bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefProcessId source_process, CefRefPtr<CefProcessMessage> message) OVERRIDE;
然后实现这个接口, 完成消息的接收处理:
- bool QCefClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
- {
- emit webMsgReceived(QString(message->GetName().ToString().c_str()));
- return true;
- }
可以看到这里只是对收到的消息进行了简单的转换, 并通过信号发送给感兴趣的下游控件使用. 在第四小节的实现中, 我们将 QCefClient 封装到了 QCefView 中, 因此在 QCefView 中也需要将这个信号转发给它的下游控件:
connect(cefClientPtr, SIGNAL(webMsgReceived(QString)), this, SIGNAL(webMsgReceived(QString)));
这样, QCefView 接收 JS 消息的通道就实现完成了.
这里额外讲解一下有关 js alert 的特殊处理. 要实现 js 调用 alert 方法时的弹窗提醒, 需要额外在 CefClient 中实现 CefJSDialogHandler 接口的 OnJSDialog 方法, 参考实现如下:
- bool QCefClient::OnJSDialog(CefRefPtr<CefBrowser> browser, const CefString& origin_url,JSDialogType dialog_type, const CefString& message_text,const CefString& default_prompt_text, CefRefPtr<CefJSDialogCallback> callback, bool& suppress_message)
- {
- QMessageBox::warning(NULL, "JavaScript Alert", QString(message_text.ToString().c_str()));
- return true;
- }
浏览器 -> 页面
承前所述, 浏览器到页面的消息发送通过 CEF 的 JS 脚本执行接口实现. 首先在 QCefView 中, 声明并实现一个执行 JS 脚本的方法:
- void QCefView::runJavaScript(QString script)
- {
- CefRefPtr<CefFrame> frame = m_cefClient->browser()->GetMainFrame();
- frame->ExecuteJavaScript(script.toStdString(), frame->GetURL(), 0);
- }
然后指定一个特定的 JS 方法, 作为消息通道使用:
- void QCefView::sendToWeb(QString msg)
- {
- runJavaScript(QString("recvMessage('%1');").arg(msg));
- }
如此, QCefView 发送 JS 的通道也实现完成了.
使用消息通道发送消息
完成了消息通道的实现, 接下来我们实际使用一下我们定义好的消息通道.
首先是 Qt 端的实现, 在 MainDlg 的 initWebView 方法中, 添加对 JS 消息的监听, 并将监听到的消息通过 QMessageBox 显示出来:
- connect(m_webview, SIGNAL(webMsgReceived(QString)), this, [this](QString msg){
- QMessageBox::information(this, "接收到 Web 消息", msg);
- });
然后添加文本输入和发送按钮, 并在按钮点击信号对应的槽中调用 QCefView 的消息发送方法:
- connect(ui->btnSend, &QPushButton::clicked, this, [this]() {
- QString msg = ui->editJsMsg->text();
- if(!msg.isEmpty()){
- m_webview->sendToWeb(msg);
- }
- });
接下来在页面端实现消息接收和发送的接口 msgutils.js:
- // 接收 qt 发送的消息
- function recvMessage(msg)
- {
- alert("接收到 Qt 发送的消息:" + msg);
- }
- // 控件控制函数
- function onBtnSendMsg()
- {
- var cmd = document.getElementById("待发送消息").value;
- window.sendMessage(cmd);
- }
可以看到这里我们使用了上面定义的 recvMessage 和 sendMessage 两个函数. 然后在页面上调用这些接口:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>CEF JS 通道测试 </title>
- </head>
- <body>
- <p>Cef Js Channel Test</p>
- <script type="text/javascript" src="./msgutils.js"></script>
- <input id="待发送消息" type="text" name="msgText" />
- <input type="button" value="发送消息到浏览器" onclick="onBtnSendMsg()" />
- </body>
- </html>
实际运行一下浏览器, 并加载我们实现的这个页面, 消息发送效果如下:
cef 消息通道 qt->web.png
cef 消息通道 web->qt.png
有关 CEF 消息通道的讲解就先进行到这里. 下一节将分析使用 CEF 接口实现 Https 双向认证的方法.
来源: http://www.jianshu.com/p/fb83fe2b6a97