QT集成CEF10-JavaScript与C++互调

我们为什么要使用CEF? 很多情况下都是为了能够实现JavaScript与 native C++之间的相互调用。即网页中的JavaScript调用的时候,触发本地C++代码的执行,比如访问硬件等JavaScript无法完成的功能。本地C++代码也可以回调JavaScript,比如本地代码收到操作系统的一些通知后,将通知内容转交给JavaScript代码来执行。

​ CEF中,JavaScript代码是运行在Renderer进行中的,理解这一点非常重要,native代码写的地方不合适,JavaScript代码在调用的时候,就无法正常工作。CEF使用V8 JS引擎执行内部的JS,每一个Frame在浏览器进程中都由一个属于自己的JS Context,而他们都运行在Renderer进程中。

​ 那我们如何才能使用native的 C++代码来扩展JavaScript的执行?主要是通过 CefRenderProcessHandler 中的两个方法:

  • virtual void OnWebKitInitialized() {}

    当 webkit 初始化完毕之后。在这个函数中,我们可以通过 CefRegisterExtension() 函数来注册JavaScript与C++代码之间的“映射”关系,官方管这种方式叫做扩展(Extension)

  • virtual void OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) {}

    JavaScript 上下文被创建以后,在这个函数中,我们可以为JavaScript中的 window 对象绑定属性和方法,官方将这种方式叫做 窗口绑定(window binding)

不管使用那种方式,都必须在渲染进程中完成。

本小节的代码都在 QyRender项目中完成, 参考文档
QT集成CEF10-JavaScript与C++互调

1. 扩展(Extenstion)

​ 这里演示为JavaScript扩展AES加密解密的方法。本节使用了 QT-AES 这个开源库来实现AES加密解密。

1.1 准备QT-AES

到 GitHub 下载 QT-AES 源码,只需要将 qaesencryption.cppqaesencryption.h 这两个文件拷贝到我们的项目中即可。
QT集成CEF10-JavaScript与C++互调

1.2 QyAppRenderer 类

QyAppRenderer 从CefApp 和 CefRenderProcessHandler 继承,需要重写 OnWebKitInitialized 方法:

// QyAppRenderer.h

class QyAppRenderer :public CefApp, public CefRenderProcessHandler {
public:
	QyAppRenderer();
    // ... 省略
	void OnWebKitInitialized() OVERRIDE;
    // ... 省略
};


// QyAppRenderer.cpp
void QyAppRenderer::OnWebKitInitialized() {
	qDebug() << "=====OnWebKitInitialized=======";
}

1.3 扩展

在OnWebKitInitialized 方法中,我们需要通过下面步骤来完成扩展:

1.3.1 定义JS 文本

定义一段JavaScript 文本字符串,在这段JavaScript文本中,声明 native function,这个例子中演示了三个方法:

  • app.encrypt(originText) //加密 ,参数为要加密的字符串
  • app.decrypt(encryptText) // 解密, 参数为要解密的密文字符串
  • app.sayHello(callback) // 参数为一个回调函数 callback =function(data) { … }
var app;
if(!app){
    app={};
    (function(){
        app.encrypt=function(originText){
            native function encrypt();//声明本地方法名
            return encrypt(originText); //执行本地方法
        }
        app.decrypt=function(encryptText){
            native function decrypt();//声明本地方法名
            return decrypt(encryptText); //执行本地方法
        }
        app.sayHello=function(callback) {
            native function sayHello();//声明本地方法名
            return sayHello(callback);//执行本地方法
        }
    })(); //JavaScript自执行函数
}

如果将这段JavaScript代码定义成字符串,格式不太美观,这里将它放到QT项目的资源文件中,使用的时候从资源文件中读取出来:

首先在项目 QyRender下创建一个文件夹resources, 然后在这个文件夹中创建 extention_js.js 文件,内容就是上面的JavaScript代码:
QT集成CEF10-JavaScript与C++互调

然后添加一个QT资源文件: extention_js.qrc
QT集成CEF10-JavaScript与C++互调

将 extention_js.js 文件添加到资源文件中

QT集成CEF10-JavaScript与C++互调

C++ 11 提供了多行字符串的语法,也可以使用这种语法,直接将JavaScript 代码定义到字符串中。

	std::string jsCode = R"(
		var app;
        if(!app){
			app={};
			....
        }
    )";

1.3.2 实现 CefV8Handler 接口

​ native代码需要实现 CefV8Handler 接口,为了方便,直接在 写在了 QyAppRenderer.cpp 文件中:

// QyAppRenderer.cpp
#pragma execution_character_set("UTF-8")
#include "QyAppRenderer.h"
#include <QDebug>
#include <QProcess>
#include "encrypt/qaesencryption.h"
#include <QCryptographicHash>
#include <QDateTime>
namespace {
}
class AppNativeV8Handler : public CefV8Handler {
	public:
		AppNativeV8Handler() {}
		bool Execute(const CefString& name, //JavaScript 函数名
			CefRefPtr<CefV8Value> object,     //JavaScript函数持有者
			const CefV8ValueList& arguments,//JavaScript 参数
			CefRefPtr<CefV8Value>& retval,  // JavaScript返回值
			CefString& exception) override {
			// 加密
			if (name == "encrypt") {
				std::string originText = arguments.at(0).get()->GetStringValue();
				QString	 rtText = encrypt(QString::fromStdString(originText));
				//返回值交给 retval
				retval = CefV8Value::CreateString(rtText.toStdString());
				return true;
			}
			// 解密
			if (name == "decrypt") {
				std::string encryptText = arguments.at(0).get()->GetStringValue();
				QString	 rtText = decrypt(QString::fromStdString(encryptText));
				retval = CefV8Value::CreateString(rtText.toStdString());
				return true;
			}
			// sayHello 带回调的JavaScript 函数
			// 名称为 sayHello, 参数数量为1,而且类型是一个函数
			if (name == "sayHello" && arguments.size() == 1 && arguments[0]->IsFunction()) {
				// 获取回调函数
				CefRefPtr<CefV8Value> callback = arguments[0];
				QDateTime now = QDateTime::currentDateTime();//获取系统当前时间
				time_t nowTime = now.toTime_t();
				//转换成CefDate 类型
				CefTime cefNow(nowTime);
				CefRefPtr<CefV8Value> callbackFunctionParam = CefV8Value::CreateDate(cefNow);

				CefV8ValueList arguments;
				arguments.push_back(callbackFunctionParam);

				// 执行JavaScript回调,并将参数传递给它,参数是一个CefV8ValueList
				callback->ExecuteFunction(NULL, arguments);
				return true;
			}
			return false;
		}

	private:
		// aes加密算法密钥
		QString _aes_secret_key = "a0123456789";

		/*
		* AES算法字符串加密
		*/
		QString encrypt(QString originText) {

			QAESEncryption encryption(QAESEncryption::AES_128, QAESEncryption::ECB, QAESEncryption::ZERO);
			QByteArray hashKey = QCryptographicHash::hash(_aes_secret_key.toUtf8(), QCryptographicHash::Md5);
			QByteArray encodedText = encryption.encode(originText.toUtf8(), hashKey);
			QString str_encode_text = QString::fromLatin1(encodedText.toBase64());
			qDebug() << "encrypt:" << originText << "    result:" << str_encode_text;
			return str_encode_text;
		}

		/*
		* AES算法字符串解密
		*/
		QString decrypt(QString encryptText) {
			QAESEncryption encryption(QAESEncryption::AES_128, QAESEncryption::ECB, QAESEncryption::ZERO);
			QByteArray hashKey = QCryptographicHash::hash(_aes_secret_key.toUtf8(), QCryptographicHash::Md5);
			QByteArray decodedText = encryption.decode(QByteArray::fromBase64(encryptText.toLatin1()), hashKey);
			qDebug() << "decrypt:" << encryptText << "    result:" << QString::fromUtf8(decodedText);
			return QString::fromUtf8(decodedText);
		}
		IMPLEMENT_REFCOUNTING(AppNativeV8Handler);
	};

代码比较长,其实这里就一个方法,native的 c++ 代码在这里执行, JavaScript函数名等信息通过参数传递过来。这个方法中,通过判断函数的名称来判断应该执行什么样的代码。常用的JavaScript 类型在CEF中都有对应的类型,详情可以见参考文档

bool Execute(const CefString& name, //JavaScript 函数名
			CefRefPtr<CefV8Value> object,     //JavaScript函数持有者
			const CefV8ValueList& arguments,//JavaScript 参数
			CefRefPtr<CefV8Value>& retval,  // JavaScript返回值
			CefString& exception) override

1.3.3 OnWebKitInitialized 函数实现

在QyAppRenderer 类中实现 OnWebKitInitialized 函数:

// QyAppRenderer.cpp
// ...省略
void QyAppRenderer::OnWebKitInitialized() {
	//1. 从资源文件中获取要扩展的JavaScript代码
	QFile jsFile(":/extention_js.js");
	jsFile.open(QIODevice::ReadOnly);
	QByteArray jsFileData = jsFile.readAll();
	//2.  JavaScript 字符串
	QString jsCode(jsFileData);

	// 3. Register app extension module
	// JavaScript里调用函数时,就会去通过CefRegisterExtension注册的CefV8Handler列表里查找
	// 找到"v8/app"对应的CefV8HandlerImpl,就调用它的Execute方法
	CefRefPtr<CefV8Handler> v8Handler = new AppNativeV8Handler();
	CefRegisterExtension("v8/app", jsCode.toStdString(), v8Handler);
}
// ...省略

1.3.4 网页中使用

QT集成CEF10-JavaScript与C++互调

  • 第一个文本框输入字符串,点击"加密",结果会在第二个文本框中显示出来
  • 点击"解密" ,解密结果会在下面显示
  • 点击“测试 app.sayHello” 按钮,c++代码会回调sayHello传入的回调函数
<!-- index.html  -->
<div>
        <input id="msg" type="text" /> <button id="btnEncrypt">加密</button> <br />
        加密结果:
        <input id="msgEncryptResult" type="text" />
        <button id="btnDncrypt">解密</button><br />
        <button id="btnTestSayHello">测试 app.sayHello</button><br />
</div>
<div id="dncryptInfo">

</div>
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/index.js"></script>
// index.js
console.log(app);
window.onload = () => {
    $("#btnEncrypt").click(() => {
        // 加密
        var result = app.encrypt($("#msg").val());
        $("#msgEncryptResult").val(result);
    });

    $("#btnDncrypt").click(() => {
        // 解密
        var result = app.decrypt($("#msgEncryptResult").val());
        $("#dncryptInfo").html("解密结果:" + result);
    });
    $("#btnTestSayHello").click(() => {
		// c++回调函数,data是c++传入的时间对象
        app.sayHello((data) => {
            console.log(data);
            console.log("Hello" + data);
        });
    });


}

启动应用,按F12打开开发者工具:
QT集成CEF10-JavaScript与C++互调

可以看到, “app” 对象被成功注册,app对象中包含了三个函数: decrypt,encrypt, sayHello
QT集成CEF10-JavaScript与C++互调

点击"测试app.sayHello" ,控制台输出:
QT集成CEF10-JavaScript与C++互调

1.3.5 扩展小结

  • 渲染进程CefApp实现CefRenderProcessHandler接口中的 OnWebKitInitialized 函数
  • OnWebKitInitialized 函数中定义JavaScript代码文本,在JavaScript代码文本中声明native方法。
  • native方法的具体执行的C++代码要通过 重写CefV8Handler 类中的 Execute方法来完成。
  • 通过函数CefRegisterExtension 将JavaScript代码和CefV8Handler 关联起来。

JavaScript对应的数据类型的值可以通过 CefV8Value:: CreateXXX 来创建,JavaScript各种数据类型的值的类型都是 CefV8Value

CefV8Value中的

  • CefRefPtr<CefV8Value> ExecuteFunction(CefRefPtr<CefV8Value> object,const CefV8ValueList& arguments)
  • virtual CefRefPtr<CefV8Value> ExecuteFunctionWithContext(CefRefPtr<CefV8Context> context,CefRefPtr<CefV8Value> object,const CefV8ValueList& arguments)

这两个方法可以执行JavaScript代码,

2. 窗口绑定(window binding)

窗口绑定也可以实现 JavaScript与C++ 代码的相互调用,其思路与扩展一样。

本节实现一个为JavaScript 中的window对象绑定一个只读的全局对象window.appEnv,其中包含:

QT集成CEF10-JavaScript与C++互调

  • cpuName:cpu名称
  • cpuSn:cpu序列号
  • encrypt:加密方法 (就是上面的AES加密放大)
  • decrypt:解密方法(就是上面的AES解密方法)
  • hddSn: 硬盘序列号
  • ipAddr: 本机IP地址
  • macAddr: 本机网卡mac地址
  • memory: 本机内存

注: 这里采集的是windows 10 操作系统

​ 当浏览器JavaScript上下文创建完毕之后,此时,Dom已经解析完毕了。在CefRenderProcessHandler接口的 OnContextCreated 函数 中,就可以往 JavaScript window对象中绑定各种JavaScript 对象(字符串,对象,函数等等)。

2.1 OnContextCreated 函数

void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefRefPtr<CefV8Context> context) {
	...
}
  • browser: 当前的浏览器对象
  • frame : 当前窗口frame对象.
  • context : JavaScript上下文

2.2 获取硬件信息方法

通过下面代码来获取硬件信息,硬件信息保存到AppEnv 结构体中,这里通过调用 windows中的wmic 命令来获取系统硬件信息

// QyAppRenderer.cpp
namespace {
    // 获取cpu名称
	const QString CPU_NAME_CMD = "wmic cpu get name";
	// 查询cpu序列号
	const QString CPU_ID_CMD = "wmic cpu get processorid";
	// 查看硬盘序列号
	const QString DISK_NUM_CMD = "wmic diskdrive where index=0 get serialnumber";
	// 查询网卡和IP地址
	const QString IP_MAC_ADDRESS_CMD = "wmic PATH Win32_NetworkAdapterConfiguration WHERE \"IPEnabled = True and not MACAddress like '00:%'\" get Ipaddress,MACAddress";
	// 内存大小
	const QString MEM_SIZE_CMD = "wmic ComputerSystem get TotalPhysicalMemory";

	//IP地址正则表达式
	QString IP_REG_PATTERN = "((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)";
	//MAC地址正则表达式
	QString MAC_REG_PATTERN = "([0-9a-fA-F]{2})(([/\\s:][0-9a-fA-F]{2}){5})";
    
    //数据保存到结构体中
    typedef struct 
	{
		QString cpuSn;			//cpu序列号
		QString cpuName;	// cpu名称
		QString ipAddr;			//IP地址
		QString macAddr;		//网卡mac地址
		QString hddSn;			// 硬盘序列号
		qint64 memory;				//内存大小,单位 字节
	} AppEnv;
    // 执行命令,获取返回结果
    QString getWMIC(const QString& cmd)
	{
		QProcess p;
		p.start(cmd);
		p.waitForFinished();
		QString result = QString::fromLocal8Bit(p.readAllStandardOutput());
		QStringList list = cmd.split(" ");
		result = result.remove(list.last(), Qt::CaseInsensitive);
		result = result.replace("\r", "");
		result = result.replace("\n", "");
		result = result.simplified();
		return result;
	}
    
    // 获取应用程序运行环境
	static AppEnv getAppEnv() {
		AppEnv appEnv;
		appEnv.cpuName = getWMIC(CPU_NAME_CMD); // CPU名称
		appEnv.cpuSn = getWMIC(CPU_ID_CMD); // CPU序列号

		QString	ipMac = getWMIC(IP_MAC_ADDRESS_CMD); //IP地址和mac地址
		QRegExp ipRx(IP_REG_PATTERN); //IP正则
		if (ipMac.indexOf(ipRx) >= 0) {
			appEnv.ipAddr = ipRx.cap(0);
		}

		QRegExp macRx(MAC_REG_PATTERN);//MAC正则
		if (ipMac.indexOf(macRx) >= 0) {
			appEnv.macAddr = macRx.cap(0);
		}
		appEnv.hddSn = getWMIC(DISK_NUM_CMD); //硬盘

		appEnv.memory = getWMIC(MEM_SIZE_CMD).toLongLong();

		return appEnv;
	}
    
    }

2.3 OnContextCreated 函数实现

​ 在这个函数中,主要就是创建各种JavaScript 对象对应的 CefV8Value,如果创建的是JavaScript函数,则需要CefV8Value::CreateFunction 来创建,创建函数就需要用到 CefV8Handler 了,CefV8Handler的作用就是native JavaScript函数对应的C++代码。

如果C++要调用 JavaScript代码,则只需要使用 CefV8Value 调用 ExecuteFunction 或者 ExecuteFunctionWithContext

void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
	CefRefPtr<CefFrame> frame,
	CefRefPtr<CefV8Context> context) {
	// 获取app运行环境硬件信息
	AppEnv appEnv = ::getAppEnv();

	 Retrieve the context's window object.
	CefRefPtr<CefV8Value> window = context->GetGlobal();

	//创建一个JavaScript 对象,全部为只读属性
	CefRefPtr<CefV8Value> appEnvObject = CefV8Value::CreateObject(NULL, NULL);
    // 为JavaScript 对象设置属性值
	appEnvObject->SetValue("cpuSn", CefV8Value::CreateString(appEnv.cpuSn.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY);
	appEnvObject->SetValue("cpuName", CefV8Value::CreateString(appEnv.cpuName.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY);
	appEnvObject->SetValue("ipAddr", CefV8Value::CreateString(appEnv.ipAddr.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY);
	appEnvObject->SetValue("macAddr", CefV8Value::CreateString(appEnv.macAddr.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY);
	appEnvObject->SetValue("hddSn", CefV8Value::CreateString(appEnv.hddSn.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY);
	appEnvObject->SetValue("memory", CefV8Value::CreateString(QString::number(appEnv.memory).toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY);
	// JavaScript 函数就需要 CefV8Handler 来处理。这里使用了前面定义的 AppNativeV8Handler
	CefRefPtr<CefV8Handler> handler = new AppNativeV8Handler();
	CefRefPtr<CefV8Value> funcEncrypt = CefV8Value::CreateFunction("encrypt", handler);
	CefRefPtr<CefV8Value> funcDecrypt = CefV8Value::CreateFunction("decrypt", handler);
	appEnvObject->SetValue("encrypt", funcEncrypt, V8_PROPERTY_ATTRIBUTE_NONE);
	appEnvObject->SetValue("decrypt", funcDecrypt, V8_PROPERTY_ATTRIBUTE_NONE);

	//绑定到window对象上,同样为只读属性
	window->SetValue("appEnv", appEnvObject, V8_PROPERTY_ATTRIBUTE_READONLY);
}

2.4 测试是否绑定成功

在index.js 中,打印出 window.appEnv 进行查看

// index.js
if (window.appEnv) {
    console.log(window.appEnv);
}

启动应用,打开F12:

QT集成CEF10-JavaScript与C++互调

下一章节将会尝试JavaScript与C++之间如何实现异步调用。

上一篇:VS2019+qt string,QString中文乱码问题解决及string转换为QString时中文乱码问题


下一篇:QString