付莎
(云南省少數(shù)民族語文指導工作委員會辦公室 云南省昆明市 650021)
傳統(tǒng)軟件開發(fā)C/S、B/S 模式各有利弊。C/S 模式可控性、定制性高,但技術(shù)門檻高、開發(fā)周期長、綜合成本大。B/S 模式技術(shù)門檻稍低,開發(fā)周期相對較短,構(gòu)建UI 交互高效快捷。但B/S 模式也因受限瀏覽器限制,在一些需要和操作系統(tǒng)、硬件設(shè)備等底層進行數(shù)據(jù)交互時近乎無能為力。復雜UI 交互如使用C/S 模式則需要花費大量時間精力且未必能達到B/S 模式效果。
CEF(Chromium Embedded Framework)是一套基于Google Chromium 的BSD 協(xié)議開源跨平臺項目。通過CEF 提供的接口,第三方應(yīng)用程序可方便、高效的將Google Chromium 集成進自己的項目[3]。
通過CEF 的嵌入使用,應(yīng)用程序可使用現(xiàn)代化的Web 技術(shù)快速、高效的構(gòu)建應(yīng)用程序UI 交互。而少部分其他需要和操作系統(tǒng)、第三方API 或硬件設(shè)備等底層進行交互的功能模塊又可全部交由原生軟件完成。并通過CEF 相關(guān)接口和Web UI 進行數(shù)據(jù)交互。
使用CEF 可使應(yīng)用系統(tǒng)構(gòu)建為一種混合模式。大量的UI 交互使用Web 技術(shù)完成。而不可以使用Web 技術(shù)完成的或使用Web 構(gòu)建較為復雜的以及使用Web 效率低下的部分則可使用原生C++編寫完成??沙浞肿畲蠡疌/S 和B/S 模式的各自優(yōu)勢。使軟件系統(tǒng)開發(fā)效率高效、而成本低廉。
最近幾年隨著Google Chromium 項目和計算機硬件的不斷發(fā)展,混合模式的開發(fā)優(yōu)勢逐漸凸顯。獲得了大量行業(yè)龍頭企業(yè)的認可與應(yīng)用。例如騰訊QQ、微信PC 版、網(wǎng)易云音樂、阿里旺旺、阿里釘釘?shù)榷际褂昧薈EF 技術(shù)或基于CEF 的深度訂制技術(shù)。
CEF 是一套基于Google Chromium[6]項目的開源嵌入式框架。隨著Google Chromium 項目的推進,其一開始使用的WebKit 內(nèi)核被新的Blink 內(nèi)核取代[1],并推出了全新的Chromium content API。新版增強了對HTML5 和GPU 硬件加速的支持,機制更現(xiàn)代化,極大改進了瀏覽器性能和穩(wěn)定性。但隨之而來的是原有底層結(jié)構(gòu)大幅改動,因此CEF 也隨content API 推出了全新版本:CEF3。原版本改名為CEF1。因此現(xiàn)今一般CEF 特指CEF3 版本。本文所述也特指CEF3 版本[3]。
Google Chromium 采用C++語言編寫。由于功能強大與技術(shù)先進,代碼量極其龐大和復雜,擴展及二次開發(fā)極為困難[5]。直接使用Chromium 源碼進行集成使用極為復雜,學習與使用成本高昂,一般開發(fā)人員掌握、使用都非常困難[4]。CEF 使用C++語言對Chromium 公共接口進行了重新封裝。對大量的 Chromium 接口進行了默認實現(xiàn),隔離了Chromium 極為龐大的源碼與具體實現(xiàn)。提供了新的易于使用的對外接口。第三方嵌入時只需使用CEF 接口提供的默認實現(xiàn),并通過少量的代碼編寫即可將 Chromium 嵌入第三方應(yīng)用使用。
CEF 是多進程模型。在CEF 構(gòu)架中定義了Broswer 和Render兩種不同類型的進程。Browser 進程負責UI 部分的窗口管理、界面繪制和相關(guān)網(wǎng)絡(luò)傳輸。Render 進程負責Blink 內(nèi)核的渲染和JavaScript 執(zhí)行。嵌入使用時主應(yīng)用程序的一些應(yīng)用邏輯例如:JavaScript 綁定、DOM 節(jié)點的訪問等也在Render 進行中執(zhí)行。默認的進程模型中,會為每個頁面創(chuàng)建一個新的Render 進程。進程之間通過IPC 進行通信。Browser 和Render 進程可以通過發(fā)送異步消息進行雙向通信[2]。
CEF 提供了libcef_dll_wrapper 工程導出了相關(guān)C++接口。使用時只需使用此包裝接口即可快速集成CEF 到第三方主應(yīng)用程序,從而隔絕了Chromium 的復雜接口。以下CEF 主要C++接口均來自libcef_dll_wrapper 工程包的導出。
CefBrowser 對象代表當前的瀏覽器窗口。通過CefBrowser 對象可以獲取當前Browser 的各項信息,并對Browser 進行特定操作。例如操作Browser 打開頁面、回退、強制刷新當前頁面等。
CefFrame 對象代表Broswer 下的Frames。每個CefBrowser 對象包含一個主CefFrame 對象,主CefFrame 對象代表Web 頁面的頂層frame。Browser 對象下可以包含零個或多個的CefFrame 對象,分別代表不同的子Frame。獲取Frame 對象后可對Frame 進行特定的操作。例如當前Frame 下的源碼獲取、DOM 獲取、Frame 內(nèi)容拷貝以及當前Frame 執(zhí)行JavaScript 語句等。
CefClient 接口提供訪問瀏覽器實例的回調(diào)接口。瀏覽器實例的相關(guān)各種控制:例如自定義處理瀏覽器的生命周期、右鍵菜單、下載處理、對話框、通知顯示、拖曳事件、焦點事件、鍵盤事件等,都必須通過CefClient 接口相應(yīng)方法指定處理handler。一個CefClient 對象實現(xiàn)可以在任意數(shù)量的Browser 進程中共享。
CefBrowserProcessHandler 接口對應(yīng)Browser 進程的回調(diào)??捎糜谠贐rowser 創(chuàng)建時進行一些特定的初始化。
CefRenderProcessHandler 接口對應(yīng)Render 進程的回調(diào)。自定義的一些應(yīng)用邏輯:例如JavaScript 綁定、擴展等需要在此接口實現(xiàn)。
CefApp 接口提供訪問進程相關(guān)的回調(diào)。包括代碼化設(shè)置啟動參數(shù)、自定義請求頭部、設(shè)置Browser 和Render 進程處理Handler。一些特殊的設(shè)置,例如開始攝像頭支持、允許使用Flash、關(guān)閉同源策略、允許訪問本機文件等都可在CefApp 提供的接口中設(shè)置。初始化CEF 函數(shù)CefInitialize 需提供此接口的實例。必須實現(xiàn)。
CefLifeSpanHandler 接口提供管理Browser 生命周期回調(diào)。
CefV8Handler 用于實現(xiàn)自注冊的JS 函數(shù)處理邏輯。當我們在CefRenderProcessHandler 接口上注冊了自定義函數(shù)或?qū)ο螅赪eb端執(zhí)行相應(yīng)的JS 代碼時,將會調(diào)用本接口的Execute 方法。
CefSettings 對象用于定義全局的CEF 配置項。例如是否使用Cookie 緩存、日志輸出、定義當前語言、遠程調(diào)試端口等。
CefString CEF 由于跨平臺和不同運行時編譯等原因提供了統(tǒng)一的字符串管理,提供統(tǒng)一的內(nèi)存堆管理、支持UTF8,UTF16 等字符串類型,為CEF 字符串定義了自己的數(shù)據(jù)結(jié)構(gòu)。在使用CEF 的過程中所有使用字符串的地方都需要使用CefString 結(jié)構(gòu)。
CefInitialize 函數(shù)用于在主應(yīng)用程序中初始化CEF Broswer 進程。
CefShutdown 函數(shù)用于在主應(yīng)用程序中關(guān)閉使CEF 停止工作。
使用CEF 時,宿主應(yīng)用程序據(jù)結(jié)構(gòu)應(yīng)按下述步驟構(gòu)建才能使CEF 正常工作,主要有以下四個步驟[2]:
(1)提供CefApp 的實現(xiàn),用于處理CEF 所需的進程相關(guān)的回調(diào)。
定義CCefClientApp 類。繼承于CefApp、CefBrowserProcess Handler 以及CefRenderProcessHandler。實現(xiàn)OnBeforeCommandLine Processing、GetBrowserProcessHandler 與GetRenderProcessHandler三個主要回調(diào)。主要用于參數(shù)設(shè)置。定義Browser 與Render 進程回調(diào)處理。
(2)提供CefClient 的實現(xiàn),用于處理CEF 所需的Browser 實例相關(guān)的回調(diào)。
定義CCefClientHandler 類。繼承于CefClient 與CefLifeSpan Handler。實現(xiàn)GetDisplayHandler、GetLifeSpanHandler、GetLoad Handler、OnProcessMessageReceived 以及OnAfterCreated、DoClose、OnBeforeClose。定義Brower 實例的各相關(guān)回調(diào)。
(3)在CefClient 實現(xiàn)中使用CEF 提供的CefBrowserHost::CreateBrowser()函數(shù)創(chuàng)建一個Browser 實例。
在CCefClientHandler 類中定義CreateBrowser 函數(shù)。使用靜態(tài)函數(shù)CefBrowserHost::CreateBrowser()創(chuàng)建Browser 實例。當宿主應(yīng)用程序窗口創(chuàng)建時,調(diào)用CreateBrowser 函數(shù)創(chuàng)建Browser 實例窗口。
(4)使用CefInitialize 初始化CEF。
在宿主應(yīng)用程序入口處,使用CefInitialize 函數(shù),傳入上述定義的CCefClientApp 對象作為啟動參數(shù)啟動CEF。
在實現(xiàn)上述四個步驟的相關(guān)定義后,即可在宿主應(yīng)用程序中嵌入CEF。由于使用了CEF 的預制默認實現(xiàn),上述大部分實現(xiàn)函數(shù)只需直接return this;即可啟動CEF。實現(xiàn)了方便、簡單的嵌入使用。
CEF 的嵌入使用最為關(guān)鍵的一項技術(shù)應(yīng)用即為構(gòu)建的Web 頁面怎么樣和原生C++程序相互交互。只有C++與JS 語言之間能實現(xiàn)數(shù)據(jù)相互交互、協(xié)助運行,本文所述的混合模式、CEF 的嵌入使用才能發(fā)揮最大的作用。
C++調(diào)用JavaScript:
C++調(diào)用執(zhí)行JavaScript 相對簡單。通過CefBrowser 獲取包含的CefFrame 對象,執(zhí)行CefFrame 對象方法ExecuteJavaScript()函數(shù)即可實現(xiàn)C++調(diào)用JavaScript。該函數(shù)在Browser 與Render 進程均可調(diào)用。需要注意的是ExecuteJavaScript 不支持JS 處理結(jié)果返回[7]。
JavaScript 調(diào)用C++:
(1)CEF 窗口綁定。允許C++將變量、對象或函數(shù)附加到一個Frame 的window 對象上,供JavaScript 端調(diào)用。窗口綁定需在CefRenderProcessHandler::OnContextCreated() 方法中實現(xiàn)并使用CefV8Value::SetValue 函數(shù)添加到上下文中,即上述CCefClientApp類中實現(xiàn)OnContextCreated 方法??舍槍Σ煌腇rame 設(shè)置同一個對象不同的值。
(2)CEF 擴展。與窗口綁定類似,區(qū)別是擴展將變量、對象或函數(shù)附加到特定對象上,非窗口綁定的window 對象。擴展將加載到所有Frame 對象上,且不能修改其值。擴展需在Ce fRenderProcessHandler::OnWebKitInitialized() 方法中實現(xiàn),即上述CCefClientApp 類中實現(xiàn)OnWebKitInitialized 方法,并使用CefRegisterExtension 函數(shù)注冊。
當JS 需要調(diào)用C++函數(shù)代碼時,一般處理方式為:首先將函數(shù)在CefV8Handler::Execute()方法中實現(xiàn),然后使用以上兩種方式之一注冊對應(yīng)函數(shù)。當JS 調(diào)用相應(yīng)函數(shù)時,將會觸發(fā)CefV8Handler::Execute()方法[8]。
本實例將演示使用CEF 嵌入式瀏覽器技術(shù)支持使用第三方廠家提供的二代身份證讀卡器SDK 讀取居民身份證信息。通過Web UI 上JS 調(diào)用宿主C++代碼操作身份證讀卡器獲取相應(yīng)信息。通過本實例可充分展示CEF 下JS 與原生C++代碼的相互交互過程。
本實例使用Windows 下Visual Studio2015 版本構(gòu)架MFC Dialog 應(yīng)用項目,使用CEF3 3325Chromium 65 版本構(gòu)建。將展示兩種方式:同步與異步JS 調(diào)用原生C++代碼。本示例主要展示CEF 使用中最核心的JS 與C++交互部分,其他部分如宿主應(yīng)用程序構(gòu)數(shù)據(jù)結(jié)構(gòu)、類定義等不再展示,只列出核心代碼。將采用最小化構(gòu)建。
MFC 工程中初始化CEF,并創(chuàng)建Browser 窗口進程。
(1)App::InitInstance()函數(shù)CEF 初始化。
(2)Dlg::OnInitDialog()函數(shù)中初始化Browser 窗口進程。
(3)Dlg::OnClose()函數(shù)中關(guān)閉Browser 進行,并銷毀CEF
(4)使用C++封裝讀卡器SDK 相關(guān)函數(shù),創(chuàng)建讀取身份證信息函數(shù)ReadIDCardInfo(wstring &info);
JS 端調(diào)用C++函數(shù),并在Render 進程處理返回。
(1)派生MyCefApp、MyCefClient 與MyCefV8Handler 類,實現(xiàn)CEF CefApp、CefRenderProcessHandler、CefClient、CefLife SpanHandler、CefV8Handler 等接口。
(2)實現(xiàn)CefRenderProcessHandler::OnContextCreated(),使用CEF Window 綁定,建并綁定JS 函數(shù)ReadIDCard 至瀏覽器window對象。
(3)實現(xiàn)CefV8Handler::Execute()。當Web 端JS 代碼中調(diào)用ReadIDCard 函數(shù)時,CEF 將會觸發(fā)Execute()。在Execute()中將調(diào)C++原生ReadIDCardInfo 函數(shù)獲取居民身份證信息并返回給JS。
詳細步驟如下:
(1)定義MyCefApp 類,實現(xiàn)CefRenderProcessHandler 接口中的OnContextCreated。并在OnContextCreated 函數(shù)中定義身份證信息讀取函數(shù)ReadIDCard 供Web 端JS 調(diào)用。
(2)定義MyCefClient 類,繼承CefClient 與CefLifeSpan Handler。CefClient 提供訪問Browser 實例的回調(diào)接口。CefLifeSpan Handler 是與之相關(guān)的Browser 實例相關(guān)生存周期回調(diào)接口,管理Brower 生存周期。
(3)定義MyCefV8Handler 類,繼承CefV8Handler。CefV8 Handler::Execute 提供JS 窗口綁定和擴展的函數(shù)回調(diào)。實現(xiàn)Execute函數(shù),當Web 端JS 調(diào)用ReadIDCard 函數(shù)時將回調(diào)此函數(shù)。
CEF 提供了一個通用的消息路由實現(xiàn)CefMessageRouter,用于在Render 進程中執(zhí)行的JS 和在Brower 進程中執(zhí)行的C++之間傳遞異步消息。使用此消息路由即可實現(xiàn)我們所希望的JS 異步調(diào)用。
(1)建立全局CefMessageRouterConfig 類實例。并在CEF 初始化時定義CefMessageRouterConfig 中定義在Web 端JS 需調(diào)用函數(shù)。
CefMessageRouterConfig g_messageRouterConfig;
messageRouterConfig.js_query_function=“cefQuery”;
messageRouterConfig.js_cancel_function=“cefQueryCancel”
(2)在Render 進程處理類MyCefApp 中添加CefMessage RouterRendererSide 成員變量,實現(xiàn)Render 一側(cè)消息路由。
m_renderer_side_router=CefMessageRouterRendererSide::Create(g_messageRouterConfig);
(3)在MyCefApp 中分別實現(xiàn)并調(diào)用CefMessageRouter RendererSide 預留的OnContextCreated、OnProcessMessageReceived與OnProcessMessageReceived 三個同名接口,使用默認實現(xiàn)即可。
(4)在Browser 進程處理類MyCefClient 中添加CefMessage RouterBrowserSide 成員變量,用于實現(xiàn)Browser 一側(cè)消息路由。
m_browser_side_router=CefMessageRouterBrowserSide::Create(g_messageRouterConfig);
(5)在MyCefClient 中分別實現(xiàn)并調(diào)用CefMessageRouter BrowserSide 預留 的OnProcessMessageReceived、OnBeforeClose、OnBeforeBrowse 與OnRenderProcessTerminated 四個同名接口,使用默認實現(xiàn)即可。
(6)實現(xiàn)JSHandler 類,繼承自CefMessageRouterBrowserSide::Handler 接口,實現(xiàn)OnQuery 與OnQueryCanceled 函數(shù),用于處理JS 端調(diào)用C++而被傳遞過來的消息。
(7)在MyCefClient 類中調(diào)用CefMessageRouterBrowserSide成員變量AddHandler 函數(shù),將上述(6)步驟實現(xiàn)的接口實例添加到消息路由中。
JSHandler m_jsHandler;
...
m_browser_side_router->AddHandler(&m_jsHandler,true);
通過上述步驟,當在Web 端調(diào)用JS 函數(shù)window.cefQuer(‘ReadIDCard’,...)時,Rander 進程將首先捕獲調(diào)動,并通過消息路由路由至Browser 進程一側(cè),最終將調(diào)用交由CefMessageRouter BrowserSide::Handler 接口實現(xiàn)二代身份證信息的讀取功能,并在完成后異步將數(shù)據(jù)傳遞給Web 端。
通過上述兩例同步與異步應(yīng)用實例,展示了嵌入式使用CEF時Web 端JS 與宿主端C++的相互調(diào)用與數(shù)據(jù)傳遞過程。通過實例可見CEF為我們隔離了大量的Chromium接口初始化以及實現(xiàn)過程,并提供了豐富的默認實現(xiàn),第三方應(yīng)用程序只需定義少量關(guān)鍵業(yè)務(wù)核心代碼即可將CEF 嵌入到第三方應(yīng)用程序中使用。為我們開發(fā)混合應(yīng)用實例提供了極大的便利。