此文已由作者邓佳佳授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验

本文主要介绍 CEF 场景中 C++ 和 JavaScript 交互(以下简称 JS Bridge)中的一些重要节点,包括了 C++/JavaScript 的方法注册、方法调用、回调管理。以下是一些重要的参考资料:

现有实现的不足

在制作新的 JS Bridge 之前,团队中已经有将 Cef 整合到项目中的离屏渲染实现,但 C++ 与 JavaScript 交互的代码相对单一,仅实现了一些简单的方法,没有拓展性和统一性。也没有处理一些多 Render 和多 Browser 实例的情况。比如我希望调用一个 C++ 的方法,需要重新在 Render 和 Browser 进程中实现单独的通信代码,这样是非常麻烦的而且容易出错。

期望的样子

因为未来有跨平台的打算,所以侧重点还是往偏前端一些,希望所有界面展示的功能均交由前端来实现。所以首先前端可以很方便的提供接口让 C++ 调用,并且可以很方便的调用一个 C++ 接口并得到适当的回调返回信息。同理 C++ 端也希望能很容易的调用前端的方法或注册方法提供前端调用。它们之间传递数据使用通用的 JSON 格式,在 C++ 端总是以字符串方式解析,而在前端总是以一个 Object 的方式解析。因为 JSON 的拓展性极高,当做桥梁之间传递数据的通道最合适不过了。

前端调用 C++ 方法的流程

Render 进程的 OnWebKitInitialized 接口在 WebKit 初始化完成后被调用,此时我们可以通过 CefRegisterExtension 来注册一个拓展让 WebKit 初始化完成后就执行这部分代码,而这部分代码就完全靠你发挥了,你可以声明一个全局的对象,给该对象实现两个方法来提供前端页面注册方法和调用 Native 的方法,如下所示:

void ClientApp::OnWebKitInitialized() 
{
    /**
     * JavaScript 扩展代码,这里定义一个 NimCefWebFunction 对象提供 call 和 register 方法来让 Web 端触发 CefV8Handler 处理代码
     * param[in] functionName 要调用的 C++ 方法名称
     * param[in] params 调用该方法传递的参数,在前端指定的是一个 Object,但转到 Native 的时候转为了字符串
     * param[in] callback 执行该方法后的回调函数
     * 前端调用示例
     * NimCefWebHelper.call('showMessage', { message: 'Hello C++' }, (arguments) => {
     *    console.log(arguments)
     * })
     */
    std::string extensionCode = R"(
        var NimCefWebInstance = {};
        (() => {
            NimCefWebInstance.call = (functionName, arg1, arg2) => {
                if (typeof arg1 === 'function') {
                    native function call(functionName, arg1);
                    return call(functionName, arg1);
                } else {
                    const jsonString = JSON.stringify(arg1);
                    native function call(functionName, jsonString, arg2);
                    return call(functionName, jsonString, arg2);
                }
            };
            NimCefWebInstance.register = (functionName, callback) => {
                native function register(functionName, callback);
                return register(functionName, callback);
            };
        })();
    )";
    CefRefPtr<CefJSHandler> handler = new CefJSHandler();      CefRegisterExtension("v8/extern", extensionCode, handler);
}

代码中新增了一个 NimCefWebInstance 全局对象,并拓展了一个 call 方法和一个 register 方法,分别提供前端调用 C++ 方法和注册本地的方法让 C++ 调用。并且做了适当判断,允许传递参数和不传递参数。如果你更了解 JavaScript 可以进一步拓展。

另外可以看到我们新建了一个派生于 CefV8Handler 类的 CefJSHandler 类,该类仅实现了一个方法,就是用来接收我们刚才注册到页面中的方法事件的。实现如下:

