CEF3开发者系列之JS与C++交互之二

时间:2022-09-22 09:07:38

本文翻译自JavaScriptIntegration (https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration)。本人在CEF3方面的功力尚浅,翻译中有不当之处,请赐教。对于一些没有太大把握的地方,同时给出了英文和翻译。如不想看本人的拙作,亦可看幻灰龙的JavaScript和Cpp交互示例(Custom Implementation)

  1. 简介
  2. 执行JavaScript
  3. 窗体绑定
  4. 扩展
  5. 基本JS类型
  6. JS数组
  7. JS对象
    1. 对象存取器
  8. JS函数
    1. 窗体绑定函数
    2. 扩展函数
  9. 上下文应用
  10. 执行函数
    1. 使用JS回调
    2. 重新抛出异常

简介

Chromium 和CEF使用V8 JS 引擎 执行内部的JS。每一个Frame在浏览器进程中都有一个属于自己的JS上下文,在frame( 更多内容参考“使用上下文”)中提供一个安全和有限的环境执行js代码。cef对外有大量的js特征在客户端应用里。

CEF3 Blink (WebKit) 和 JS执行运行在独立的渲染进程中。渲染进程中的主线程命名为TID_RENDERER并且所有的V8运行在这个线程中。回调与JS运行通过对外的CefRenderProcessHandler 接口相关联。当一个新的渲染进程被初始化时,该接口通过CefApp::GetRenderProcessHandler()唤醒。

JS APIs 被设计用异步回调方法在浏览器和渲染进程之间通讯。参考wiki GeneralUsage “异步JS绑定”章节,获取更多的信息。

 

执行JavaScript

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

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

上边简单的实例返回的结果是弹出('ExecuteJavaScript works!');运行在browser的主Frame中。

ExecuteJavaScript()函数可以用来与函数和变量交互框架的JS上下文,为了JS返回结果到客户端应用,可以使用窗口绑定或扩展

 

窗体绑定

窗口绑定允许客户端应用程序把值附上一个框架窗口对象,窗口绑定使用 CefRenderProcessHandler::OnContextCreated() method实现。

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);
}

frame中的JavaScript 与窗口绑定相配合使用

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

 

窗口绑定每一次重新加载,框架重新加载都会给客户端应用程序在必要时修改绑定的机会。 例如:不同的框架可能被允许有权使用不同的客户端应用特性,通过修改与该框架的窗口对象绑定的值来实现。

 

扩展

扩展和窗体绑定类似,除了在每个框架的上下文中加载和加载后不能修改。当扩展加载后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);
}

该字符串代表拓展代码,可以是任何合法的JS代码,在框架中JS可以和扩展代码交互。

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

 

基本的JS类型

CEF支持创建JS基本数据类型,包含undefined, null, bool, int, double, date 和 string。这些类型使用CefV8Value::Create*()静态方法创建。例如,创建一个新的JS string类型使用CreateString()方法。CefRefPtr<CefV8Value> str = CefV8Value::CreateString("My Value!");

基本值类型可以在任何时候创建并且不需要首先和特定的上下文关联(查看“Working with Contexts”获取更多地信息)。检测值类型使用Is*()方法:

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

获取变量的值使用 Get*Value() 方法.

CefString strVal = val.GetStringValue();

 

JS 数组

js 数组可以使用静态方法CefV8Value::CreateArray()创建,创建时可初始化长度。数组仅能在上下文中创建和使用(查看“Working with Contexts”获取更多信息)

//创建一个包含两个值的数组

CefRefPtr<CefV8Value> arr = CefV8Value::CreateArray(2);

使用SetValue()方法对数组进行赋值,第一个变量作为数组的索引。

//添加两个值到数组

arr->SetValue(0, CefV8Value::CreateString("My First String!"));
arr->SetValue(1, CefV8Value::CreateString("My Second String!"));

