翻译:JavaScript集成

本文翻译自:https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration.md

本文档讲解如何在客户端应用程序中使用V8 JavaScript集成。

介绍

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

CEF3 Blink(WebKit)和JS执行在单独的render进程中运行。render进程中的主线程标识为TID_RENDERER,并且必须在此线程上执行所有V8操作。与JS执行相关的回调通过CefRenderProcessHandler接口公开。初始化新的render进程时,将通过CefApp::GetRenderProcessHandler()获取此接口。

在browser和render进程之间进行通信的JS API应设计成异步回调。有关详细信息,请参阅GeneralUsage的“异步JavaScript绑定”部分。

执行Javascript

从客户端应用程序执行JS的最简单方法是使用CefFrame::ExecuteJavaScript()函数。此功能在browser进程和render进程中都可用,并且可以安全地从JS上下文之外使用。

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

以上示例将在浏览器的主frame中执行并显示alert('ExecuteJavaScript works!');

ExecuteJavaScript()函数可用于与网页frame的JS上下文中的函数和变量进行交互。为了将值从JS返回到客户端应用程序,请考虑使用window对象绑定或Extensions扩展。

window对象绑定

window对象绑定允许客户端应用程序将值附加到网页frame的window对象上。使用CefRenderProcessHandler::OnContextCreated()方法实现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 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可以与window对象绑定交互。

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

每次重新加载网页frame时都会重新加载window对象绑定,从而为客户端应用程序提供在必要时更改绑定的机会。例如,通过修改绑定到该网页frame的window对象的值,可以给不同的帧访问客户端应用程序中的不同特征。

Extensions扩展

扩展类似window对象绑定,除了它们被加载到每个frame的上下文中,并且一旦加载就不能被修改。如果加载扩展时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代码。然后,网页frame中的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();

Javascript数组

使用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()变体。

Javascript对象

使用CefV8Value::CreateObject()静态方法创建对象,该方法有可选的CefV8Accessor参数。只能在上下文中创建和使用对象(有关详细信息,请参阅“使用上下文”部分)。

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

使用SetValue()方法变量将值分配给对象,该变量将键字符串作为第一个参数。

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

带访问器的对象

对象可以选择具有关联的CefV8Accessor,它提供了获取和设置值的native实现。

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

Javascript函数

CEF支持使用native实现创建JS函数。使用接受name和CefV8Handler参数的CefV8Value::CreateFunction()静态方法创建函数。只能在上下文中创建和使用函数(有关详细信息,请参阅“使用上下文”部分)。

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

函数和window对象绑定

函数可用于创建复杂的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>

函数和扩展

函数可用于创建复杂的扩展。请注意使用扩展时所需的“native function”前向声明。

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中定义的所有变量,对象和函数的作用域。如果当前代码位置在调用堆栈中具有更高的CefV8Handler、CefV8Accessor或OnContextCreated()/OnContextReleased()回调,则V8将位于上下文中。

OnContextCreated()和OnContextReleased()方法定义与frame关联的V8上下文的完整生命周期。使用这些方法时,请务必遵循以下规则:

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

如果V8不在当前上下文中,或者您需要获取并存储对上下文的引用,则可以使用两种可用的CefV8Context静态方法之一。GetCurrentContext()返回当前正在执行JS的帧的上下文。GetEnteredContext()返回JS已经进入执行的frame的上下文。例如,如果frame1中的函数调用frame2中的函数,则当前上下文将为frame2,进入的上下文将为frame1。

如果V8在上下文中,则只能创建,修改数组,对象和函数,并且在函数的情况下执行。如果V8不在上下文中,则应用程序需要通过调用Enter()来进入上下文,并通过调用Exit()退出上下文。只应使用Enter()和Exit()方法:

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

执行函数

native代码可以使用ExecuteFunction()和ExecuteFunctionWithContext()方法执行JS函数。只有在V8已经位于上下文中时才应使用ExecuteFunction()方法,如“使用上下文”部分所述。ExecuteFunctionWithContext()方法允许应用程序指定将要输入以执行的上下文。

使用Javascript回调

当使用native代码注册JS函数回调时,应用程序应该存储对本机代码中当前上下文和JS函数的引用。这可以如下实现。

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

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

通过Javascript注册js回调:

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

稍后执行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.
  }
}

有关使用回调的更多信息,请参阅GeneralUsage wiki页面的Asynchronous JavaScript Bindings部分。

重新抛出异常

如果在CefV8Value::ExecuteFunction*()之前调用CefV8Value::SetRethrowExceptions(true),则在函数执行期间由V8生成的任何异常将立即重新抛出。如果重新抛出异常,则任何本机代码都需要立即返回。如果调用堆栈中存在更高的JS调用,则只应重新抛出异常。例如,考虑以下调用堆栈,其中“JS”是JS函数,“EF”是native ExecuteFunction调用:

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

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

对于堆栈1,重新抛出对于EF1和EF2都应该是真实的。 对于堆栈2,重新抛出对于EF1应为假,对于EF2应为真。

这可以通过在本机代码中为EF提供两种调用站点来实现:

  1. 仅从V8处理程序调用。这包括堆栈1中的EF 1和EF2以及堆栈2中的EF2.Rethrow始终为真。
  2. 只是本地调用。这涵盖了堆栈2中的EF1.Rethrow总是错误的。

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