CEF中如何进行C++代码(客户端)和JS代码(前端)互相调用?官方文档翻译&校对&注语

简介

Chromium和CEF在其内部JavaScript(JS)实现中使用V8 JavaScript引擎。浏览器中的每个帧(frame)都有其自己的JS上下文(context),为该帧中执行的JS代码提供作用域和安全性。(有关更多信息,请参见“使用上下文”部分)。CEF暴露了许多JS功能,可供客户端应用程序集成使用。

译者注:若不清楚什么是帧(frame)和JS上下文(context),以及两者的关系(译者作为客户端开发就不太了解),可以参考以下ChatGPT中的解释:
“在Web应用中,Frame是HTML页面中的一种标记,它可以在一个HTML页面中嵌套另一个HTML页面。Frame提供了一种在同一窗口或页面中显示多个独立HTML页面的方法,通过Frame标记可以将单个HTML页面分成多个区域,每个区域里面可以加载不同的HTML页面,称为框架。通过这种方式,用户就可以同时在一个页面中查看多个HTML页面的内容。
JS context是指JavaScript环境执行代码的上下文环境。它包含了当前JavaScript代码执行时的所有变量、函数和对象等,是代码执行时的运行环境。JS context 可以分为全局上下文和函数上下文。
Frame和JS context之间的关系在于,当使用Frame来嵌套另一个HTML页面时,每个Frame都有自己的独立的 JS context,它们之间相互隔离,互不影响。也就是说,在一个Frame中执行的JavaScript代码不能访问其他Frame中的JavaScript环境,例如变量、函数等。另外,全局 JS context 通常在第一个Frame/Page加载时初始化,而每个Frame也会有对应的 JS context ,用于在 Frame/Page 中执行 JavaScript 代码。因此,当我们需要在 Frame/Page 中运行 JavaScript 代码时,应该在对应的 JS context 中执行它们,以确保它们的正确性和可访问性。”

CEF3 Blink(WebKit)和JS程序在单独的渲染器进程中运行。渲染器进程中的主线程被标识为TID_RENDERER,所有V8执行都必须在该线程上进行。与JS执行相关的回调通过CefRenderProcessHandler接口公开。当初始化新的渲染器进程时,可以通过CefApp::GetRenderProcessHandler()检索此接口。

在浏览器和渲染进程之间通信的JS API应使用异步回调进行设计。有关更多信息,请参见GeneralUsage wiki页面的“Asynchronous JavaScript Bindings”部分。

ExecuteJavaScript

在浏览器和渲染器进程中执行JS的最简单方法是使用CefFrame::ExecuteJavaScript()函数。此函数在浏览器进程和渲染器进程中都可用,并且可以安全地从JS上下文之外使用。

CefRefPtr<CefBrowser> browser = ...;
CefRefPtr<CefFrame> frame = browser->GetMainFrame();
frame->ExecuteJavaScript("alert('ExecuteJavaScript works!');",
    frame->GetURL(), 0);

上面的示例将在浏览器的主框架中执行alert('ExecuteJavaScript works!')。

ExecuteJavaScript函数可用于与帧的JS上下文中的函数和变量进行交互。为了从JS返回值到客户端应用程序,请考虑使用Window Binding或Extensions。

窗口绑定

Window Binding(窗口绑定)允许客户端应用程序将值附加到一个框架的window对象中。Window Binding使用CefRenderProcessHandler::OnContextCreated()方法来实现。

void MyRenderProcessHandler::OnContextCreated(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefRefPtr<CefV8Context> context) {
  // Retrieve the context's window object.
  CefRefPtr<CefV8Value> object = context->GetGlobal();

  // Create a new V8 string value. See the "Basic JS Types" section below.
  CefRefPtr<CefV8Value> str = CefV8Value::CreateString("My Value!");

  // Add the string to the window object as "window.myval". See the "JS Objects" section below.
  object->SetValue("myval", str, V8_PROPERTY_ATTRIBUTE_NONE);
}

帧中的JavaScript可以与窗口绑定进行交互。

<script language="JavaScript">
alert(window.myval); // Shows an alert box with "My Value!"
</script>

每次重新加载帧时,Window Binding都会重新加载,这为客户端应用程序提供了修改绑定的机会。例如,不同的框架可以通过修改绑定到该框架的window对象上的值,来访问客户端应用程序的不同特性。

扩展

扩展(Extensions)类似于window绑定,不同的是它们被加载到每个帧的上下文中,一旦加载就无法修改。在扩展加载期间,DOM不存在,尝试访问DOM将导致崩溃。扩展使用CefRegisterExtension函数注册,应从CefRenderProcessHandler::OnWebKitInitialized方法中调用。

