本文翻譯自JavaScriptIntegration (https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration)。本人在CEF3方面的功力尚淺,翻譯中有不當之處,請賜教。對於一些沒有太大把握的地方,同時給出了英文和翻譯。如不想看本人的拙作,亦可看幻灰龍的JavaScript和Cpp交互示例(Custom Implementation)
- 簡介
- 執行JavaScript
- 窗體綁定
- 擴展
- 基本JS類型
- JS數組
- JS對象
- 對象存取器
- JS函數
- 窗體綁定函數
- 擴展函數
- 上下文應用
- 執行函數
- 使用JS回調
- 重新拋出異常
簡介
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上下文的生命周期。使用這些方法時,必須謹守以下這些規則
- Do not hold onto or use a V8 context reference past the call to OnContextReleased() for that context.
不要試圖或者使用V8上下文引用調用過OnContextReleased()的上下文
- 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:
- 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.
- 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:
- Only called from a V8 handler. This covers EF 1 and EF2 in stack 1 and EF2 in stack 2. Rethrow is always true.
- 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>