在CEF裡,JS和Native(C/C++)代碼可以很方便的交互,講解得很清楚。我照著它實現了一個簡單的交互示例。
在貼代碼之前,先來看看Browser進程和Render進程是怎麼回事兒,有什麼不同。
從cefsimple開始吧,cefsimple_win.cc中的wWinMain函數中調用了CefExecuteProcess()方法來檢測是否要啟動其它的子進程。此處的CefExecuteProcess是在libcef_dll_wrapper.cc中的,它內部又調用了cef_execute_process方法(libcef_dll.cc),cef_execute_process又調用了libcef/browser/context.cc文件內實現的CefExecuteProcess方法。這個方法代碼如下:
int CefExecuteProcess(const CefMainArgs& args,
CefRefPtr application,
void* windows_sandbox_info) {
base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
#if defined(OS_WIN)
command_line.ParseFromString(::GetCommandLineW());
#else
command_line.InitFromArgv(args.argc, args.argv);
#endif
// Wait for the debugger as early in process initialization as possible.
if (command_line.HasSwitch(switches::kWaitForDebugger))
base::debug::WaitForDebugger(60, true);
// If no process type is specified then it represents the browser process and
// we do nothing.
std::string process_type =
command_line.GetSwitchValueASCII(switches::kProcessType);
if (process_type.empty())
return -1;
CefMainDelegate main_delegate(application);
// Execute the secondary process.
#if defined(OS_WIN)
sandbox::SandboxInterfaceInfo sandbox_info = {0};
if (windows_sandbox_info == NULL) {
content::InitializeSandboxInfo(&sandbox_info);
windows_sandbox_info = &sandbox_info;
}
content::ContentMainParams params(&main_delegate);
params.instance = args.instance;
params.sandbox_info =
static_cast(windows_sandbox_info);
return content::ContentMain(params);
#else
content::ContentMainParams params(&main_delegate);
params.argc = args.argc;
params.argv = const_cast(args.argv);
return content::ContentMain(params);
#endif
它分析了命令行參數,提取”type”參數,如果為空,說明是Browser進程,返回-1,這樣一路回溯到wWinMain方法裡,然後開始創建Browser進程相關的內容。
如果”type”參數不為空,做一些判斷,最後調用了content::ContentMain方法,直到這個方法結束,子進程隨之結束。
content::ContentMain方法再追溯下去,就到了chromium的代碼裡了,在chromium/src/content/app/content_main.cc文件中。具體我們不分析了,感興趣的可以去看看。
分析了CefExecuteProcess方法我們知道,Browser進程在cefsimple_win.cc內調用了CefExecuteProcess之後做了進一步的配置,這個是在simple_app.cc內完成的,具體就是SimpleApp::OnContextInitialized()這個方法,代碼如下:
void SimpleApp::OnContextInitialized() {
CEF_REQUIRE_UI_THREAD();
CefWindowInfo window_info;
window_info.SetAsPopup(NULL, "cefsimple");
// SimpleHandler implements browser-level callbacks.
CefRefPtr handler(new SimpleHandler());
// Specify CEF browser settings here.
CefBrowserSettings browser_settings;
std::string url;
CefRefPtr command_line =
CefCommandLine::GetGlobalCommandLine();
url = command_line->GetSwitchValue("url");
if (url.empty())
url = "http://www.google.com";
CefBrowserHost::CreateBrowser(window_info, handler.get(), url,
browser_settings, NULL);
}
可以看到,這裡創建SimpleHandler,並傳遞給CefBrowserHost::CreateBrowser去使用。
現在我們清楚了,Browser進程,需要CefApp(SimpleApp實現了這個接口)和CefClient(SimpleHandler實現了這個接口)。而Renderer進程只要CefApp。
另外,CEF還定義了CefBrowserProcessHandler和CefRenderProcessHandler兩個接口,分別來處理Browser進程和Render進程的個性化的通知。因此,Browser進程的App一般還需要實現CefBrowserProcessHandler接口,Renderer進程的App一般還需要實現CefRenderProcessHandler接口。這裡https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage有詳細說明。
像cefsimple這個示例中的SimpeApp,沒有實現CefRenderProcessHandler接口,沒有針對Renderer進程做特別處理,所以當它作為Render進程時,會缺失一部分功能。比如JS與Native代碼交互,這正是我們想要的。
如果要實現JS與Native代碼交互,最好分開實現Browser進程需要的CefApp和Render進程需要的CefApp。像下面這樣:
class ClientAppRenderer : public CefApp,
public CefRenderProcessHandler
{
...
}
class ClientAppBrowser : public CefApp,
public CefBrowserProcessHandler
{
...
}
當我們實現了CefRenderProcessHandler接口,就可以在其OnContextCreated()方法中獲取到CefFrame對應的window對象,在它上面綁定一些JS函數或對象,然後JS代碼裡就可以通過window對象訪問,如果是函數,就會調用到我們實現的CefV8Handler接口的Execute方法。
另外一種實現JS與Native交互的方式,是在實現CefRenderProcessHandler的OnWebKitInitialized()方法時導出JS擴展,具體參考https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration.md,有詳細說明。
cef_js_integration是非常簡單的示例,用來演示JS與Native的交互。它在一個項目內實現了ClientAppBrowser、ClientAppRenderer、ClientAppOther三種CefApp,分別對應Browser、Render及其它類別的三種進程。
JS和Native代碼的交互發生在Render進程,App需要繼承CefRenderProcessHandler來整合JS相關功能。因此在應用在啟動時做了進程類型判斷,根據不同的進程類型創建不同的CefApp。
這個示例演示了三種JS交互方式(參見https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration.md):
- 在native代碼中通過CefFrame::ExecuteJavaScript()來執行JavaScript代碼
- 將函數或對象綁定到CefFrame對應的window對象上,JS代碼通過window對象訪問native代碼導出的函數或對象
- 使用CefRegisterExtension()注冊JS擴展,JS直接訪問注冊到JS Context中的對象
這個項目參考了cefsimple、cefclient,還有https://github.com/acristoffers/CEF3SimpleSample,最終它比cefsimple復雜一點,比cefclient簡單很多。
好啦,背景差不多,上源碼。
cef_js_integration.cpp:
#include
#include
#include "cef_js_integration.h"
#include
#include
#include "include/cef_app.h"
#include "include/cef_browser.h"
#include "ClientAppBrowser.h"
#include "ClientAppRenderer.h"
#include "ClientAppOther.h"
#include "include/cef_command_line.h"
#include "include/cef_sandbox_win.h"
//#define CEF_USE_SANDBOX 1
#if defined(CEF_USE_SANDBOX)
#pragma comment(lib, "cef_sandbox.lib")
#endif
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// Enable High-DPI support on Windows 7 or newer.
CefEnableHighDPISupport();
CefMainArgs main_args(hInstance);
void* sandbox_info = NULL;
#if defined(CEF_USE_SANDBOX)
CefScopedSandboxInfo scoped_sandbox;
sandbox_info = scoped_sandbox.sandbox_info();
#endif
// Parse command-line arguments.
CefRefPtr command_line = CefCommandLine::CreateCommandLine();
command_line->InitFromString(::GetCommandLineW());
// Create a ClientApp of the correct type.
CefRefPtr app;
// The command-line flag won't be specified for the browser process.
if (!command_line->HasSwitch("type"))
{
app = new ClientAppBrowser();
}
else
{
const std::string& processType = command_line->GetSwitchValue("type");
if (processType == "renderer")
{
app = new ClientAppRenderer();
}
else
{
app = new ClientAppOther();
}
}
// Execute the secondary process, if any.
int exit_code = CefExecuteProcess(main_args, app, sandbox_info);
if (exit_code >= 0)
return exit_code;
// Specify CEF global settings here.
CefSettings settings;
#if !defined(CEF_USE_SANDBOX)
settings.no_sandbox = true;
#endif
// Initialize CEF.
CefInitialize(main_args, settings, app.get(), sandbox_info);
// Run the CEF message loop. This will block until CefQuitMessageLoop() is
// called.
CefRunMessageLoop();
// Shut down CEF.
CefShutdown();
return 0;
}
可以看到,_tWinMain方法中解析了命令行參數,根據進程類型創建了不同的CefApp。這是它與cefsimple的區別。
ClientAppBrowser類與cefsimple示例中的SimpleApp基本一致,略過。
ClientAppRender類在ClientAppRender.h和ClientAppRender.cpp中實現。先是ClientAppRender.h:
#ifndef CEF3_CLIENT_APP_RENDERER_H
#define CEF3_CLIENT_APP_RENDERER_H
#include "include/cef_app.h"
#include "include/cef_client.h"
#include "V8handler.h"
class ClientAppRenderer : public CefApp,
public CefRenderProcessHandler
{
public:
ClientAppRenderer();
CefRefPtr GetRenderProcessHandler() OVERRIDE
{
return this;
}
void OnContextCreated(
CefRefPtr browser,
CefRefPtr frame,
CefRefPtr context);
void OnWebKitInitialized() OVERRIDE;
private:
CefRefPtr m_v8Handler;
IMPLEMENT_REFCOUNTING(ClientAppRenderer);
};
#endif
ClientAppRender聚合了ClientV8Handler類的實例,回頭再說。先來看ClientAppRender的OnContextCreated和OnWebKitInitialized,它們是實現JS與Native交互的關鍵。代碼如下:
#include "ClientAppRenderer.h"
#include "V8handler.h"
#include
#include
ClientAppRenderer::ClientAppRenderer()
: m_v8Handler(new ClientV8Handler)
{
}
void ClientAppRenderer::OnContextCreated(CefRefPtr browser,
CefRefPtr frame,
CefRefPtr context)
{
OutputDebugString(_T("ClientAppRenderer::OnContextCreated, create window binding\r\n"));
// Retrieve the context's window object.
CefRefPtr object = context->GetGlobal();
// Create the "NativeLogin" function.
CefRefPtr func = CefV8Value::CreateFunction("NativeLogin", m_v8Handler);
// Add the "NativeLogin" function to the "window" object.
object->SetValue("NativeLogin", func, V8_PROPERTY_ATTRIBUTE_NONE);
}
void ClientAppRenderer::OnWebKitInitialized()
{
OutputDebugString(_T("ClientAppRenderer::OnWebKitInitialized, create js extensions\r\n"));
std::string app_code =
"var app;"
"if (!app)"
" app = {};"
"(function() {"
" app.GetId = function() {"
" native function GetId();"
" return GetId();"
" };"
"})();";
CefRegisterExtension("v8/app", app_code, m_v8Handler);
}
OnContextCreated給window對象綁定了一個NativeLogin函數,這個函數將由ClientV8Handler類來處理,當HTML中的JS代碼調用window.NativeLogin時,ClientV8Handler的Execute方法會被調用。
OnWebKitInitialized注冊了一個名為app的JS擴展,在這個擴展裡為app定義了GetId方法,app.GetId內部調用了native版本的GetId()。HTML中的JS代碼可能如下:
alert(app.GetId());
當浏覽器執行上面的代碼時,ClientV8Handler的Execute方法會被調用。
好啦,現在來看ClientV8Handler的實現(V8Handler.cpp):
#include "V8handler.h"
#include
#include
bool ClientV8Handler::Execute(const CefString& name,
CefRefPtr object,
const CefV8ValueList& arguments,
CefRefPtr& retval,
CefString& exception)
{
if (name == "NativeLogin")
{
if (arguments.size() == 2)
{
CefString strUser = arguments.at(0)->GetStringValue();
CefString strPassword = arguments.at(1)->GetStringValue();
TCHAR szLog[256] = { 0 };
_stprintf_s(szLog, 256, _T("user - %s, password - %s\r\n"), strUser.c_str(), strPassword.c_str());
OutputDebugString(szLog);
//TODO: doSomething() in native way
retval = CefV8Value::CreateInt(0);
}
else
{
retval = CefV8Value::CreateInt(2);
}
return true;
}
else if (name == "GetId")
{
if (arguments.size() == 0)
{
// execute javascript
// just for test
CefRefPtr frame = CefV8Context::GetCurrentContext()->GetBrowser()->GetMainFrame();
frame->ExecuteJavaScript("alert('Hello, I came from native world.')", frame->GetURL(), 0);
// return to JS
retval = CefV8Value::CreateString("72395678");
return true;
}
}
// Function does not exist.
return false;
}
Execute在處理GetId方法時,還使用CefFrame::ExecuteJavaScript演示了如何在native代碼中執行JS代碼。
最後,來看一下html代碼:
<script type="text/javascript"> function Login(){ window.NativeLogin(document.getElementById("userName").value, document.getElementById("password").value); } function GetId(){ alert("get id from native by extensions: " + app.GetId()); } </script>Call into native by Window bindings:
<form> <code>UserName: <input id="userName" type="text" /> Password: <input id="password" type="text" /> <input onclick="Login()" type="button" value="Login" /> </code></form>
Call into native by js extensions:
通過下面的命令可以測試:
cef_js_integration.exe --url=file:///cef_js_integration.html
好啦,JS與Native交互的示例就到這裡了。