void MyRenderProcessHandler::OnWebKitInitialized() {
  // Define the extension contents.
  std::string extensionCode =
    "var test;"
    "if (!test)"
    "  test = {};"
    "(function() {"
    "  test.myval = 'My Value!';"
    "})();";

  // Register the extension.
  CefRegisterExtension("v8/test", extensionCode, NULL);
}

字符串extensionCode可以是任何有效的JS代码。框架中的JS代码可以与扩展代码交互。

<script language="JavaScript">
alert(test.myval); // Shows an alert box with "My Value!"
</script>

基本JavaScript类型

CEF支持创建基本的JS数据类型,包括undefined、null、bool、int、double、date和string。这些类型使用CefV8Value::Create*静态方法创建。例如,要创建新的JS字符串值,请使用CreateString方法。

CefRefPtr<CefV8Value> str = CefV8Value::CreateString("My Value!");

基本值类型可以随时创建,并且最初不与特定上下文相关联(有关更多信息,请参见“使用上下文”部分)。

要测试值类型,请使用Is*方法。

CefRefPtr<CefV8Value> val = ...;
if (val.IsString()) {
  // The value is a string.
}

要检索值,请使用Get*Value方法。

CefString strVal = val.GetStringValue();

JS数组

使用CefV8Value::CreateArray静态方法创建数组,该方法接受一个长度参数。仅可以从上下文中创建和使用数组(有关更多信息,请参见“使用上下文”部分)。

// Create an array that can contain two values.
CefRefPtr<CefV8Value> arr = CefV8Value::CreateArray(2);

要将值分配给数组,请使用SetValue方法变体,该变体接受索引作为第一个参数。

// Add two values to the array.
arr->SetValue(0, CefV8Value::CreateString("My First String!"));
arr->SetValue(1, CefV8Value::CreateString("My Second String!"));

要测试CefV8Value是否为数组,请使用IsArray方法。要获取数组的长度,请使用GetArrayLength方法。要从数组中获取值,请使用GetValue变体,该变体以索引为第一个参数。

JS对象

使用CefV8Value::CreateObject静态方法创建对象,该方法接受一个可选的CefV8Accessor参数。仅可以从上下文中创建和使用对象(有关更多信息,请参见“使用上下文”部分)。

CefRefPtr<CefV8Value> obj = CefV8Value::CreateObject(NULL);

要将值分配给对象,请使用SetValue方法变体,该变体接受键字符串作为第一个参数。

obj->SetValue("myval", CefV8Value::CreateString("My String!"));

带访问器的对象

对象可以选择具有关联的CefV8Accessor,该访问器提供获取和设置值的本地(C++)实现。

CefRefPtr<CefV8Accessor> accessor = …;
CefRefPtr<CefV8Value> obj = CefV8Value::CreateObject(accessor);

CefV8Accessor的实现类必须由客户端代码提供。

class MyV8Accessor : public CefV8Accessor {
public:
  MyV8Accessor() {}

  virtual bool Get(const CefString& name,
                   const CefRefPtr<CefV8Value> object,
                   CefRefPtr<CefV8Value>& retval,
                   CefString& exception) OVERRIDE {
    if (name == "myval") {
      // Return the value.
      retval = CefV8Value::CreateString(myval_);
      return true;
    }

    // Value does not exist.
    return false;
  }

  virtual bool Set(const CefString& name,
                   const CefRefPtr<CefV8Value> object,
                   const CefRefPtr<CefV8Value> value,
                   CefString& exception) OVERRIDE {
    if (name == "myval") {
      if (value->IsString()) {
        // Store the value.
        myval_ = value->GetStringValue();
      } else {
        // Throw an exception.
        exception = "Invalid value type";
      }
      return true;
    }

    // Value does not exist.
    return false;
  }

  // Variable used for storing the value.
  CefString myval_;

  // Provide the reference counting implementation for this class.
  IMPLEMENT_REFCOUNTING(MyV8Accessor);
};

在对象中传递的值必须使用接受AccessControl和PropertyAttribute参数的SetValue方法变体进行设置。

obj->SetValue("myval", V8_ACCESS_CONTROL_DEFAULT, 
    V8_PROPERTY_ATTRIBUTE_NONE);

JS函数

CEF支持使用本地实现创建JS函数。使用CefV8Value::CreateFunction静态方法创建函数,该方法接受函数名和CefV8Handler参数。仅可以从上下文中创建和使用函数(有关更多信息,请参见“使用上下文”部分)。

CefRefPtr<CefV8Handler> handler = …;
CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction("myfunc", handler);

CefV8Handler的实现类必须由客户端程序提供。

class MyV8Handler : public CefV8Handler {
public:
  MyV8Handler() {}