检测一个CefV8Value 是否是数组使用IsArray() 方法。获取数组长度使用 GetArrayLength() 方法。从数组中获取一个值使用GetValue(),其参数为数组的索引值。

 

JS对象

JS对象由静态方法CefV8Value::CreateObject()创建,包含一个可选的CefV8Accessor 参数。对象仅能在上下文中创建和使用(查看“Working with Contexts”获取更多信息)

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

使用SetValue() 方法给对象赋值,第一个参数作为对象的键值字符串

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

 

对象存储器

对象可以使用一个关联CefV8Accessor 提供本地接口实现获取和设置数值。

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);
};

为了给访问器传值必须使用SetValue()方法,函数接受为AccessControl 和PropertyAttribute属性变量。

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

 

JS函数

CEF3支持本地实现创建JS函数,函数使用静态方法CefV8Value::CreateFunction() 创建。接收name和CefV8Handler属性参数。函数仅能在上线文中创建和使用(查看“Working with Contexts”获取更多信息)

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);
};

 

窗体绑定函数

函数可被使用为创建复杂的窗体绑定

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>

扩展函数

可使用函数创建复杂的扩展。注意使用“本地函数”前置声明时需要使用扩展。

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>

 

使用上下文

在浏览器窗体中的每一个frame都有自己的V8上下文。上下文决定了所有的定义在frame中的变量、对象和函数的使用范围。V8在上下文中  如果当前代码设置为CefV8Handler, CefV8Accessor 或者OnContextCreated()/OnContextReleased()回调在调用堆栈中更高一级。 OnContextCreated()和 OnContextReleased()方法决定了frame中V8上下文的生命周期。使用这些方法时,必须谨守以下这些规则

  1. Do not hold onto or use a V8 context reference past the call to OnContextReleased() for that context.

        不要试图或者使用V8上下文引用调用过OnContextReleased()的上下文

  1. The lifespan of all V8 objects is unspecified (up to the GC). Be careful when maintaining references directly from V8 objects to your own internal implementation objects. In many cases it may be better to use a proxy object that your application associates with the V8 context and which can be "disconnected" (allowing your internal implementation object to be freed) when OnContextReleased() is called for the context.

         所有V8的生命周期未指定,从V8对象到你自己的内部实现对象保持直接引用要留心。很多情况下使用代理对象  应用程序和V8上下文相关联 当OnContextReleased()被上线文调用“断开”(让内部实现对象被释放)

 

If V8 is not currently inside a context, or if you need to retrieve and store a reference to a context, you can use one of two available CefV8Context static methods. GetCurrentContext() returns the context for the frame that is currently executing JS. GetEnteredContext() returns the context for the frame where JS execution began. For example, if a function in frame1 calls a function in frame2 then the current context will be frame2 and the entered context will be frame1.

如果V8当前不在上下文中,或者你需要获取和保存一个引用到上下文,你可以使用两种可用CefV8Context 静态方法。GetCurrentContext()返回frame中当前正在执行JS的上下文, GetEnteredContext() 返回frame中JS开始执行时的上下文。例如,如果一个frame1中的函数调用frame2中的函数,而当前的上下文在frame2中,入口上下文在frame1中。

Arrays, objects and functions may only be created, modified and, in the case of functions, executed, if V8 is inside a context. If V8 is not inside a context then the application needs to enter a context by calling Enter() and exit the context by calling Exit(). The Enter() and Exit() methods should only be used:

      1. When creating a V8 object, function or array outside of an existing context. For example, when creating a JS object in response to a native menu callback.
      2. When creating a V8 object, function or array in a context other than the current context. For example, if a call originating from frame1 needs to modify the context of frame2.

数组、对象和函数只能被创建、修改和执行,如果V8在上下文中。当V8不在上下文中时,应用程序需要通过Enter( )进入上下文,通过Exit()退出上下文。Enter() 和Exit()方法只能在以下情况应用:

1、当在一个已存在的上下文之外创建V8对象、函数和数组时。例如:当创建一个JS对象回应本地菜单回调。