bool CefJSHandler::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
    // 当Web中调用了"NimCefWebFunction"函数后,会触发到这里,然后把参数保存,转发到Broswer进程
    // Broswer进程的BrowserHandler类在OnProcessMessageReceived接口中处理kJsCallbackMessage消息,就可以收到这个消息     if (arguments.size() < 2)
    {
        exception = "Invalid arguments.";
        return false;
    }     CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
    CefRefPtr<CefFrame> frame = context->GetFrame();
    CefRefPtr<CefBrowser> browser = context->GetBrowser();     int64_t browser_id = browser->GetIdentifier();
    int64_t frame_id = frame->GetIdentifier();     if (name == "call")
    {
        // 允许没有参数列表的调用,第二个参数为回调
        // 如果传递了参数列表,那么回调是第三个参数
        CefString function_name = arguments[0]->GetStringValue();
        CefString params = "{}";
        CefRefPtr<CefV8Value> callback;
        if (arguments[0]->IsString() && arguments[1]->IsFunction())
        {
            callback = arguments[1];
        }
        else if (arguments[0]->IsString() && arguments[1]->IsString() && arguments[2]->IsFunction())
        {
            params = arguments[1]->GetStringValue();
            callback = arguments[2];
        }
        else
        {
            exception = "Invalid arguments.";
            return false;
        }         // 执行 C++ 方法
        if (!js_bridge_->CallCppFunction(function_name, params, callback))
        {
            exception = nbase::StringPrintf("Failed to call function %s.", function_name).c_str();
            return false;
        }         return true;
    }
    else if (name == "register")
    {
        if (arguments[0]->IsString() && arguments[1]->IsFunction())
        {
            std::string function_name = arguments[0]->GetStringValue();
            CefRefPtr<CefV8Value> callback = arguments[1];
            if (!js_bridge_->RegisterJSFunc(function_name, callback))
            {
                exception = "Failed to register function.";
                return false;
            }
            return true;
        }
        else
        {
            exception = "Invalid arguments.";
            return false;
        }
    }     return false;
}

这里我们区分了 call 和 register 方法,并且进一步判断了参数的传递顺序。当前端执行了 call 方法时就可以将执行的函数名、传递参数保存下来,然后通知 Browser 进程去执行这个方法(前提是 Browser 端已经注册过使用相同字符串命名的这个方法)。我将该操作传递给了一个 js_bridge 对象的 CallCppFunction 方法。这是我封装的一个用来管理两端注册的方法和回调的管理类,并将两端通讯的方法封装了起来,如下所示:

bool CefJSBridge::CallCppFunction(const CefString& function_name, const CefString& params, CefRefPtr<CefV8Value> callback)
{
    auto it = render_callback_.find(js_callback_id_);
    if (it == render_callback_.cend())
    {
        CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
        CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kCallCppFunctionMessage);         message->GetArgumentList()->SetString(0, function_name);
        message->GetArgumentList()->SetString(1, params);
        message->GetArgumentList()->SetInt(2, js_callback_id_);         render_callback_.emplace(js_callback_id_++, std::make_pair(context, callback));         // 发送消息到 browser 进程
        CefRefPtr<CefBrowser> browser = context->GetBrowser();
        browser->SendProcessMessage(PID_BROWSER, message);         return true;
    }     return false;
}

这里我们维护了一份 callback 的索引,每当发起新的调用时,这个索引值自增,并插入到我们管理回调的 map 结构中。map 中以 callback 索引为标准,存储了运行环境和真正的 callback 实体。最后使用 SendProcessMessage方法通知 Browser 来执行我们要运行的代码。当消息发出后,Browser 进程就会收到这个消息了。

bool BrowserHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
{
    // 处理render进程发来的消息
    std::string message_name = message->GetName();
    if (message_name == kFocusedNodeChangedMessage)
    {
        is_focus_oneditable_field_ = message->GetArgumentList()->GetBool(0);
        return true;
    }
    else if (message_name == kCallCppFunctionMessage)
    {
        CefString fun_name    = message->GetArgumentList()->GetString(0);
        CefString param        = message->GetArgumentList()->GetString(1);
        int js_callback_id    = message->GetArgumentList()->GetInt(2);         if (handle_delegate_)
            handle_delegate_->OnExecuteCppFunc(fun_name, param, js_callback_id, browser);         return true;
    }
    else if (message_name == kExecuteCppCallbackMessage)
    {
        CefString param = message->GetArgumentList()->GetString(0);
        int callback_id = message->GetArgumentList()->GetInt(1);         if (handle_delegate_)
            handle_delegate_->OnExecuteCppCallbackFunc(callback_id, param);
    }     return false;
}

Browser 进程接收到消息后,判断如果是 kCallCppFunctionMessage 消息类型那么就将要执行的函数名和参数传递给一个委托类去做具体的执行。实际委托类的子类中实现了这些执行 C++ 方法的虚函数,在实现的虚函数中解析了参数和要调用的函数名,通过 js_bridge 对象来执行曾经注册过的方法。当 C++ 方法执行完以后,我们还要通知 Render 进程去执行回调函数,如下所示:

bool CefJSBridge::ExecuteCppFunc(const CefString& function_name, const CefString& params, int js_callback_id, CefRefPtr<CefBrowser> browser)
{
    CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kExecuteJsCallbackMessage);
    CefRefPtr<CefListValue> args = message->GetArgumentList();     auto it = browser_registered_function_.find(std::make_pair(function_name, browser->GetIdentifier()));
    if (it != browser_registered_function_.cend())
    {
        auto function = it->second;
        Post2UI([=]() {
            function(params, [=](bool has_error, const std::string& json_result) {
                // 测试代码,需要封装到管理器中
                args->SetInt(0, js_callback_id);
                args->SetBool(1, has_error);
                args->SetString(2, json_result);
                browser->SendProcessMessage(PID_RENDERER, message);
            });
        });
        return true;
    }
    else
    {
        args->SetInt(0, js_callback_id);
        args->SetBool(1, true);
        args->SetString(2, R"({"message":"Function does not exist."})");
        browser->SendProcessMessage(PID_RENDERER, message);
        return false;
    }
}

通过 SendProcessMessage 通知 Render 进程,我们要执行某个 Id 的 callback。当 Render 进程接收到这个消息后,会根据传递进来的 callback id 去 map 中寻找这个 callback 的运行环境和实体来执行 callback 并传入 Browser 进程携带过来的参数。

C++ 调用前端方法流程

还记得上面提到的全局方法中有个 register 方法吗?这个方法提供了前端注册持久化的方法提供 C++ 调用。注册的方法如下所示:

(() => {
    /*
     * 注册一个回调函数,用于在 C++ 应用中调用
     * param[in] showJsMessage 回调函数的名称,C++ 会使用该名称来调用此回调函数
     * param[in] callback 回调函数执行体
     */
    NimCefWebInstance.register('showJsMessage', (arguments) => {
        const receiveMessageInput = document.getElementById('receive_message_input')
        receiveMessageInput.value = arguments.message
        return {
            message: 'showJsMessage function was executed, this message return by JavaScript.'
        }
    })
})()

同样,在执行 register 方法注册一个持久化方法时会进入到上面提到的我们自己注册的 Handler::Execute 方法中。在一系列判断后开始将注册的函数放到 JS Bridge 维护的列表中,代码如下:

bool CefJSBridge::RegisterJSFunc(const CefString& function_name, CefRefPtr<CefV8Value> function, bool replace/* = false*/)
{
    CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
    CefRefPtr<CefFrame> frame = context->GetFrame();     if (replace)
    {
        render_registered_function_.emplace(std::make_pair(function_name, frame->GetIdentifier()), function);
        return true;
    }
    else
    {
        auto it = render_registered_function_.find(std::make_pair(function_name, frame->GetIdentifier()));
        if (it == render_registered_function_.cend())
        {
            render_registered_function_.emplace(std::make_pair(function_name, frame->GetIdentifier()), function);
            return true;
        }         return false;
    }     return false;
}

存放这些持久化函数时,我们根据函数名和当前注册函数所在的 frame id 为标准,为什么要加一个 frame id 呢?主要我们要考虑的是如果一个页面下存在多个 frame,不同的 frame 我们要允许他们注册同名的方法,在调用的时候去调用对应 frame 中的方法。另外一种情况就是如果你的 JS Bridge 是一个单例,它维护了所有 render 进程的所有 browser 实例的函数和回调列表,我们一样还是要用一个唯一的数据来区分某个 callback 要在哪个 frame 里执行。frame 是最小单位,并且在我实战情况下不同的 browser 下的 frame id 是不会重复的。所以用 frame id 做一个唯一标识是最靠谱的。 当 C++ 要调用前端已经注册好的方法时,只需要到这个列表中根据名字和 frame id 找到对应的 frame,通过 frame 得到运行上下文(context),然后进入这个上下文执行环境执行具体的函数体就可以啦。代码如下:

bool CefJSBridge::ExecuteJSFunc(const CefString& function_name, const CefString& json_params, CefRefPtr<CefFrame> frame, int cpp_callback_id)
{
    auto it = render_registered_function_.find(std::make_pair(function_name, frame->GetIdentifier()));
    if (it != render_registered_function_.cend())
    {         auto context = frame->GetV8Context();
        auto function = it->second;         if (context.get() && function.get())
        {
            context->Enter();             CefV8ValueList arguments;             // 将 C++ 传递过来的 JSON 转换成 Object
            CefV8ValueList json_parse_args;
            json_parse_args.push_back(CefV8Value::CreateString(json_params));
            CefRefPtr<CefV8Value> json_object = context->GetGlobal()->GetValue("JSON");
            CefRefPtr<CefV8Value> json_parse = json_object->GetValue("parse");
            CefRefPtr<CefV8Value> json_stringify = json_object->GetValue("stringify");
            CefRefPtr<CefV8Value> json_object_args = json_parse->ExecuteFunction(NULL, json_parse_args);
            arguments.push_back(json_object_args);             // 执行回调函数
            CefRefPtr<CefV8Value> retval = function->ExecuteFunction(NULL, arguments);
            if (retval.get() && retval->IsObject())
            {
                // 回复调用 JS 后的返回值
                CefV8ValueList json_stringify_args;
                json_stringify_args.push_back(retval);
                CefRefPtr<CefV8Value> json_string = json_stringify->ExecuteFunction(NULL, json_stringify_args);
                CefString str = json_string->GetStringValue();                 CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kExecuteCppCallbackMessage);
                CefRefPtr<CefListValue> args = message->GetArgumentList();
                args->SetString(0, json_string->GetStringValue());
                args->SetInt(1, cpp_callback_id);
                context->GetBrowser()->SendProcessMessage(PID_RENDERER, message);
            }             context->Exit();             return true;
        }         return false;
    }     return false;
}

这样前端应用就可以正常执行已经注册过的函数了。另外在上面的代码中,我们看到 ExecuteFunction 方法是又返回值的,这个返回值是前端 return 的数据。我们可以使用这个返回值再来通知 C++ 端执行的结果,我这里直接将执行结果通过进程间通信发送给了 C++ 端,虽然与前端调用 C++ 的回调实现不太一样,但是还是可以达到我们的需求的。

总结

上面分别介绍了两端互相注册和调用对端方法的示例,实际情况还是自己要根据项目需求设计一下,上面的实现思路还是有一些缺陷的。比如调用函数使用的是字符串名字,这样是挺不靠谱的做法,但从目前情况来看是最方便快捷的实现方式。但最终还是期望后期可以拓展成以对象方式直接调用对端方法,这可能要再对 Cef 做挖掘,根据自己实际项目情况再继续拓展了。

免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 Spring缓存穿透问题修复
【推荐】 用scrapy数据抓取实践
【推荐】 私有云爆发,未来增长空间巨大

