戰(zhàn):從下載配置到生命周期管理)
簡介為MFC應(yīng)用嵌入CEF瀏覽器內(nèi)核的完整示例工程源自Code Project社區(qū)專家之手面向希望在桌面客戶端中集成現(xiàn)代Web技術(shù)、增強(qiáng)界面表現(xiàn)力的開發(fā)者。壓縮包共26個(gè)文件包括10個(gè)h頭文件、7個(gè)cpp源文件以及rc資源腳本、html演示頁面、ico圖標(biāo)、bmp工具欄圖片和vcxproj項(xiàng)目文件整體僅81KB頭文件與源文件構(gòu)成核心實(shí)現(xiàn)rc/html/bmp分別承擔(dān)界面資源與測試頁面目錄結(jié)構(gòu)清晰便于快速定位關(guān)鍵模塊。示例圍繞CEF初始化、瀏覽器視圖創(chuàng)建、消息循環(huán)協(xié)同、生命周期管理四大核心展開通過CefWindowsHelpers、ClientHandler、CefView等類展示了MFC窗口如何承載并驅(qū)動(dòng)Chromium內(nèi)核附帶的html頁面可用于驗(yàn)證前端與CEF接口的聯(lián)動(dòng)效果同時(shí)借助CefV8Context等接口還能實(shí)現(xiàn)JavaScript與C的數(shù)據(jù)交互。已有1225人學(xué)習(xí)對于初學(xué)者和有經(jīng)驗(yàn)的MFC開發(fā)者均具參考價(jià)值可直接作為項(xiàng)目起點(diǎn)進(jìn)行二次開發(fā)與功能定制。 先說個(gè)場景老舊的 MFC 項(xiàng)目跑得好好的客戶突然提了一句“這個(gè)窗口能不能直接顯示網(wǎng)頁”登錄頁、報(bào)表頁、H5 活動(dòng)頁全想塞進(jìn)桌面客戶端。最開始我想的是 MFC 自帶的 WebBrowser 控件畢竟拖一下就能用但真遇到復(fù)雜頁面就開始卡殼渲染邏輯落后JS 兼容性問題一堆。所以把目光轉(zhuǎn)向 CEFChromium Embedded Framework直接在 MFC 窗口里嵌一個(gè) Chromium 內(nèi)核的瀏覽器頁面兼容性、渲染速度、定制能力都解決了。這篇文章我不講空話就按一個(gè)可直接運(yùn)行的 demo 為主線把從下載 CE F 到嵌入、初始化、關(guān)閉、踩坑的全過程都捋一遍適合剛接手 MFC 老項(xiàng)目、又需要內(nèi)嵌瀏覽器的同學(xué)。1. 為什么選 CEF而不是被 IE 控件逼瘋1.1 客戶的需求其實(shí)只有一句話做桌面端的人都有體會(huì)客戶說“做個(gè)瀏覽器功能”時(shí)真實(shí)需求往往很樸素你客戶端里現(xiàn)有的窗口列表、菜單、工具欄都保留只是在某個(gè)區(qū)域 Web 頁面能和 C 代碼互相通信。比如點(diǎn)頁面上一個(gè)按鈕客戶端去調(diào)本地接口或者客戶端主動(dòng)通知網(wǎng)頁刷新數(shù)據(jù)。這種需求在 MFC 里有幾種實(shí)現(xiàn)路徑但我勸你別一步就跳進(jìn)成熟方案的坑。最早我用 WebBrowser 控件時(shí)加載個(gè)帶 ES6 語法的頁面直接白屏更別提 WebRTC、Canvas 這些能力了。后來公司內(nèi)部做過一次選型評估發(fā)現(xiàn)團(tuán)隊(duì)里大部分瀏覽器相關(guān)的歷史包袱全是因?yàn)榈讓觾?nèi)核版本太低導(dǎo)致的兼容性問題。換掉內(nèi)核這些問題能消掉七八成。1.2 主流嵌入式瀏覽器方案對比市面上能嵌到 MFC 里的方案大概四個(gè)MFC 自帶的 WebBrowser 控件、WebView2、CEF、以及自己封裝 Chrome 的 ActiveX 版本。橫向參數(shù)我整理成表格實(shí)際選型時(shí)會(huì)更直觀方案內(nèi)核集成難度體積影響定制能力長期維護(hù)WebBrowser 控件IE 內(nèi)核系統(tǒng)決定低小弱微軟已停止迭代 IE 內(nèi)核功能WebView2ChromiumEdge 同源中中等較強(qiáng)官方持續(xù)更新依賴 Edge Runtime 安裝環(huán)境CEFChromium較高較大約 80~150MB極強(qiáng)可改渲染、資源加載、JS 橋接版本更新自己負(fù)責(zé)ActiveX 殼封裝各廠商封裝中不定一般受制于封裝方C 端產(chǎn)品可能更偏向 WebView2因?yàn)榘惭b包不用自帶 Chromium系統(tǒng)裝好 Edge Runtime 就能跑。但我們是 MFC 為主、需要離線部署、又要深入定制頁面加載和通信邏輯CEF 這種“自己控制發(fā)行包”的模式明顯更可控。另外 CEF 可以脫離系統(tǒng)瀏覽器版本限制把內(nèi)核版本鎖在自己手里這就避免了很多“客戶電腦瀏覽器太老頁面渲染全亂”的運(yùn)維災(zāi)難。選型時(shí)的另一個(gè)決策點(diǎn)是團(tuán)隊(duì)是否愿意長期維護(hù)一個(gè)自集成內(nèi)核。CEF 每次大版本升級都意味著重新適配、回歸測試如果你的項(xiàng)目只是偶爾用一下網(wǎng)頁那 WebView2 更省心如果你打算把大量業(yè)務(wù)頁面都搬到 Web 側(cè)同時(shí)保留桌面端的操作習(xí)慣CEF 的投資是值得的。2. 動(dòng)手之前先把 CEF 的“脾氣”摸清2.1 多進(jìn)程架構(gòu)為什么會(huì)看到一堆 cef.exe第一次跑 CEF demo 的同學(xué)大概率會(huì)被任務(wù)管理器嚇到一個(gè)主進(jìn)程后面跟著渲染進(jìn)程、GPU 進(jìn)程、網(wǎng)絡(luò)進(jìn)程、實(shí)用程序進(jìn)程就像一個(gè)縮小版 Chrome。這是 CEF 從 Chromium 繼承來的能力每個(gè)標(biāo)簽頁或站點(diǎn)隔離在獨(dú)立進(jìn)程里一個(gè)頁面崩了不會(huì)拖垮整個(gè)客戶端。這個(gè)機(jī)制同時(shí)也意味著關(guān)閉瀏覽器不等于進(jìn)程全沒了。如果某個(gè)渲染進(jìn)程里還有定時(shí)器、Web Worker 或者正在進(jìn)行的下載任務(wù)進(jìn)程退出就會(huì)比預(yù)期慢。如果我們的 MFC 代碼沒有按照 CEF 要求的順序去關(guān)閉瀏覽器就會(huì)出現(xiàn)“exe 已經(jīng)關(guān)了cef 相關(guān)進(jìn)程還掛在后臺(tái)”的詭異現(xiàn)象。這不是病毒而是生命周期沒收干凈。另外CEF 有“瀏覽器進(jìn)程”和“渲染子進(jìn)程”的區(qū)分。應(yīng)用第一次啟動(dòng)時(shí)CefInitialize 創(chuàng)建的進(jìn)程負(fù)責(zé)創(chuàng)建窗口、管理 UI當(dāng)頁面需要渲染時(shí)CEF 會(huì)用同一個(gè)可執(zhí)行文件再拉起子進(jìn)程。這種設(shè)計(jì)決定了我們的程序入口需要具備一些“命令分派”邏輯如果當(dāng)前命令行參數(shù)表明這是一個(gè) CEF 子進(jìn)程就進(jìn)入 CEF 的子進(jìn)程處理函數(shù)而不是走 MFC 的界面初始化流程。2.2 版本與工具鏈匹配要先確認(rèn)很多教程一上來就讓你下載最新版 CEF然后直接配置鏈接。但這里有個(gè)隱藏條件CEF 新版本對編譯器和 Windows SDK 版本要求很高。比如現(xiàn)在的新版 CEF基本要求 VS2019/VS2022 和 C17 環(huán)境而團(tuán)隊(duì)里大量 MFC 老項(xiàng)目還停在 VS2013 或 VS2015。如果你打開一個(gè) VS2013 的 MFC 工程非要硬剛新版 CEF光是編譯 libcef_dll_wrapper 就可能報(bào)一堆 C 標(biāo)準(zhǔn)庫不匹配的錯(cuò)。我自己的建議是老工程該升編譯器的就升CEF 版本選最近一年穩(wěn)定分支。另外如果要支持 Windows 7CEF 110 以上版本基本已經(jīng)放棄對 Win7 的支持不要為了追新版本把客戶的老機(jī)器坑了。還有一點(diǎn)容易被忽略CEF 有 Debug 和 Release 兩種構(gòu)建產(chǎn)物。Release 版體積小、運(yùn)行穩(wěn)定Debug 版會(huì)帶完整符號但調(diào)試啟動(dòng)特別慢。實(shí)際開發(fā)時(shí)我一般先用 Release 版本做功能聯(lián)調(diào)只在排查內(nèi)存問題時(shí)才換 Debug。很多 demo 跑不起來的直接原因就是把 Debug 和 Release 的 dll 混著放了啟動(dòng)時(shí)要么 0xc000007b 要么直接彈 DLL 加載失敗。3. 一步步把 CEF 塞進(jìn) MFC 工程3.1 下載并配置 CEF 發(fā)行包CEF 官方提供預(yù)編譯發(fā)行包選好分支后下載 Standard Distribution 即可。包里目錄大概長這樣include 頭文件目錄、libcef_dll 包裝器源碼、Release 目錄下的 libcef.dll、Resources 目錄下的資源文件。我們要做的第一件事不是急著建工程而是把目錄結(jié)構(gòu)理順。我習(xí)慣建一個(gè)ThirdParty/cef根目錄將解壓后的內(nèi)容放進(jìn)去然后在 MFC 工程屬性里配置“VC 目錄”→“包含目錄”添加ThirdParty/cef/include“VC 目錄”→“庫目錄”添加ThirdParty/cef/Release或根據(jù)平臺(tái)選 x86/x64“鏈接器”→“輸入”→“附加依賴項(xiàng)”加上libcef.lib。這里的libcef.lib是導(dǎo)入庫真正的libcef.dll還要丟到 exe 同級目錄下。另外CEF 的 C API 使用 C 接口導(dǎo)出直接調(diào)用非常難受所以官方提供了 libcef_dll_wrapper本質(zhì)上是把 C API 包成 C 類。這個(gè)包裝器并不是一個(gè)現(xiàn)成 .lib你要么把libcef_dll目錄下的源文件加入工程一起編譯要么先用 CMake 單獨(dú)構(gòu)建出libcef_dll_wrapper.lib。為了方便我通常會(huì)建一個(gè)靜態(tài)庫工程專門放這些源文件MFC 主工程只引用它。3.2 初始化 CEF 并改造 MFC 消息循環(huán)MFC 工程創(chuàng)建好后在 App 類的 InitInstance 里調(diào)用 CefInitialize這一步是整個(gè)集成的地基。這里有個(gè)關(guān)鍵選擇要不要讓 CEF 使用獨(dú)立的消息循環(huán)。BOOL CMFCCefDemoApp::InitInstance() { // ... MFC 標(biāo)準(zhǔn)初始化代碼 CefMainArgs main_args(m_hInstance); CefSettings settings; settings.no_sandbox true; settings.multi_threaded_message_loop true; CefString(settings.log_file) _T(cef.log); CefRefPtrCBrowserApp app(new CBrowserApp()); if (!CefInitialize(main_args, settings, app.get(), nullptr)) { AfxMessageBox(_T(CEF 初始化失敗)); return FALSE; } // 創(chuàng)建主窗口、進(jìn)入消息循環(huán) }multi_threaded_message_loop設(shè)為 true 后CEF 會(huì)在自己的線程里跑消息循環(huán)MFC 的CWinApp::Run()不用動(dòng)。如果設(shè)成 false你就得在 MFC 的 PreTranslateMessage 或空閑處理里反復(fù)調(diào)用CefDoMessageLoopWork()否則頁面會(huì)假死。對初學(xué)者來說多線程模式是最省心的我后面的 demo 也默認(rèn)用這個(gè)模式。還要注意一點(diǎn)程序入口處最好加上子進(jìn)程分流。如果在 InitInstance 一開始發(fā)現(xiàn)命令行參數(shù)里帶--typerenderer、--typegpu-process之類的參數(shù)就應(yīng)該直接調(diào)用CefExecuteProcess然后退出當(dāng)前實(shí)例而不是繼續(xù)初始化 MFC 窗口。很多實(shí)際項(xiàng)目把子進(jìn)程當(dāng)普通窗口啟動(dòng)結(jié)果內(nèi)存暴漲、進(jìn)程管理混亂就是這個(gè)分流沒做。3.3 在 MFC 窗口里創(chuàng)建瀏覽器并完成消息對接界面布局我先放一個(gè)占位的CStatic控件名字叫IDC_CEF_PLACEHOLDER。創(chuàng)建瀏覽器時(shí)把 CEF 窗口直接掛在占位控件下面相當(dāng)于把靜態(tài)控件當(dāng)成一個(gè)“畫布容器”。void CMainFrame::CreateBrowser() { CefWindowInfo info; CRect rc; m_cefPlaceHolder.GetWindowRect(rc); info.SetAsChild(m_cefPlaceHolder.GetSafeHwnd(), rc); CefBrowserSettings browser_settings; // 這里建議先加載一個(gè)簡單頁面驗(yàn)證環(huán)境比如本機(jī) demo 頁面 CefString url _T(file:///D:/demo/index.html); CefRefPtrCBrowserClient client(new CBrowserClient(GetSafeHwnd())); m_browser CefBrowserHost::CreateBrowserSync(info, client.get(), url, browser_settings, nullptr); }CBrowserClient需要從CefClient派生并實(shí)現(xiàn)生命周期管理相關(guān)的接口。多數(shù)人忽略GetLifeSpanHandler和GetLoadHandler導(dǎo)致頁面加載狀態(tài)拿不到、關(guān)閉流程亂套。推薦一開始就把這兩個(gè) handler 的骨架寫好在OnAfterCreated里保存瀏覽器對象在OnLoadEnd里通知 MFC“頁面加載完成”在DoClose里做特殊處理防止 MFC 主窗口銷毀時(shí)瀏覽器連帶出問題。窗口大小變化時(shí)還需要調(diào)用m_browser-GetHost()-WasResized()否則 CEF 子窗口不會(huì)重新布局頁面會(huì)出現(xiàn)白色邊條或者拉伸錯(cuò)亂。4. 避坑實(shí)錄關(guān)閉、殘留、白屏與崩潰4.1 進(jìn)程關(guān)不掉問題大概率不在 CEF“cef 進(jìn)程如何關(guān)掉”這個(gè)話題在社區(qū)里問爛了。項(xiàng)目關(guān)閉后電腦上還掛著幾個(gè)cef.exe這些進(jìn)程既占用內(nèi)存又會(huì)讓下一次啟動(dòng)時(shí)端口資源沖突。遇到這種情況我最先檢查的永遠(yuǎn)是關(guān)閉順序。CEF 官方推薦流程是先調(diào)用CefBrowserHost::CloseBrowser(false)讓頁面有機(jī)會(huì)執(zhí)行beforeunload事件等最后一個(gè)瀏覽器窗口關(guān)閉后觸發(fā)OnBeforeClose這時(shí)再去調(diào)用CefShutdown。如果 MFC 主窗口先銷毀了但 CEF 子窗口沒來得及銷毀或者你根本沒調(diào)用CloseBrowser那么渲染進(jìn)程必然殘留。在 MFC 的 OnClose 里我一般這么處理void CMainFrame::OnClose() { if (m_browser m_browser-GetHost()) { m_browser-GetHost()-CloseBrowser(true); // 等待 OnBeforeClose 回調(diào)中發(fā)出退出消息 return; // 先不銷毀主窗口 } CWnd::OnClose(); }CloseBrowser(true)是強(qiáng)制關(guān)閉省心但不給頁面善后機(jī)會(huì)生產(chǎn)環(huán)境建議用CloseBrowser(false)然后在DoClose回調(diào)里向 MFC 主窗口發(fā)送自定義消息由主窗口統(tǒng)一銷毀。簡單來說進(jìn)程殘留十有八九是“MFC 主窗口死得太早CEF 還不知道自己該下班了”。4.2 DLL 加載失敗與字符集設(shè)置CEF 運(yùn)行起來依賴的 dll 不少除了libcef.dll還有l(wèi)ibEGL.dll、libGLESv2.dll、icudtl.dat、snapshot_blob.bin等資源文件。很多新手只復(fù)制了 libcef.dll啟動(dòng)直接報(bào)“找不到入口點(diǎn)”或“應(yīng)用程序無法正常啟動(dòng)(0xc000007b)”。其實(shí) CEF 的 Release 目錄和 Resources 目錄里的文件幾乎都要復(fù)制到 exe 同級目錄并且要保留locales、swiftshader這樣的子文件夾。另一個(gè)坑在于 MFC 項(xiàng)目字符集。CEF 要求項(xiàng)目使用 Unicode 字符集打開項(xiàng)目屬性“常規(guī)”→“字符集”里如果還停留在“多字節(jié)字符集”鏈接階段就會(huì)報(bào)一堆字符類型不匹配的錯(cuò)誤。這個(gè)配置在新建 MFC 工程時(shí)默認(rèn)不是 Unicode很多人栽在這里浪費(fèi)半天時(shí)間排查。如果項(xiàng)目跑在 32 位環(huán)境必須確保 libcef.dll 也選的是 x86 版本千萬別把 x64 的 Release dll 硬塞給編譯出來是 x86 的程序這幾乎 100% 會(huì)出現(xiàn) 0xc000007b。反之64 位程序要用 x64 dll這條規(guī)則簡單但必須刻在腦子里。4.3 窗口大小變化白屏或閃爍嵌入 CE F 后最影響觀感的問題就是用戶拖動(dòng)窗口邊緣時(shí)瀏覽器區(qū)域閃爍、白屏。這個(gè)問題的根源通常有兩個(gè)一是父窗口沒有正確觸發(fā)布局刷新二是 CEF 子窗口沒有被同步調(diào)節(jié)尺寸。MFC 窗口在OnSize里應(yīng)該把占位控件鋪滿整個(gè)客戶區(qū)再通知瀏覽器void CMainFrame::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); if (m_cefPlaceHolder.GetSafeHwnd()) { m_cefPlaceHolder.MoveWindow(0, 0, cx, cy); } if (m_browser m_browser-GetHost()) { m_browser-GetHost()-WasResized(); } }如果還閃爍看一下占位控件是否啟用了NotifyParent或者被設(shè)置成透明背景。CEF 本質(zhì)上是一個(gè)獨(dú)立子窗口不要讓 MFC 在它上面頻繁重繪背景否則視覺上就會(huì)“白一下、閃一下”。有時(shí)候把主窗口的WS_CLIPCHILDREN樣式加上能明顯減少閃爍。5. 從 Demo 到生產(chǎn)的擴(kuò)展建議5.1 JS 與 C 的雙向通信Demo 做出來只能證明“能顯示網(wǎng)頁”實(shí)際業(yè)務(wù)更關(guān)心的是“網(wǎng)頁和客戶端怎么通信”。CEF 的辦法不像 WebView2 那么簡單但也不復(fù)雜。頁面?zhèn)扔脀indow.cefQuery或者自定義 schemeC 側(cè)用CefMessageRouter注冊處理器。以cefQuery為例C 側(cè)處理函數(shù)接收 JavaScript 傳來的請求字符串解析后調(diào)用本地邏輯再通過回調(diào)把結(jié)果返回給 JS。這個(gè)機(jī)制非常適合“頁面報(bào)表查詢本地?cái)?shù)據(jù)庫”這類場景。我實(shí)際項(xiàng)目里把本地加密、讀卡器、打印服務(wù)全部封裝成這類橋接接口網(wǎng)頁端只需要統(tǒng)一調(diào)用window.cefQuery({request: print, ...})就行。如果你希望做得更底層還可以在OnBeforeResourceLoad里攔截資源請求給特定 URL 加上自定義 Header 做鑒權(quán)或者在OnResourceLoadComplete里統(tǒng)計(jì)頁面資源加載耗時(shí)。CEF 的定制空間很大理論上 Chromium 能做的CEF 都能做關(guān)鍵在于投入的開發(fā)精力值不值得。5.2 打包與分發(fā)要點(diǎn)CEF 集成后的客戶端體積不會(huì)小。即使已經(jīng)把 Release 目錄剝掉一部分無用 dll一個(gè)最小可運(yùn)行包也基本在 50MB 以上。打包時(shí)我不是簡單把 dll 丟到 exe 目錄就完事還要注意幾個(gè)細(xì)節(jié)libcef.dll和resources.pak等文件必須和 exe 同級或者按 CEF 的目錄規(guī)則放置不能隨意改路徑如果使用file://加載本地頁面要注意本地頁面里的相對路徑和 JS 模塊加載方式很多頁面在瀏覽器里能跑放到 CEF 里就找不到資源多半是路徑基座問題發(fā)布時(shí)建議把 CEF 版本號寫進(jìn)產(chǎn)品說明或版本文件里方便后續(xù)排查兼容性和安全補(bǔ)丁。我自己的習(xí)慣是把整個(gè) CEF 相關(guān)文件作為“運(yùn)行時(shí)組件”單獨(dú)打一個(gè)子目錄例如runtime/cef/然后啟動(dòng)時(shí)調(diào)用CefSettings.browser_subprocess_path和CefSettings.resources_dir_path明確指定路徑。這樣主系統(tǒng)升級時(shí)CEF 組件可以被單獨(dú)替換不至于每次都要重做整個(gè)安裝包。按我這幾年的經(jīng)驗(yàn)CEF 集成這事本身不復(fù)雜真正復(fù)雜的是生命周期和進(jìn)程管理。如果你第一次搞建議先跑通最小 demo確認(rèn)消息循環(huán)、關(guān)閉流程都干凈再往里面加業(yè)務(wù)邏輯。否則后續(xù)每加一個(gè)功能都有可能在“關(guān)閉時(shí)殘留”或“渲染崩潰”上翻車。等這版跑穩(wěn)了你再看 WebView2 或新版 CEF 分支心里會(huì)踏實(shí)很多。本文還有配套的精品資源點(diǎn)擊獲取