我们为什么要使用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
项目中完成, 参考文档
1. 扩展(Extenstion)
这里演示为JavaScript扩展AES
加密解密的方法。本节使用了 QT-AES 这个开源库来实现AES加密解密。
1.1 准备QT-AES
到 GitHub 下载 QT-AES 源码,只需要将 qaesencryption.cpp 和 qaesencryption.h 这两个文件拷贝到我们的项目中即可。
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资源文件: extention_js.qrc
将 extention_js.js 文件添加到资源文件中
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 网页中使用
- 第一个文本框输入字符串,点击"加密",结果会在第二个文本框中显示出来
- 点击"解密" ,解密结果会在下面显示
- 点击“测试 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打开开发者工具:
可以看到, “app” 对象被成功注册,app对象中包含了三个函数: decrypt,encrypt, sayHello
点击"测试app.sayHello" ,控制台输出:
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
,其中包含:
- 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:
下一章节将会尝试JavaScript与C++之间如何实现异步调用。