QT集成CEF11-JavaScript与C++异步调用

​ 在上一个章节《QT集成CEF10-JavaScript与C++互调》 中,JavaScript与C++之间已经能够相互调用了,但是它们只能在renderer进程中进行通信,不能跨进程通信。当JavaScript 也可以与renderer进行实现异步通信,可以参考上一章节中的sayHello 实现.

​ 本章节将通过一个 使用JavaScript读取本地文本文件的案例来探索在CEF中如何实现JavaScript与C++之间跨进程的异步调用,关于CEF的进程间通信,在《QT集成CEF08-多进程通信》已经介绍过,实现本案例就需要用到进程间的通信。收发进程间的消息主要靠以下几个接口实现:

  • renderer进程

    发消息通过 CEFFrame::SendProcessMessage 函数来发送消息

    收消息通过 CefRenderProcessHandler::OnProcessMessageReceived 方法 来接收消息

  • browser进程

    发消息通过 CEFFrame::SendProcessMessage 函数来发送消息

    收消息通过 CEFClient::OnProcessMessageReceived 函数来接收消息

1. 基本思路整理

​ 我们要实现的是通过JavaScript调用,打开文件选择对话框,选择文本文件后读取文件中的内容交给JavaScript,JavaScript将内容显示在网页上。

​ JavaScript 要打开本地对话框,在renderer进程中是无法实现的,需要:

  1. 将JavaScript调用消息发送给browser进程
  2. browser进程收到消息后发送QT信号给主窗口
  3. 主窗口打开文件选择对话框,选择文件后读取文件内容
  4. 将文件内容通过消息回传给renderer进程
  5. renderer进程调用JavaScript 注册的回调函数
  6. JavaScript在回调函数中接收文本内容后,将内容渲染到网页中。

为了简化这种进程间通信开发,从1574版本开始,CEF提供了在Render进程执行的JavaScript和在Browser进程执行的C++代码之间同步通信的转发器,我暂且叫他消息路由(MessageRouter),其中用到的类分布在两侧:

  • browser进程侧:
    • 消息路由器: CefMessageRouterBrowserSide
    • 消息处理器: CefMessageRouterBrowserSide::Handler
  • render进程侧:
    • 消息路由器: CefMessageRouterRendererSide

通过配置,会为JavaScript window对象绑定一个 cefQuery函数, JavaScript通过调用这个函数来发起消息:

// 发起一个消息,返回消息ID
var request_id = window.cefQuery({
    request: 'my_request', // 消息名称
    persistent: false, // 消息是否要持久化,就是将消息保存起来
    onSuccess: function(response) {}, // 成功情况下的回调, response 为C++ 回传的数据
    onFailure: function(error_code, error_message) {} // 失败情况下的回调,error_code 为错误编码,error_message 为错误消息
});

// 通过消息ID取消消息
window.cefQueryCancel(request_id);

每个消息在传递的时候,都携带了以下几个内容:

  • CefBrowser 对应浏览器对象
  • CefFrame 对应网页中的frame,因为一个网页中如果使用了iframe,就会有多个frame,这个对象指定了具体是哪个frame中发送的消息
  • query_id 一个64位的整数,其实就是消息ID。每个消息都有唯一的消息ID。这个query_id 还是比较重要的,因为消息传递过程本身就是异步的,并非同步调用。我们需要将 query_id 和相关联的JavaScript注册的回调暂存起来, 消息在回传的时候,通过 query_id 能够找到JavaScript 注册的回调函数,然后执行这个回调函数将数据传给JavaScript。
  • request 消息的名称,一个字符串
  • callback JavaScript注册的回调函数
# 消息处理器源码
class Handler {
   public:
    typedef CefMessageRouterBrowserSide::Callback Callback;

    ///
    // Executed when a new query is received. |query_id| uniquely identifies the
    // query for the life span of the router. Return true to handle the query
    // or false to propagate the query to other registered handlers, if any. If
    // no handlers return true from this method then the query will be
    // automatically canceled with an error code of -1 delivered to the
    // JavaScript onFailure callback. If this method returns true then a
    // Callback method must be executed either in this method or asynchronously
    // to complete the query.
    ///
    virtual bool OnQuery(CefRefPtr<CefBrowser> browser,
                         CefRefPtr<CefFrame> frame,
                         int64 query_id,
                         const CefString& request,
                         bool persistent,
                         CefRefPtr<Callback> callback) {
      return false;
    }

    ///
    // Executed when a query has been canceled either explicitly using the
    // JavaScript cancel function or implicitly due to browser destruction,
    // navigation or renderer process termination. It will only be called for
    // the single handler that returned true from OnQuery for the same
    // |query_id|. No references to the associated Callback object should be
    // kept after this method is called, nor should any Callback methods be
    // executed.
    ///
    virtual void OnQueryCanceled(CefRefPtr<CefBrowser> browser,
                                 CefRefPtr<CefFrame> frame,
                                 int64 query_id) {}

    virtual ~Handler() {}
  };

思路介绍完了,下面就从JavaScript 代码开始,实现本地文本文件的读取。本节代码参考官方文档

2. JavaScript中发起消息

首先是HTML页面:

<div>
   <button id="btnReadTextFile">读取文本文件</button>
</div>
<div id="textFileContent" style="border:1px solid #808080;height:400px;overflow-y:auto">
</div>
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/index.js"></script>

javaScript, 发起消息调用, 消息名称为MSG_READ_TEXT_FILE ,后端 c++会用到。这里很简单,就是将消息发出去,然后等待 c++端回调接收数据,然后将接收的数据渲染到 div 中。

window.onload = () => {
    console.log(window.cefQuery);
    $("#btnReadTextFile").click(() => {
        window.cefQuery({
            request: 'MSG_READ_TEXT_FILE', // 消息名称
            onSuccess: function (response) {
                $("#textFileContent").text(response); // 将结果放到div中
            },
            onFailure: function (error_code, error_message) {
                $("#textFileContent").text("错误:" + error_code + "  " + error_message);
            }
        });
    });
}

3. renderer侧

​ renderer侧加入 消息路由很简单:

  1. OnWebKitInitialized 函数中创建路由对象

  2. OnContextCreated 函数中注册 window.cefQuery

  3. OnContextReleased 函数中释放路由对象

  4. OnProcessMessageReceived 函数中接收 browser进程发送的消息

    代码很简单,只需要调用路由对象对应的方法即可:

// QyAppRenderer.h
// ...省略代码
// 引入消息路由组件
#include "include/wrapper/cef_message_router.h"
class QyAppRenderer :public CefApp, public CefRenderProcessHandler {
public
    // ...省略代码
    void OnWebKitInitialized() OVERRIDE;
    void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
                                         CefRefPtr<CefFrame> frame,
                                         CefRefPtr<CefV8Context> context) OVERRIDE;
    void OnContextReleased(CefRefPtr<CefBrowser> browser,
                           CefRefPtr<CefFrame> frame,
                           CefRefPtr<CefV8Context> context) OVERRIDE;
    bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
                                  CefRefPtr<CefFrame> frame,
                                  CefProcessId source_process,
                                  CefRefPtr<CefProcessMessage> message);
private:
	// 消息路由对象
	CefRefPtr<CefMessageRouterRendererSide> m_message_router;
     // ...省略代码
}

