CEF C++与JS 交互 全部
摘要
由于工作需要,公司开辟一个新项目需要用到CEF框架,由此去认识了一段时间的CEF框架,记录下一些心得。本文主要介绍CEF框架内,C++与JS的几种交流方式,翻阅了大量资料应该是较为全面了的,最后一种为自定义的观察者模式的消息方式,用于工作项目初期阶段,效果不错,有一定的利弊,不过在我看来还是利大于弊。此文适合有一定JS、C++、CEF框架基础的开发者阅读,推荐阅读刘晓伦老师的《CEF 桌面软件开发实战》课程零基础认识CEF框架。博文谨代表个人开发的经验之谈,有需要的小伙伴酌情获取,辩证思考,也欢迎小伙伴们在评论区纠错补充。
开发环境:win10、VS2019(C++17)、cef_binary_94.4.11_windows32
引流:CEF、CEF3、C++与JS交互、C++调用JS、JS调用C++、发布订阅模式、观察者模式、信号与槽、
声明:
本文作者原创,转载请附上文章出处与本文链接。
@
正文:
C++与JS交互首先分为同步通信方式和异步通信方式,同步通信方式适用条件较少,性能影响大,有一定的风险本文并不会过多阐述,更多的是详解异步通信方式。
同步通信:
如果需要使CEF与前端界面同步请求的话,可以考虑使用XMLHttpRequest,XMLHttpRequest在等待Browser进程的网络响应的时候会等待。Browser进程可以通过自定义scheme Handler或者网络交互处理XMLHttpRequest。前端界面部署于Render进程上,会对同步通信Render进程的性能造成负面影响,甚至可能崩溃,这应该被尽可能避免。同步通信实例参考资料:https://www.jianshu.com/p/845771fc650b
异步通信:
一、执行JavaScript(C++调用JS)
在CEF中执行JS最简单的方法是使用CefFrame::ExecuteJavaScript()函数,只要有CefRefPtr
- JS 执行上下文创建完成后调用
// ps: Renderer 是 CefRenderProcessHandler 的派生类,后续其它类就不过多说明。
void Renderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
// 弹出显示 ExecuteJavaScript works! 的消息框
frame->ExecuteJavaScript("alert('ExecuteJavaScript works!');", frame->GetURL(), 0);
}
- 浏览进程接收处理IPC发送过来的数据
bool PageHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
{
CEF_REQUIRE_UI_THREAD();
CefString strJS = "window.obj.age = 888; console.log(\"window.obj.age: \" + window.obj.age)";
frame->ExecuteJavaScript(strJS, frame->GetURL(), 0);
return true;
}
ExecuteJavaScript()函数可以用来与函数和变量交互框架的JS上下文,若需要C++返回结果到JS应用,应该使用窗口绑定或扩展。
二、扩展JavaScript
扩展JS在某种方面上即执行多条JS语句,比执行JS多个能执行C++、回调的部分。扩展JS和窗体绑定类似,除了在每个框架的上下文中加载和加载后不能修改,当扩展加载后DOM不存在和在扩展加载期间试图访问DOM将会导致崩溃。扩展使用 cef_v8.h 内 CefRegisterExtension() 函数注册,这个函数只能在渲染进程主线程中调用,需要注意的是,CefRegisterExtension第一个参数不能与其他的CefRegisterExtension重复,不然,后注册的不起作用。
1. 扩展JavaScript语句(C++调用JS)
涉及:CefRenderProcessHandler::OnWebKitInitialized().
void Renderer::OnWebKitInitialized()
{
std::string extensionCode =
"var example;"
"if (!example)"
" example = {};"
"if (!example.test)"
" example.test = {};"
"(function() {"
" var myint = 0;"
" example.test.increment = function() {"
" myint += 1;"
" return myint;"
" };"
"})();";
CefRegisterExtension("v8/example", extensionCode, nullptr);
}
<script language="JavaScript">
console.log();
console.log(example.test.increment());
</script>
2. 扩展注册函数(JS调C++)
通过扩展JS方式,注册一个函数给JS调用C++代码。涉及:CefRenderProcessHandler::OnWebKitInitialized()、CefV8Handler::Execute().
- 扩展注册JS函数
void Renderer::OnWebKitInitialized()
{
std::string extensionCode =
"var example;"
"if (!example)"
" example = {};"
"if (!example.test)"
" example.test = {};"
"(function() {"
" example.test.myfunction = function(param) {"
" native function MyFunction();" // 注册
" return MyFunction(param);" // 调用 MyFunction
" };"
"})();";
CefRefPtr<V8Handler> handler = new V8Handler(); // 一定要new的,已定义的不行
CefRegisterExtension("v8/extern", extensionCode, handler);// 绑定 V8 function calls(函数处理器)
}
- V8函数处理器对 MyFunction 的处理
当JS调用 example.test.myfunction 从而触发 MyFunction(param) 时根据 native function MyFunction() 和因为绑定 handler,会跳转到V8Handler::Execute()内。
bool V8Handler::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
if (name == "MyFunction" && arguments.size() == 1)
{
auto msgValue = arguments[0]->GetStringValue();
// MyFunction函数 的返回值
retval = CefV8Value::CreateString("MyFunction:" + msgValue.ToString());
return true;
}
return false;
// 崩溃:
// frame->ExecuteJavaScript(); // 不能在这里进行CefFrame相关的工作
// ->SendProcessMessage(PID_RENDERER, msg) // 只能渲染进程内发向浏览进程,浏览进程内发向渲染进程
}
- 现在JS调用
<script language="JavaScript">
console.log(example.test.myfunction("888"));
</script>
就会在控制台打印出 MyFunction:888 了。还可延展成C++与JS互调,和窗体绑定内的注册全局函数保存回调原理类似,即把回调函数作为参数传递到渲染进程保存。
三、窗口绑定
窗口绑定允许客户端应用程序把值附上一个框架窗口对象,将函数或对象绑定到CefFrame相应的window对象上。 通过CefV8Value::SetValue()实现窗体绑定全局对象、函数,涉及V8的都是在渲染进程编写运作。
1. 绑定全局对象(C++调用JS)
涉及:CefRenderProcessHandler::OnContextCreated()
void Renderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
CefRefPtr<CefV8Value> globalObject = context->GetGlobal();
CefRefPtr<CefV8Value> obj2 = CefV8Value::CreateObject(nullptr, nullptr);
obj2->SetValue("jscode", CefV8Value::CreateString("JScode"), V8_PROPERTY_ATTRIBUTE_NONE);
CefRefPtr<CefV8Value> obj = CefV8Value::CreateObject(nullptr, nullptr);
obj->SetValue("name", CefV8Value::CreateString("My String!"), V8_PROPERTY_ATTRIBUTE_NONE);
obj->SetValue("obj2", obj2, V8_PROPERTY_ATTRIBUTE_NONE);
globalObject->SetValue("obj", obj, V8_PROPERTY_ATTRIBUTE_NONE);
}
为JS绑定全局对象obj,类似于:
<script language="JavaScript">
var obj = {
name : "My String!",
obj2 : {
jscode : "JScode"
}
}
</script>
现在JS即可通过window.obj访问绑定的对象了。
2. 监控绑定对象(JS调用C++)
通过绑定 V8 accessor(存取器)监控对对象的存取操作,涉及: CefRenderProcessHandler::OnContextCreated()、CefV8Accessor::Get()、CefV8Accessor::Set.
void Renderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
CefRefPtr<CefV8Value> globalObject = context->GetGlobal();
myV8accessor = new MyV8Accessor();
CefRefPtr<CefV8Value> obj = CefV8Value::CreateObject(myV8accessor, nullptr);
obj->SetValue("value", V8_ACCESS_CONTROL_DEFAULT, V8_PROPERTY_ATTRIBUTE_NONE);
globalObject->SetValue("obj", obj, V8_PROPERTY_ATTRIBUTE_NONE);
}
为新建的V8对象obj绑定V8 accessor,并为对象obj添加变量value,再把obj绑定进JS的window对象上。然后我们需要实现绑定的V8存取器myV8accessor的监控存取功能:
- MyV8Accessor类
// MyV8Accessor.h
#pragma once
#include "include/cef_v8.h"
class MyV8Accessor : public CefV8Accessor
{
public:
MyV8Accessor() { count = 0; }
virtual bool Get(const CefString& name, const CefRefPtr<CefV8Value> object, CefRefPtr<CefV8Value>& retval, CefString& exception) override;
virtual bool Set(const CefString& name, const CefRefPtr<CefV8Value> object, const CefRefPtr<CefV8Value> value, CefString& exception) override;
private:
IMPLEMENT_REFCOUNTING(MyV8Accessor);
CefString obj_val = "myVal";
CefString oldVal = "myVal";
int count;
};
// MyV8Accessor.cpp
#include "MyV8Accessor.h"
bool MyV8Accessor::Get(const CefString& name, const CefRefPtr<CefV8Value> object, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
if (name == "value")
{
retval = CefV8Value::CreateString(obj_val.ToString() + "--" + std::to_string(++count));
return true;
}
return false;
}
bool MyV8Accessor::Set(const CefString& name, const CefRefPtr<CefV8Value> object, const CefRefPtr<CefV8Value> value, CefString& exception)
{
if (name == "value")
{
if (value->IsString())
{
obj_val = value->GetStringValue().ToString() + "--old:" + oldVal.ToString();
oldVal = value->GetStringValue();
}
else
exception = "TypeError~";
return true;
}
return false;
}
现在JS即可通过对window.obj.value的存取调用到MyV8Accessor::Get、MyV8Accessor::Set函数并返回相应的值:
<script language="JavaScript">
window.obj.value; //对应MyV8Accessor::Get,显示oldValue和计数功能
window.obj.value = "666"; //对应MyV8Accessor::Set,会保存oldValue
window.obj.value = 666; //类型出错,报异常
</script>
3. 绑定全局函数(JS调用C++)
编写和绑定全局对象类似,新建V8对象改为新建V8函数,并设置V8 function calls(函数处理器),最后窗体绑定。涉及:CefRenderProcessHandler::OnContextCreated()、CefV8Handler::Execute()、
- 为 NativeEventCall 设置V8函数处理,并绑定到窗口
void Renderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
CefRefPtr<CefV8Value> globalObject = context->GetGlobal();
v8Handler = new V8Handler();
CefRefPtr < CefV8Value> nativeeventcall = CefV8Value::CreateFunction("NativeEventCall", v8Handler);
globalObject->SetValue("NativeEventCall", nativeeventcall, V8_PROPERTY_ATTRIBUTE_READONLY);
}
- V8函数处理器对 NativeEventCall 的处理
bool V8Handler::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
if (name == "NativeEventCall")
{
std::string strVal = "NativeEventCall:";
for (int i = 0; i < arguments.size(); i++)
{
if(!arguments[i]->IsString())
{
exception = "TypeError~";
return true; // 需要 return true 才能 Throw an exception.
}
strVal += arguments[i]->GetStringValue().ToString();
if (i == arguments.size() - 1)
break;
strVal += "--";
}
retval = CefV8Value::CreateString(strVal);
}
return false;
}
- 现在JS调用
<script language="JavaScript">
console.log(window.NativeEventCall("123", "888"));
</script>
窗口控制台就会打印出 NativeEventCall:123--888 了,和扩展JS函数类似,根据情况选择使用即可。窗口绑定在JS与C++交互中的作用,主要体现在绑定全局函数保存回调内。
4. 绑定全局函数保存回调(C++与JS互调)
绑定全局函数保存回调在CEF官方Demo文档GeneralUsage中介绍的Asynchronous JavaScript Bindings部分,是CEF框架应用最广泛最多的一种标准C++与JS互相交互的方式,这里详解采用即时保存即使调用,先简单介绍流程。涉及:CefRenderProcessHandler::OnContextCreated()、CefV8Handler::Execute()、CefClient::OnProcessMessageReceived()、CefRenderProcessHandler::OnProcessMessageReceived()、CefRenderProcessHandler::OnContextReleased()
- 为 NativeEventCall 设置V8函数处理,并绑定到窗口
void Renderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
CefRefPtr<CefV8Value> globalObject = context->GetGlobal();
v8Handler = new V8Handler();
CefRefPtr < CefV8Value> nativeeventcall = CefV8Value::CreateFunction("NativeEventCall", v8Handler);
globalObject->SetValue("NativeEventCall", nativeeventcall, V8_PROPERTY_ATTRIBUTE_READONLY);
}
- V8函数处理器添加回调函数保存容器
// V8Handler.h
#pragma once
#include "include/cef_v8.h"
class V8Handler : public CefV8Handler
{
public:
V8Handler() = default;
virtual bool Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception) override;
CefRefPtr<CefV8Value> callBack; // 保存回调函数容器;JavaScript 与 C++交互都可用这个类型保存
private:
IMPLEMENT_REFCOUNTING(V8Handler);
};
- V8函数处理器对 NativeEventCall 的处理
bool V8Handler::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
if (name == "NativeEventCall" && arguments.size() == 2)
{
if ( !(arguments[0]->IsString() && arguments[1]->IsFunction()) )
{
exception = "TypeError~";
return true;
}
callBack = arguments[1]; // 保存了回调方法的引用
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("NativeEventCall");
msg->GetArgumentList()->SetString(0, arguments[0]->GetStringValue());
CefV8Context::GetCurrentContext().get()->GetFrame()->SendProcessMessage(PID_BROWSER, msg);
return true;
}
return false;
}
- 通过 SendProcessMessage 把 处理的V8函数消息发送给浏览进程处理
bool PageHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
{
CEF_REQUIRE_UI_THREAD();
std::string msgName = message->GetName();
if (msgName == "NativeEventCall")
{
std::string msgValue = message->GetArgumentList()->GetString(0);
msgValue = msgName + ":" + msgValue + ",browser a tour";
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create(msgName);
msg->GetArgumentList()->SetString(0, msgValue);
frame->SendProcessMessage(PID_RENDERER, msg);
return true;
}
return false;
}
- 把在浏览进程处理后的信息发送给渲染进程进一步处理
bool Renderer::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
{
std::string msgName = message->GetName();
if (msgName == "NativeEventCall")
{
// 准备回调函数的参数
CefString result = message->GetArgumentList()->GetString(0).ToString() + ",render a tour";
CefRefPtr<CefV8Value> resultV8 = CefV8Value::CreateString(result);
CefV8ValueList argsForJs;
argsForJs.push_back(resultV8); // 回调函数几个参数就push几个
CefRefPtr<CefV8Context> context = frame->GetV8Context(); // 获取到 JavaScript 的执行上下文
context->Enter(); // 进入 JavaScript 的执行上下文环境
v8Handler->callBack->ExecuteFunction(nullptr, argsForJs); // 之前已保存了 NativeEventCall 的回调函数参数
context->Exit(); // 退出 JavaScript 的执行上下文环境
return true;
}
return false;
}
- V8上下文释放之前先释放资源
void Renderer::OnContextReleased(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
if (v8Handler != NULL)
{
v8Handler->Release(); // 释放JavaScript注册的回调函数以及其他V8资源
}
}
- 现在JS调用
<script language="JavaScript">
window.NativeEventCall("giegie", (param) => {
console.log(param);
});
</script>
窗口控制台即会打印:NativeEventCall:giegie,browser a tour,render a tour。JS调用 NativeEventCall 会跳到V8Handler::Execute 保存好回调,再通过 SendProcessMessage IPC通信传到 PageHandler::OnProcessMessageReceived 浏览进程处理完后也通过IPC传到 Renderer::OnProcessMessageReceived 渲染进程做进一步处理,处理好后通过保存的回调函数执行JS。
四、观察者模式
自定义的一种观察者模式,是窗口绑定的进一步拓展,当被订阅的后台数据源发生变动时,观察者即通知订阅者最新的数据,好处是把需要JS开刷新数据的定时器搬到了C++内从而提高页面流畅,并能在一定程度上降低I/O传输的负荷,整体上设计成前端只能对数据读,不能添加修改删除数据,保证了一定的数据安全性,不过也有暴露接口,如果真要设计也是可以。对于时时数据变动的数据源不适用,对于需要实时监控,而数据不会时时变动的数据源适用。
- 为窗口绑定观察者模式的全局函数
void Renderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
messageV8 = new MessageV8();
CefRefPtr < CefV8Value> registeventcall = CefV8Value::CreateFunction("RegistEventCall", messageV8);
globalObject->SetValue(REGISTEVENTCALL, registeventcall, V8_PROPERTY_ATTRIBUTE_READONLY);
}
- messageV8对 RegistEventCall 的处理
// MessageV8.h
#pragma once
#include "include/cef_v8.h"
#include "MessageRouter.h" // 保存回调函数容器单例类
#include "EventDefinition.h" // 通用函数方法集合
class MessageV8 : public CefV8Handler
{
public:
MessageV8() = default;
~MessageV8();
virtual bool Execute(const CefString & name, CefRefPtr<CefV8Value> object, const CefV8ValueList & arguments, CefRefPtr<CefV8Value>&retval, CefString & exception) override;
private:
IMPLEMENT_REFCOUNTING(MessageV8);
};
通过区分RegistEventCall函数的第1个参数划分订阅与取消订阅的功能。订阅的主要工作是把RegistEventCall的第2个参数数据名、第3个参数回调函数保存下来,并向浏览进程发送相关订阅消息。取消订阅的主要工作是把数据名key从保存回调容器内删除,并向浏览进程发送相关取消订阅消息。整体传输流程以第2个参数数据名为唯一标识贯穿渲染进程、浏览进程(可以用宏定义规范)。
// MessageV8.cpp
#include "MessageV8.h"
MessageV8::~MessageV8()
{
MessageRouter::GetInstance()->Release();
}
bool MessageV8::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
CallbackuMap* uMap = MessageRouter::GetInstance()->GetUnorderMap(); // 单例回调函数容器
CefRefPtr<CefV8Context> conText = CefV8Context::GetCurrentContext(); // 回调相应的上下文环境
auto msgName = arguments[0]->GetStringValue();
std::vector<std::string> arr = split_V8(msgName, '_'); // 字符串分割函数,native_******
if ("native" == arr[0])
{
if ("registe" == arr[1]) // 订阅
{
if ( !(arguments.size() == 3 && arguments[1]->IsString() && arguments[2]->IsFunction()) ) // 规范参数
{
exception = "ParamError~";
return true;
}
// 保存回调相关
MSGCALL msgCall;
msgCall.msgName = arguments[1]->GetStringValue().ToString();
msgCall.context = conText;
msgCall.callback = arguments[2];
(*uMap)[arguments[1]->GetStringValue()].push_back(msgCall);
CefRefPtr<CefProcessMessage> msgtype = CefProcessMessage::Create("RegistEventCall");
CefRefPtr<CefListValue> msgbody = msgtype->GetArgumentList();
msgbody->SetString(0, arr[1]);
msgbody->SetString(1, arguments[1]->GetStringValue());
conText.get()->GetFrame()->SendProcessMessage(PID_BROWSER, msgtype);
}
else if ("cancel" == arr[1]) // 取消订阅
{
if ( !(arguments.size() == 2 && arguments[1]->IsString()) ) // 规范参数
{
exception = "ParamError~";
return true;
}
uMap->erase(arguments[1]->GetStringValue().ToString());
CefRefPtr<CefProcessMessage> msgtype = CefProcessMessage::Create("RegistEventCall");
CefRefPtr<CefListValue> msgbody = msgtype->GetArgumentList();
msgbody->SetString(0, arr[1]);
msgbody->SetString(1, arguments[1]->GetStringValue());
conText.get()->GetFrame()->SendProcessMessage(PID_BROWSER, msgtype);
}
return true;
}
return false;
}
- 保存回调函数容器单例类
创建单例类设计模式即可在渲染进程内各个部分调用,灰常方便,单例类中主要功能部分为变量callback_umap和函数Dispatch,变量callback_umap充当保存容器,而Dispatch函数充当响应器,响应浏览进程发来的数据更新请求,调用callback_umap内保存的相应回调函数从而传输给JS更新后的数据。
// MessageRouter.h
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include "include/cef_app.h"
#include "include/cef_v8.h"
#include <list>
typedef struct
{
std::string msgName;
CefRefPtr<CefV8Context> context;
CefRefPtr<CefV8Value> callback;
}MSGCALL; // 可自定义数据结构体,有context、callback即可
// 键:订阅对象;值:list结构体
typedef std::unordered_map<std::string, std::list<MSGCALL>> CallbackuMap;
class MessageRouter
{
public:
MessageRouter() = default;
MessageRouter(const MessageRouter&) = delete;
MessageRouter& operator=(const MessageRouter&) = delete;
// 创建-使用数据库单例 lazy_load
static MessageRouter* GetInstance()
{
static std::once_flag s_flag;
std::call_once(s_flag, [&]()
{
messageRouter = new MessageRouter();
});
return messageRouter;
}
// 释放单例
static void Release()
{
if (messageRouter != NULL)
{
messageRouter->callback_umap.clear();
delete messageRouter;
messageRouter = NULL;
}
}
// 获取umap
CallbackuMap* GetUnorderMap() { return &callback_umap; }
// 发射-消息池
bool Dispatch(std::string, CefRefPtr<CefValue>);
private:
CallbackuMap callback_umap;
static MessageRouter* messageRouter;
};
// MessageRouter.cpp
#include "MessageRouter.h"
// 初始化静态成员
MessageRouter* MessageRouter::messageRouter = NULL;
bool MessageRouter::Dispatch(std::string _msgstr, CefRefPtr<CefValue> _value)
{
auto itor = callback_umap.find(_msgstr);
if (itor != callback_umap.end())
{
CefV8ValueList arguments;
CefRefPtr<CefV8Value> msgstr;
CefRefPtr<CefV8Value> value;
// 判断传输过来的数据的数据类型生成相应的V8类型
if (VTYPE_STRING == _value->GetType())
value = CefV8Value::CreateString(_value->GetString());
else if (VTYPE_DOUBLE == _value->GetType())
value = CefV8Value::CreateDouble(_value->GetDouble());
else if (VTYPE_BOOL == _value->GetType())
value = CefV8Value::CreateBool(_value->GetBool());
else
return false;
for (MSGCALL msgcall : itor->second)
{
msgstr = CefV8Value::CreateString(msgcall.msgName);
arguments.push_back(msgstr);
arguments.push_back(value);
msgcall.context->Enter();
CefRefPtr<CefV8Value> retval = msgcall.callback->ExecuteFunction(nullptr, arguments);
if (retval.get())
{
if (retval->IsBool())
bool handled = retval->GetBoolValue();
}
msgcall.context->Exit();
}
return true;
}
return false;
}
- 把处理的RegistEventCall消息发送给浏览进程
开启CommunicateBase数据中台单例类的定时器,通过区分发送过来的msgType划分订阅和取消订阅,调用数据中台单例类的接口实现功能。
bool PageHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
{
CEF_REQUIRE_UI_THREAD(); // 这个宏保证执行此方法的是浏览器进程的主线程
std::string messageType = message->GetName();
if("RegistEventCall" == messageType)
{
if (NULL == CommunicateBase::GetInstance()->timer)
{
CommunicateBase::GetInstance()->timer = new CTimer();
// timer每55ms调用PollingDataPub,并传入一个frame参数
CommunicateBase::GetInstance()->timer->Start(55, std::bind(&CommunicateBase::PollingDataPub, CommunicateBase::GetInstance(), frame));
}
std::string msgType = message->GetArgumentList()->GetString(0);
std::string msg = message->GetArgumentList()->GetString(1);
if (msgType == "registe") // 订阅
CommunicateBase::GetInstance()->UpdateObr(msg, true);
else if(msgType == "cancel")// 取消订阅
CommunicateBase::GetInstance()->UpdateObr(msg, false);
return true;
}
return false;
}
- 数据中台单例类
通过std::unordered_map<std::string, ISSUEDATA> data集合数据;再通过EmplaceData、EraseData、UpdateData、FindData、UpdateObr,增、删、改、查、订阅处理数据;发放数据则由PollingDataPub、CTimer* timer负责;应该还有一个功能是绑定生产数据,这里只做模拟就只用_InitData初始化数据,有需要就扩展读写接口,及定时轮询数据是否改变配合接口UpdateData实现监察。
// CommunicateBase.h 数据中台:集合数据,处理数据,发放数据,(绑定数据源)
#pragma once
#include <memory>
#include <mutex>
#include <iostream>
#include <any>
#include <unordered_map>
#include <include/cef_app.h>
#include "EventDefinition.h" // 通用函数方法集合
#include "CTimer.h" // 定时器类
#include "include/base/cef_callback.h"
#include "include/cef_task.h"
#include "include/wrapper/cef_closure_task.h"
struct ISSUEDATA
{
ISSUEDATA() : isChange(true), isObserver(false) {}
ISSUEDATA operator () (std::any _value)
{
this->value = _value;
return *this;
}
std::any value; // 可任意类型的value
bool isChange; // 是否有变
bool isObserver; // 是否被订阅
};
typedef std::unordered_map<std::string, ISSUEDATA> iter;
class CommunicateBase
{
public:
CommunicateBase() = default;
CommunicateBase(const CommunicateBase&) = default;
CommunicateBase& operator=(const CommunicateBase&) = default;
// 创建-使用数据库单例 lazy_load
static CommunicateBase* GetInstance()
{
static std::once_flag s_flag;
std::call_once(s_flag, [&]()
{
communicatebase = new CommunicateBase();
communicatebase->_InitData();
});
return communicatebase;
}
// 释放资源
static void Release()
{
if (communicatebase != NULL)
{
communicatebase->GetUnorderMap()->clear();
delete timer;
timer = NULL;
delete communicatebase;
communicatebase = NULL;
}
}
// 获取data/timer
std::unordered_map<std::string, ISSUEDATA>* GetUnorderMap() { return &data; }
static CTimer* GetTimer() { return timer; }
// 增、删、改、查
bool EmplaceData(std::string, std::any); // true成功,false失败
int EraseData(std::string); // 1成功,0失败
bool UpdateData(std::string, std::any, bool = false); // true成功,false失败
bool FindData(std::string, std::any&, bool = false); // true成功,false失败
bool UpdateObr(std::string, bool); // 订阅和取消订阅
void PollingDataPub(CefRefPtr<CefFrame>); // 轮询订阅数据是否发送改变
static CTimer* timer;
private:
// 初始化数据
void _InitData();
// 处理发生改变的订阅数据
bool _PollingDataSwitch(CefRefPtr<CefFrame>, std::string, std::any);
static CommunicateBase* communicatebase;
std::unordered_map<std::string, ISSUEDATA> data;
};
#include "CommunicateBase.h"
CommunicateBase* CommunicateBase::communicatebase = NULL;
CTimer* CommunicateBase::timer = NULL;
bool CommunicateBase::EmplaceData(std::string _name, std::any _value)
{
std::pair<iter::iterator, bool> ret;
ISSUEDATA issue;
ret = data.emplace(std::make_pair(_name, issue(_value)));
return ret.second;
}
int CommunicateBase::EraseData(std::string _name)
{
int ret = data.erase(_name);
return ret;
}
bool CommunicateBase::UpdateData(std::string _name, std::any _value, bool _change)
{
if (data.end() == data.find(_name))
return false;
data[_name].value = _value;
if (_change)
data[_name].isChange = true;
return true;
}
bool CommunicateBase::FindData(std::string _name, std::any& _value, bool _change)
{
if (data.end() == data.find(_name))
return false;
_value = data[_name].value;
if (_change)
data[_name].isChange = true;
return true;
}
bool CommunicateBase::UpdateObr(std::string _name, bool _observer)
{
if (data.end() == data.find(_name))
return false;
data[_name].isObserver = _observer;
return true;
}
void CommunicateBase::PollingDataPub(CefRefPtr<CefFrame> _frame)
{
iter* data = CommunicateBase::GetInstance()->GetUnorderMap();
for (auto& [k, v] : (*data))
{
if (!v.isChange || !v.isObserver)
continue;
v.isChange = false;
_PollingDataSwitch(_frame, k, v.value);
}
}
void CommunicateBase::_InitData()
{
ISSUEDATA issue;
data.emplace(std::make_pair(std::string("isString"), issue(std::string("giegie"))));
data.emplace(std::make_pair(std::string("isNum"), issue(double(0.0))));
data.emplace(std::make_pair(std::string("isBool"), issue(bool(true))));
}
bool CommunicateBase::_PollingDataSwitch(CefRefPtr<CefFrame> _frame, std::string _key, std::any _value)
{
/*
* 子线程一直调用该函数
* 查询被订阅数据是否更新
* 如果更新则判断更新的类型
* 向 Renderer 进程发送更新后的数据
*/
switch (hash_str_to_uint32(_key.c_str())) // hash_str_to_uint32、 _hash 是哈希转换功能,加快switch字符串匹配速度
{
case "isString"_hash:
{
std::string value = std::any_cast<std::string>(_value);
/*
* 发送给 Renderer 消息事件参数为:
* 消息名: PUB, 参数: ["isString", std::any_cast<std::string>]
*/
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("PUB");
CefRefPtr<CefListValue> msgArgs = msg->GetArgumentList();
msgArgs->SetString(0, _key);
msgArgs->SetString(1, value);
_frame->SendProcessMessage(PID_RENDERER, msg);
}break;
case "isBool"_hash:
{
bool value = std::any_cast<bool>(_value);
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("PUB");
CefRefPtr<CefListValue> msgArgs = msg->GetArgumentList();
msgArgs->SetString(0, _key);
msgArgs->SetBool(1, value);
_frame->SendProcessMessage(PID_RENDERER, msg);
}break;
case "isNum"_hash:
{
double value = std::any_cast<double>(_value);
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("PUB");
CefRefPtr<CefListValue> msgArgs = msg->GetArgumentList();
msgArgs->SetString(0, _key);
msgArgs->SetDouble(1, value);
_frame->SendProcessMessage(PID_RENDERER, msg);
}break;
default:
break;
}
return true;
}
- 在_PollingDataSwitch内把信息发送给渲染进程
在_PollingDataSwitch内把已订阅更新对象的key和区分好数据类型的value打包命名为PUB发送给渲染进程接收并调用Dispatch发射消息池函数做相应的回调函数处理。
bool Renderer::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
{
std::string msg = message->GetName();
if (msg == "PUB")
{
msg = message->GetArgumentList()->GetString(0);
MessageRouter::GetInstance()->Dispatch(msg, message->GetArgumentList()->GetValue(1));
return true;
}
return false;
}
- 释放资源
void Renderer::OnContextReleased(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
if (messageV8 != NULL)
{
messageV8->Release();
}
}
- 响应方法及效果
现在整体的流通桥梁已经搭建完毕,我们就可以来实验效果,只要在浏览进程内使用改值更新或者查找更新就会使JS相应的回调函数调用一次,而更新时机就需要系统功能需求自定义了。
// CefClient 浏览进程
{
// 改值更新
CommunicateBase::GetInstance()->UpdateData("isString", std::string("working"), true);
CommunicateBase::GetInstance()->UpdateData("isNum", double(11.11), true);
CommunicateBase::GetInstance()->UpdateData("isBool", bool(false), true);
// 查找更新
std::any value;
CommunicateBase::GetInstance()->FindData("isString", value, true);
CommunicateBase::GetInstance()->FindData("isNum", value, true);
CommunicateBase::GetInstance()->FindData("isBool", value, true);
}
// 订阅
<script language="JavaScript">
window.RegistEventCall("native_registe", "isString", (msgName, value) => {
console.log(msgName + "1:" + value)
});
window.RegistEventCall("native_registe", "isString", (msgName, value) => {
console.log(msgName + "2:" + value)
});
window.RegistEventCall("native_registe", "isNum", (msgName, value) => {
console.log(msgName + ":" + value)
});
window.RegistEventCall("native_registe", "isBool", (msgName, value) => {
console.log(msgName + ":" + value)
});
</script>
// 取消订阅
<script language="JavaScript">
window.RegistEventCall("native_cancel", "isString")
</script>
看得感觉还不错的话,点赞收藏关注三连一下吧,over~
推荐阅读:
https://www.cnblogs.com/sr0808/
资料文献:
CEF官方API文档:https://cef-builds.spotifycdn.com/docs/107.1/classes.html
CEF官方Demo文档:https://bitbucket.org/chromiumembedded/cef-project/src/master/
CEF论坛-提问题:https://www.magpcss.org/ceforum/
相关资料:
https://www.lmlphp.com/user/101394/article/item/1568315/
https://www.cnblogs.com/guolixiucai/p/4943748.html
https://blog.csdn.net/zhuhongshu/article/details/70159672
https://blog.csdn.net/qq_29067097/article/details/109777202