  virtual bool Execute(const CefString& name,
                       CefRefPtr<CefV8Value> object,
                       const CefV8ValueList& arguments,
                       CefRefPtr<CefV8Value>& retval,
                       CefString& exception) OVERRIDE {
    if (name == "myfunc") {
      // Return my string value.
      retval = CefV8Value::CreateString("My Value!");
      return true;
    }

    // Function does not exist.
    return false;
  }

  // Provide the reference counting implementation for this class.
  IMPLEMENT_REFCOUNTING(MyV8Handler);
};

译者注:这其实就是JS代码调用本机代码(C++)的方式。以上代码相当于为前端注册了一个叫myfunc的C++客户端函数,前端使用window.func就可以异步调用C++代码。

窗口绑定中使用函数

函数可以用于创建复杂的window绑定。

void MyRenderProcessHandler::OnContextCreated(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefRefPtr<CefV8Context> context) {
  // Retrieve the context's window object.
  CefRefPtr<CefV8Value> object = context->GetGlobal();

  // Create an instance of my CefV8Handler object.
  CefRefPtr<CefV8Handler> handler = new MyV8Handler();

  // Create the "myfunc" function.
  CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction("myfunc", handler);

  // Add the "myfunc" function to the "window" object.
  object->SetValue("myfunc", func, V8_PROPERTY_ATTRIBUTE_NONE);
}
<script language="JavaScript">
alert(window.myfunc()); // Shows an alert box with "My Value!"
</script>

函数和扩展

函数可以用于创建复杂的扩展。请注意,“本机函数”前向声明的使用,在使用扩展时需要。 (译者注:也就是提前在客户端中把JS代码以字符串的形式定义好)

void MyRenderProcessHandler::OnWebKitInitialized() {
  // Define the extension contents.
  std::string extensionCode =
    "var test;"
    "if (!test)"
    "  test = {};"
    "(function() {"
    "  test.myfunc = function() {"
    "    native function myfunc();"
    "    return myfunc();"
    "  };"
    "})();";

  // Create an instance of my CefV8Handler object.
  CefRefPtr<CefV8Handler> handler = new MyV8Handler();

  // Register the extension.
  CefRegisterExtension("v8/test", extensionCode, handler);
}
<script language="JavaScript">
alert(test.myfunc()); // Shows an alert box with "My Value!"
</script>

译者注:这种方式需要C++客户端定义JS函数,注册给前端使用。感觉有点鸡肋,还是CefV8Value::CreateFunction加上窗口绑定的方法比较实在。

使用上下文

每个浏览器窗口中的每个帧都有自己的V8上下文。上下文定义了在该帧中定义的所有变量、对象和函数的范围。如果当前代码位置在调用堆栈中具有CefV8Handler、CefV8Accessor或OnContextCreated/OnContextReleased回调,则V8将在上下文内。

OnContextCreated和OnContextReleased方法定义了与帧关联的V8上下文的完整生命周期。在使用这些方法时,请注意遵循以下规则:

  1. 不要在调用OnContextReleased之后保留或使用V8上下文引用。
  2. 所有V8对象的生命周期是未指定的。在直接从V8对象到自己的内部实现对象维护引用时要小心。在许多情况下,使用代理对象可能更好,其应用程序与V8上下文相关联,并且可以在为上下文调用OnContextReleased时“断开连接”(允许释放内部实现对象)。

如果V8当前不在上下文中,或者如果需要检索和存储对上下文的引用,则可以使用两个可用的CefV8Context静态方法。GetCurrentContext返回当前执行JS的帧的上下文。GetEnteredContext返回开始执行JS的帧的上下文。例如,如果frame1中的一个函数调用frame2中的一个函数,则当前上下文将是frame2,并且输入的上下文将是frame1。

仅当V8在上下文中时,才能创建、修改和执行数组、对象和函数。如果V8不在上下文中,则应用程序需要通过调用Enter进入上下文,并通过调用Exit退出上下文。务必仅在以下情况下使用Enter和Exit方法:

  1. 在现有上下文之外创建V8对象、函数或数组时。例如,在响应本地菜单回调时创建JS对象。
  2. 在当前上下文之外创建V8对象、函数或数组时。例如,如果从frame1发起的调用需要修改frame2的上下文。

执行函数

本机代码可以使用ExecuteFunction和ExecuteFunctionWithContext方法来执行JS函数。 ExecuteFunction方法应仅在V8已经存在上下文的情况下使用,如“使用上下文”部分所述。ExecuteFunctionWithContext方法允许应用程序指定将用于执行的上下文。

使用JS回调