// QyAppRenderer.cpp
// ...省略代码
void QyAppRenderer::OnWebKitInitialized() {
	if (m_message_router == NULL) {
		// Create the renderer-side router for query handling.
		CefMessageRouterConfig config;
		//创建 渲染进程侧消息路由对象
		m_message_router = CefMessageRouterRendererSide::Create(config);
	}
}
void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefRefPtr<CefV8Context> context) {
	// 注册 window.cefQuery
	m_message_router->OnContextCreated(browser, frame, context);
}
void QyAppRenderer::OnContextReleased(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefRefPtr<CefV8Context> context) {
	// 释放消息路由
	m_message_router->OnContextReleased(browser, frame, context);
}
bool QyAppRenderer::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefProcessId source_process,
	CefRefPtr<CefProcessMessage> message) {
	// 收到 browser进程发送的消息
	return m_message_router->OnProcessMessageReceived(browser, frame, source_process, message);
}

注意: renderer侧代码写在 QyRender项目中:

在这里插入图片描述

4. browser侧

browser 侧相对比较复杂,在 browser侧主要做的就是消息处理。这里我们从消息的处理开始。

4.1 消息处理类

消息处理类需要我们自己定义,从 CefMessageRouterBrowserSide::Handler 继承。 消息处理收到路由过来的消息后,需要打开文件对话框窗体,但这个操作只能在UI线程中完成,而 消息处理并非运行在UI线程,所以这就需要用QT的信号来处理,所以 消息处类需要继承QT 的 QObject ,开启 Q_OBJECT 宏它才能定义 signal,代码如下:

  • 继承QObject 与 CefMessageRouterBrowserSide::Handler 并引入Q_OBJECT 宏

  • 重写OnQuery 函数和 OnQueryCanceled。 在 OnQuery中先保存 query_id与 JavaScript注册的回调函数之间的映射,即 JavaScript的 onSuccessonFailure然后发射信号给UI线程

  • 定义void onReadFile(qint64 query_id) 信号,发送信号的时候,需要将 消息ID发射出去,因为这里是多线程操作,没有消息ID就无法找到 JavaScript注册的回调函数,这样就没法调用了,所以这个 query_id 很重要。

    注意 这是QT信号, 参数类型必须在QT中注册,这里使用的是 qint64 ,而不是 int64,因为 int64 并没有在QT中注册。

  • 定义 handleTextFileContent 函数,用来接收 文件内容,他需要传递 query_id 找到JavaScript 注册的回调。

// cef_query_handler.h
#pragma once
#include "QObject"
#include "include/wrapper/cef_message_router.h"
class CefQueryHandler : public QObject,
	public CefMessageRouterBrowserSide::Handler
{
	Q_OBJECT
public:
	explicit CefQueryHandler() {
	}

	//  从渲染进程JavaScript 中发出 window.cefQuery 调用,收到渲染进程的消息后进行处理.
	bool OnQuery(CefRefPtr<CefBrowser> browser, //哪个浏览器
		CefRefPtr<CefFrame> frame, //浏览器中的哪个frame发出的
		int64 query_id, //消息ID
		const CefString& request, // 消息名称
		bool persistent,
		CefRefPtr<Callback> callback) override;
	// 取消
	void CefQueryHandler::OnQueryCanceled(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefFrame> frame,
		int64 query_id) override;

	// 向主线程发送信号,弹出选择文件对话框,然后读取文件
	// 读取结束后,调用该方法处理文件读取的结果,回传到渲染进程
	void handleTextFileContent(int errorCode, QString fileContent, int64 query_id);

	//信号
signals:
	// 读取文件
	void onReadFile(qint64 query_id);

private:
	// 用来保存 query_id 和 callback之间的映射关系
	std::map<int64, CefRefPtr<Callback>> m_pendings;
};
// cef_query_handler.cpp
#pragma execution_character_set("UTF-8")
#include "cef_query_handler.h"
#include <QDebug>
namespace {
    //请求名称,与JavaScript 代码中的 MSG_READ_TEXT_FILE 保持一致
	const char kTestMessageName[] = "MSG_READ_TEXT_FILE";
}

//  从渲染进程JavaScript 中发出 window.cefQuery 调用,收到渲染进程的消息后进行处理.
bool CefQueryHandler::OnQuery(CefRefPtr<CefBrowser> browser, //哪个浏览器
	CefRefPtr<CefFrame> frame, //浏览器中的哪个frame发出的
	int64 query_id, //消息ID
	const CefString& request, // 消息名称
	bool persistent, // 是否持久化
	CefRefPtr<Callback> callback) { // 回调函数 onSuccess和 onFailure

	const std::string& message_name = request;
	if (message_name == kTestMessageName) {
		//这里需要将 对象和 callback对象暂时保存到map中
		//以便线程执行完毕之后,能够找到它们
		m_pendings.insert(std::make_pair(query_id, callback));
		qDebug() << "========OnQuery========";
        // 发射信号给UI线程
		emit  onReadFile(query_id);
		return true;
	}
	return false;
}
// UI线程读取完文件内容后,回传文件内容和 query_id,然后调用JavaScript回调
void CefQueryHandler::handleTextFileContent(int errorCode, QString fileContent, int64 query_id) {
	//获取回调对象
	CefRefPtr<Callback> callback = m_pendings[query_id];
	if (errorCode == -1) {
		//读取文件出错
		callback->Failure(-1, QString("读取文件失败").toStdString());
	}
	else {
		callback->Success(fileContent.toStdString());
	}
	// 清除掉query_id 与callback的映射
	auto it = m_pendings.find(query_id);
	m_pendings.erase(it);
}

// 取消
void CefQueryHandler::OnQueryCanceled(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int64 query_id)
{
	auto it = m_pendings.find(query_id);
	if (it != m_pendings.end())
	{
		it->second->Failure(-1, "canceled");
		m_pendings.erase(it);
	}
}

4.2 UI 线程读取文件

在 MainWindow中创建 消息处理器CefQueryHandler ,然后连接它的 onReadFile 信号给槽函数,槽函数执行完毕之后,调用 CefQueryHandlerhandleTextFileContent 函数回传数据:

//mainwindow.h
// ...省略代码
class MainWindow : public QMainWindow
{
// ...省略代码
private slots:
    	// 处理CefQueryHandler中的onReadFile信号的槽函数
	void onReadFile(qint64 query_id);
private:
// ...省略代码
	CefQueryHandler* m_cef_query_handler;
};

//mainwindow.cpp
// 构造方法中初始化CefQueryHandler,注意程序退出 delete掉CefQueryHandler
MainWindow::MainWindow(SimpleApp* cefApp, QWidget* parent)
	: QMainWindow(parent), m_cefApp(cefApp), m_cef_query_handler(new CefQueryHandler)
{
	//...省略代码
}
//...省略代码
void MainWindow::createBrowserWindow() {
	// SimpleHandler 需要用到CefQueryHandler
	CefRefPtr<SimpleHandler> handler(new SimpleHandler(false, m_cef_query_handler));
	//...省略代码
	// 连接 CefQueryHandler中的onReadFile信号
	connect(m_cef_query_handler, &CefQueryHandler::onReadFile, this, &MainWindow::onReadFile);
}

//读取文件
void MainWindow::onReadFile(qint64 query_id) {
	qDebug() << "mainWindow========onReadFile";
	QString fileName = QFileDialog::getOpenFileName(NULL, "文件对话框", "D:", "文本文件(*txt)");
	QFile file(fileName);
	if (file.exists()) { // 如果文件存在
		// 读取文件内容
		if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
			QString content = file.readAll();
			file.close();
			//int errorCode, QString fileContent, int64 query_id
            //成功情况下回传数据,errorCode是 0
			m_cef_query_handler->handleTextFileContent(0, content, query_id);
			return;
		}
	}
     //失败情况下回传数据,errorCode是 -1
	m_cef_query_handler->handleTextFileContent(-1, "读取文件失败", query_id);
}

4.3 browser侧路由器

消息需要靠browser侧路由器来完成,SimpleHandler 类中需要持有 CefMessageRouterBrowserSideCefMessageRouterBrowserSide::Handler 对象:

// simple_handler.h
// ...省略代码
class SimpleHandler :public QObject, public CefClient
	, public CefLifeSpanHandler
	, public CefKeyboardHandler
	, public CefRequestHandler { //用到了CefRequestHandler的OnBeforeBrowse
	Q_OBJECT
public:
    // 实例化它的时候,需要传入 CefQueryHandler,CefQueryHandler在 MainWindow中实例化的
	explicit SimpleHandler(bool use_views, CefQueryHandler* m_cef_query_handler);
	~SimpleHandler();
	// CefLifeSpanHandler methods:
    // 在这里创建路由器,并为路由器设置消息处理器
	virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
    // ...省略代码
    // 当最后一个浏览器已经关闭,释放消息路由.
	virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
	// ...省略代码
    // 接收渲染进程的消息
	virtual bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefFrame> frame,
		CefProcessId source_process,
		CefRefPtr<CefProcessMessage> message) OVERRIDE;

	//CefRequestHandler methods:
	virtual bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefFrame> frame,
		CefRefPtr<CefRequest> request,
		bool user_gesture,
		bool is_redirect) OVERRIDE;
// ...省略代码
private:
	const bool use_views_;
    // ...省略代码
	// 消息路由器
	CefRefPtr<CefMessageRouterBrowserSide> m_message_router;
    // 消息处理器
	std::unique_ptr<CefMessageRouterBrowserSide::Handler> m_message_handler;
    // ...省略代码
};

// simple_handler.cc

// 注意成员对象 m_message_handler的类型是 std::unique_ptr<CefMessageRouterBrowserSide::Handler> 调用了它的reset方法来包装 CefQueryHandler
SimpleHandler::SimpleHandler(bool use_views, CefQueryHandler* cef_query_handler)
	: use_views_(use_views), is_closing_(false) {
    //  ...省略代码
	m_message_handler.reset(cef_query_handler);
}

// 创建路由器,并为路由器设置消息处理器
void SimpleHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
	 //  ...省略代码
	if (!m_message_router) {
		// browser进程侧 消息路由配置, renderer进程中JavaScript发出消息和取消消息使用 
		// window.cefQuery 和 window.cefQueryCancel , 如果不想使用这两个名字,可以在这里配置
		CefMessageRouterConfig config;
		//    config.js_query_function = "cefQuery";
		//    config.js_cancel_function = "cefQueryCancel";
		m_message_router = CefMessageRouterBrowserSide::Create(config);
		// 为消息路由设置 消息处理器,即从 render进程发送过来的消息交给谁处理。
		m_message_router->AddHandler(m_message_handler.get(), false);
	}
}
// CefRequestHandler 接口中的方法 当浏览器发出请求资源之前调用。 返回false 浏览器继续发出请求 返回true,浏览器停止导航。
bool SimpleHandler::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefRefPtr<CefRequest> request,
	bool user_gesture,
	bool is_redirect) {
	m_message_router->OnBeforeBrowse(browser, frame);
	return false;
}
// 当最后一个浏览器已经关闭,释放消息路由.
void SimpleHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
	 //  ...省略代码
	if (browser_list_.empty()) {
		//  ...省略代码
		// 当最后一个浏览器已经关闭,释放消息路由.
		m_message_router->RemoveHandler(m_message_handler.get());
		m_message_handler.reset();
		m_message_router = nullptr;
	}
}
// 路由器接收renderer进程发送的消息
bool SimpleHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefProcessId source_process,
	CefRefPtr<CefProcessMessage> message) {
	CEF_REQUIRE_UI_THREAD();
	return m_message_router->OnProcessMessageReceived(browser, frame,
		source_process, message);
}

5. 编译运行

分别编译 QyRender项目和 QyCefVS项目,运行后选择一个本地的文件,此时已经能够正常工作了
在这里插入图片描述

代码请访问 GitHub qt_cef_11分支

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

paopao_wu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值