简介
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上下文的完整生命周期。在使用这些方法时,请注意遵循以下规则:
- 不要在调用OnContextReleased之后保留或使用V8上下文引用。
- 所有V8对象的生命周期是未指定的。在直接从V8对象到自己的内部实现对象维护引用时要小心。在许多情况下,使用代理对象可能更好,其应用程序与V8上下文相关联,并且可以在为上下文调用OnContextReleased时“断开连接”(允许释放内部实现对象)。
如果V8当前不在上下文中,或者如果需要检索和存储对上下文的引用,则可以使用两个可用的CefV8Context静态方法。GetCurrentContext返回当前执行JS的帧的上下文。GetEnteredContext返回开始执行JS的帧的上下文。例如,如果frame1中的一个函数调用frame2中的一个函数,则当前上下文将是frame2,并且输入的上下文将是frame1。
仅当V8在上下文中时,才能创建、修改和执行数组、对象和函数。如果V8不在上下文中,则应用程序需要通过调用Enter进入上下文,并通过调用Exit退出上下文。务必仅在以下情况下使用Enter和Exit方法:
- 在现有上下文之外创建V8对象、函数或数组时。例如,在响应本地菜单回调时创建JS对象。
- 在当前上下文之外创建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的调用点分为两种类型来实现:
- 仅从V8处理器中调用。这包括调用栈1中的EF 1和EF2以及调用栈2中的EF2。重新抛出异常始终为true。
- 仅本地调用。这包括调用栈2中的EF1。重新抛出异常始终为false。
在重新抛出异常时要非常小心。不正确的使用(例如,在异常被重新抛出后立即调用ExecuteFunction())可能导致应用程序崩溃或以难以调试的方式出现故障。
译者总结
原文章的标题是JavaScript Integration,所以主题也就是如何使用CEF框架对V8引擎进行交互。这里的交互,根据我的理解,有三种形式:
- C++客户端代码中直接将JS代码以字符串的形式传递给V8引擎,让V8引擎异步调用由客户端编写的JS代码。这包括调用CefFrame类的ExecuteJavaScript方法,Window Binding(窗口绑定),扩展(Extensions)。其中窗口绑定可以为帧定制化需要调用的JS代码,而扩展不能定制化。
- C++客户端代码中定义JS函数,让V8引擎解析。而在后续的运行过程中,前端使用这个由C++客户端定义的JS函数。函数的扩展就是这种形式。
- C++客户端代码声明JS函数签名,在后续的运行过程中,前端若调用这个函数,则会由CEF框架转发到C++客户端代码中,相当于给前端提供了C++接口。使用CefV8Value::CreateFunction静态方法创建函数,并结合窗口绑定,则是这一种形式。这即是JS代码(前端)调用C++代码(客户端)的方法。
- 前端JS代码在特定的上下文中定义JS函数,并执行注册操作(调用"regsiter",并传入函数名)。C++客户端中通过CefV8Handler::Execute记录函数的回调指针和上下文引用,并在后续代码中使用ExecuteFunctionWithContext去实际调用前端中定义的JS函数。这即是C++代码(客户端)调用JS代码(前端)的方法。
参考:
https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration
https://openai.com/blog/chatgpt