NO IMAGE

在CEF裡,JS和Native(C/C )程式碼可以很方便的互動,這裡https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration.md講解得很清楚。我照著它實現了一個簡單的互動示例。

foruok原創,如需轉載請關注foruok的微信訂閱號“程式視界”聯絡foruok。

在貼程式碼之前,先來看看Browser程序和Render程序是怎麼回事兒,有什麼不同。

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<CefApp> 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<sandbox::SandboxInterfaceInfo*>(windows_sandbox_info);
return content::ContentMain(params);
#else
content::ContentMainParams params(&main_delegate);
params.argc = args.argc;
params.argv = const_cast<const char**>(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<SimpleHandler> handler(new SimpleHandler());
// Specify CEF browser settings here.
CefBrowserSettings browser_settings;
std::string url;
CefRefPtr<CefCommandLine> 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專案

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 <windows.h>
#include <tchar.h>
#include "cef_js_integration.h"
#include <string>
#include <algorithm>
#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<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
command_line->InitFromString(::GetCommandLineW());
// Create a ClientApp of the correct type.
CefRefPtr<CefApp> 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<CefRenderProcessHandler> GetRenderProcessHandler() OVERRIDE
{
return this;
}
void OnContextCreated(
CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context);
void OnWebKitInitialized() OVERRIDE;
private:
CefRefPtr<ClientV8Handler> m_v8Handler;
IMPLEMENT_REFCOUNTING(ClientAppRenderer);
};
#endif 

ClientAppRender聚合了ClientV8Handler類的例項,回頭再說。先來看ClientAppRender的OnContextCreated和OnWebKitInitialized,它們是實現JS與Native互動的關鍵。程式碼如下:

#include "ClientAppRenderer.h"
#include "V8handler.h"
#include <Windows.h>
#include <tchar.h>
ClientAppRenderer::ClientAppRenderer()
: m_v8Handler(new ClientV8Handler)
{
}
void ClientAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context)
{
OutputDebugString(_T("ClientAppRenderer::OnContextCreated, create window binding\r\n"));
// Retrieve the context's window object.
CefRefPtr<CefV8Value> object = context->GetGlobal();
// Create the "NativeLogin" function.
CefRefPtr<CefV8Value> 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 <Windows.h>
#include <tchar.h>
bool ClientV8Handler::Execute(const CefString& name,
CefRefPtr<CefV8Value> object,
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& 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<CefFrame> 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程式碼:

<!DOCTYPE html>
<html>
<!--
Copyright (c) 2016 foruok. All rights reserved.
歡迎關注foruok的微信訂閱好“程式視界”。
-->
<head>
<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>
<title>CEF JS Integration</title>
</head>
<body>
<h3>Call into native by Window bindings:</h3>
<form>
UserName: <input type="text" id="userName" />&nbsp;&nbsp;Password: 
<input type="text" id="password" />&nbsp;&nbsp;<input  type="button" value="Login" onclick="Login()"/>
</form>
<hr>
<h3>Call into native by js extensions:</h3>
<input  type="button" value="GetId" onclick="GetId()"/>
</html>

通過下面的命令可以測試:

cef_js_integration.exe --url=file:///cef_js_integration.html

好啦,JS與Native互動的示例就到這裡了。

其他參考文章: