我们为什么要使用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
项目中完成, 参考文档
这里演示为JavaScript扩展AES
加密解密的方法。本节使用了 QT-AES 这个开源库来实现AES加密解密。
到 GitHub 下载 QT-AES 源码,只需要将 qaesencryption.cpp 和 qaesencryption.h 这两个文件拷贝到我们的项目中即可。
QyAppRenderer 从CefApp 和 CefRenderProcessHandler 继承,需要重写 OnWebKitInitialized
方法:
// QyAppRenderer.h class QyAppRenderer :public CefApp, public CefRenderProcessHandler { public: QyAppRenderer(); // ... 省略 void OnWebKitInitialized() OVERRIDE; // ... 省略 }; // QyAppRenderer.cpp void QyAppRenderer::OnWebKitInitialized() { qDebug() << "=====OnWebKitInitialized======="; }
在OnWebKitInitialized 方法中,我们需要通过下面步骤来完成扩展:
定义一段JavaScript 文本字符串,在这段JavaScript文本中,声明 native function,这个例子中演示了三个方法:
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={}; .... } )";
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
在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); } // ...省略
<!-- 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" ,控制台输出:
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代码,
窗口绑定也可以实现 JavaScript与C++ 代码的相互调用,其思路与扩展一样。
本节实现一个为JavaScript 中的window对象绑定一个只读的全局对象window.appEnv
,其中包含:
注: 这里采集的是windows 10 操作系统
当浏览器JavaScript上下文创建完毕之后,此时,Dom已经解析完毕了。在CefRenderProcessHandler接口的 OnContextCreated 函数 中,就可以往 JavaScript window对象中绑定各种JavaScript 对象(字符串,对象,函数等等)。
void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) { ... }
通过下面代码来获取硬件信息,硬件信息保存到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; } }
在这个函数中,主要就是创建各种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); }
在index.js 中,打印出 window.appEnv
进行查看
// index.js if (window.appEnv) { console.log(window.appEnv); }
启动应用,打开F12:
下一章节将会尝试JavaScript与C++之间如何实现异步调用。