在 Cef 中实现 C++ 与 JavaScript 交互场景分析的更多相关文章

  1. CEF与JavaScript交互读取电脑信息

    CefSharp中c#和JavaScript交互读取电脑信息 介绍 CEF是由Marshall Greenblatt于2008年创建的基于Google Chromium的BSD许可开源项目.与主要关注 ...

  2. 在android中实现webview与javascript之间的交互(转)

    参见“在android中实现webview与javascript之间的交互”

  3. 使用CefSharp在.Net程序中嵌入Chrome浏览器(五)——Javascript交互

    要在CEF中和网页的JS进行交互,首先我们要通过设置启用Javascrit集成功能. CefSharpSettings.LegacyJavascriptBindingEnabled = true; 调 ...

  4. 如何用c#本地代码实现与Webbrowser中的JavaScript交互

    关键词:.Net,Webbrowser,JavaScript,communication 参考: 链接:msdn实例-简单的相互调用 代码: [PermissionSet(SecurityAction ...

  5. Android混合开发之WebView与Javascript交互

    前言: 最近公司的App为了加快开发效率选择了一部分功能采用H5开发,从目前市面的大部分App来讲,大致分成Native App.Web App.Hybrid App三种方式,个人觉得目前以Hybri ...

  6. Android中webView和网页的交互

     Android中webView和网页的交互 Android中webView跟网页的交互式通过JavaScript进行的.具体步骤: 1.创建JavaScript,在点击的时候调用JavaScript ...

  7. 使用C#在CEF中拦截并响应请求

    一.前言 忙里偷闲,研究了一下如何在CEF中拦截请求,并作出响应.这个功能对某些需要修改服务器响应的需求来说必不可少,可以直接读取本地文件作为响应内容. C#的CEF封装项目有很多,我使用的是Chro ...

  8. 移动端基于HTML模板和JSON数据的JavaScript交互

    写本文之前,我正在做一个基于Tab页的订单中心: 每点击一个TAB标签,会请求对应状态的订单列表.之前的项目,我会在js里使用 +  连接符连接多个html内容: var html = ''; htm ...

  9. 重新想象 Windows 8.1 Store Apps (80) - 控件增强: WebView 之基本应用, POST 数据, 与 JavaScript 交互

    [源码下载] 重新想象 Windows 8.1 Store Apps (80) - 控件增强: WebView 之基本应用, POST 数据, 与 JavaScript 交互 作者:webabcd 介 ...

随机推荐

  1. django-admin:command not found的解决办法

    django-admin:command not found的解决办法 找到django-admin的路径 绝对路径  然后用命令行运行 python3 /usr/local/python3/lib/ ...

  2. Firemonkey Android 虚拟机

    AVD,Android Virtual Device start menu>Android AVD Manager>create>start. [DCC Error] E2597 D ...

  3. U3D SCENEMANAGER.LOADSCENE是半异步的

    作地图加载优化时用到了SceneManager.LoadScene,发现它执行后立即执行了后面的语句,查文档才知道 它是 semi-asyncronize的,即半异步的. 说它是半异步,是因为,它本身 ...

  4. svg make a face

    1.创建项目 #使用simple模板 vue init webpack-simple vue-svg #安装依赖 cd vue-svg/ npm i #安装d3 npm i d3 --save 2.代 ...

  5. 网页中给超链接添加"是否确认"的方法

    最近在做数据库, 需要给一个"删除"链接增加是否确认的弹出框, 在网上查到了两种方法: 1, 先看看最麻烦的一种 <html xmlns="http://www.w ...

  6. SpringMvc配置拦截器

    SpringMVC可以通过配置拦截器,进行url过滤等处理. 在spring-mvc.xml的配置文件中,如下示: 其中,在<mvc:interceptors>中可以配置多个拦截器< ...

  7. php性能优化学习笔记

    编写代码 1.尽可能多的使用内置函数2.比对内置函数的时间复杂度,选择复杂度低的 比如 循环20万次-测试isset 和 array_key_exists 耗时 对比isset.php , array ...

  8. 利率计算v4.0--测试--软件工程

    利率计算v4.0--测试 package Test; import Model.Interest; import Service.CompoundInterestService; import Ser ...

  9. Android登录模块原理及实现

    1.需要保持登录状态,在本地保存登录的状态信息. 2.界面布局 3.登录流程 输入信息=>登录成功=>手机号发送验证码=>输入验证码=>登录成功

  10. 是什么优化让 .NET Core 性能飙升?(转)

    欢迎大家持续关注葡萄城控件技术团队博客,更多更好的原创文章尽在这里~~ .NET Core(开放源代码,跨平台,x-copy可部署等)有许多令人兴奋的方面,其中最值得称赞的就是其性能了. 感谢所有社区 ...