注册JS函数回调与本机代码时,应用程序应在本机代码中存储当前上下文和JS函数的引用。这可以如下所示实现。
1.在OnJSBinding中创建一个“register”函数。

void MyRenderProcessHandler::OnContextCreated(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefRefPtr<CefV8Context> context) {
  // Retrieve the context's window object.
  CefRefPtr<CefV8Value> object = context->GetGlobal();

  CefRefPtr<CefV8Handler> handler = new MyV8Handler(this);
  object->SetValue("register",
                   CefV8Value::CreateFunction("register", handler),
                   V8_PROPERTY_ATTRIBUTE_NONE);
}

2.在“register”函数的MyV8Handler::Execute实现中保留对上下文和函数的引用。

bool MyV8Handler::Execute(const CefString& name,
                          CefRefPtr<CefV8Value> object,
                          const CefV8ValueList& arguments,
                          CefRefPtr<CefV8Value>& retval,
                          CefString& exception) {
  if (name == "register") {
    if (arguments.size() == 1 && arguments[0]->IsFunction()) {
      callback_func_ = arguments[0];
      callback_context_ = CefV8Context::GetCurrentContext();
      return true;
    }
  }

  return false;
}

3.通过JavaScript注册JS回调。

<script language="JavaScript">
function myFunc() {
  // do something in JS.
}
window.register(myFunc);
</script>

4.稍后执行JS回调。

CefV8ValueList args;
CefRefPtr<CefV8Value> retval;
CefRefPtr<CefV8Exception> exception;
if (callback_func_->ExecuteFunctionWithContext(callback_context_, NULL, args, retval, exception, false)) {
  if (exception.get()) {
    // Execution threw an exception.
  } else {
    // Execution succeeded.
  }
}

关于如何使用JS回调,可以参考GenalUsage wiki页面中的“异步JS绑定”小节。

译者注:这里相当于提供了一种C++调用JS函数的方式。前端在某个上下文声明的JS函数,客户端可以通过这种方式获取并调用。

重新抛出异常

如果在调用CefV8Value::ExecuteFunction*()前,调用了CefV8Value::SetRethrowExceptions(true),那么在函数执行过程中由V8生成的任何异常都会立即被重新抛出。如果重新抛出异常,则任何本地代码都需要立即返回。仅当调用栈中存在JS调用时,才应该重新抛出异常。例如,考虑以下调用栈,其中“JS”是JS函数,“EF”是本地ExecuteFunction调用:

调用栈1:JS1 -> EF1 -> JS2 -> EF2

调用栈2:本地菜单 -> EF1 -> JS2 -> EF2

对于调用栈1,EF1和EF2的重新抛出应该都为true。对于调用栈2,应该对于EF1为false,对于EF2为true。
这可以通过在本地代码中将EF的调用点分为两种类型来实现:

  1. 仅从V8处理器中调用。这包括调用栈1中的EF 1和EF2以及调用栈2中的EF2。重新抛出异常始终为true。
  2. 仅本地调用。这包括调用栈2中的EF1。重新抛出异常始终为false。

在重新抛出异常时要非常小心。不正确的使用(例如,在异常被重新抛出后立即调用ExecuteFunction())可能导致应用程序崩溃或以难以调试的方式出现故障。


译者总结

原文章的标题是JavaScript Integration,所以主题也就是如何使用CEF框架对V8引擎进行交互。这里的交互,根据我的理解,有三种形式:

  1. C++客户端代码中直接将JS代码以字符串的形式传递给V8引擎,让V8引擎异步调用由客户端编写的JS代码。这包括调用CefFrame类的ExecuteJavaScript方法,Window Binding(窗口绑定),扩展(Extensions)。其中窗口绑定可以为帧定制化需要调用的JS代码,而扩展不能定制化。
  2. C++客户端代码中定义JS函数,让V8引擎解析。而在后续的运行过程中,前端使用这个由C++客户端定义的JS函数。函数的扩展就是这种形式。
  3. C++客户端代码声明JS函数签名,在后续的运行过程中,前端若调用这个函数,则会由CEF框架转发到C++客户端代码中,相当于给前端提供了C++接口。使用CefV8Value::CreateFunction静态方法创建函数,并结合窗口绑定,则是这一种形式。这即是JS代码(前端)调用C++代码(客户端)的方法
  4. 前端JS代码在特定的上下文中定义JS函数,并执行注册操作(调用"regsiter",并传入函数名)。C++客户端中通过CefV8Handler::Execute记录函数的回调指针和上下文引用,并在后续代码中使用ExecuteFunctionWithContext去实际调用前端中定义的JS函数。这即是C++代码(客户端)调用JS代码(前端)的方法

参考:
https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration
https://openai.com/blog/chatgpt

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容