2、当在一个除了当前上下文之外上下文中创建V8对象、函数和数组时。例如:如果从frame1中发起的调用需要修改frame2中的上下文。

 

执行函数

Native code can execute JS functions by using the ExecuteFunction() and ExecuteFunctionWithContext() methods. The ExecuteFunction() method should only be used if V8 is already inside a context as described in the "Working with Contexts" section. The ExecuteFunctionWithContext() method allows the application to specify the context that will be entered for execution.

本地代码通过 ExecuteFunction() 和ExecuteFunctionWithContext()方法执行JS函数。ExecuteFunction()方法只允许用在V8中,且V8在“使用上下文”所描述的上下文中。ExecuteFunctionWithContext()方法允许应用程序指定即将执行的上下文。

使用JS回调

When registering a JS function callback with native code the application should store a reference to both the current context and the JS function in the native code. This could be implemented as follows.

当在应用中使用本地代码注册了一个JS回调函数,要为当前上下文和本地代码中JS函数存储一个引用。该实现代码如下:

1. Create a "register" function in OnJSBinding(). 

1. 在OnJSBinding()中创建一个“注册”函数

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. In the MyV8Handler::Execute() implementation for the "register" function keep a reference to both the context and the function.

2.在MyV8Handler::Execute() 实现为上下文和JS函数保持一个可引用 "register"函数

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. Register the JS callback via JavaScript.

3.通过JavaScript注册JS回调

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

4. Execute the JS callback at some later time.

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.
  }
}

See the Asynchronous JavaScript Bindings section of the GeneralUsage wiki page for more information on using callbacks.

重新抛出异常

If CefV8Value::SetRethrowExceptions(true) is called before CefV8Value::ExecuteFunction*() then any exceptions generated by V8 during function execution will be immediately rethrown. If an exception is rethrown any native code needs to immediately return. Exceptions should only be rethrown if there is a JS call higher in the call stack. For example, consider the following call stacks where "JS" is a JS function and "EF" is a native ExecuteFunction call:

Stack 1: JS1 -> EF1 -> JS2 -> EF2

Stack 2: Native Menu -> EF1 -> JS2 -> EF2

With stack 1 rethrow should be true for both EF1 and EF2. With stack 2 rethrow should false for EF1 and true for EF2.

This can be implemented by having two varieties of call sites for EF in the native code:

      1. Only called from a V8 handler. This covers EF 1 and EF2 in stack 1 and EF2 in stack 2. Rethrow is always true.
      2. Only called natively. This covers EF1 in stack 2. Rethrow is always false.

Be very careful when rethrowing exceptions. Incorrect usage (for example, calling ExecuteFunction() immediately after exception has been rethrown) may cause your application to crash or malfunction in hard to debug ways.

 

如果CefV8Value::SetRethrowExceptions(true) 在 CefV8Value::ExecuteFunction*() 之前被调用。V8执行函数时产生的异常会被立即抛出。如果一个异常抛出任何本地代码需要立即返回。异常仅被抛出如果这是一个调用堆栈中更高级的JS调用。例如:在以下条件下,“JS”是一个JS方法  “EF”是一个本地ExecuteFunction 调用

Stack 1: JS1 -> EF1 -> JS2 -> EF2

Stack 2: Native Menu -> EF1 -> JS2 -> EF2

堆栈1重新抛出正确为 EF1和EF2,堆栈2重新抛出EF1错误和EF2正确

这个可以通过有两个变量调用的本地代码的EF:

1、只通过V8 handler调用,该情况包含EF1和EF2在堆栈1中,EF2在堆栈2中。重新抛出返回true

2、只通过本地调用,该情况包含EF1在堆栈2中,重新抛出返回false

当重新抛出错误时要异常小心,错误使用(例如:重新抛出异常后立即调用ExecuteFunction())可能导致应用崩溃或者故障很难被调试。

 

源文档 <https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration>