CEF C++与JS 交互 全部

发布时间 2023-10-18 15:17:49作者: 空手只套小鱼干

在这里插入图片描述

摘要

  由于工作需要,公司开辟一个新项目需要用到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 frame指针,在渲染进程和浏览器进程中都可以使用,并且能在JS上下文之外使用。方法使用方式与JS的 eval方法一样,异步执行,无返回值。涉及(渲染进程) CefRenderProcessHandler::OnContextCreated()、(浏览进程) CefClient::OnProcessMessageReceived().

  • 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