资讯动态

VS2010+CEF3 90.6.0:MFC内嵌网页与JS/C++双向调用

发布时间:2026/10/29 23:21:09 来源:尧图企业网站定制
简介面向使用VS2010维护传统MFC桌面项目的C开发者这份工程包聚焦CEF3 90.6.0嵌入MFC窗口并打通JavaScript与C互调的实现适合需要为存量客户端加入HTML5界面、复用Web前端能力或研究CEF消息路由与渲染窗口托管的场景。压缩包共629个文件、约457.22MB包含269个头文件、168个pak资源、28个DLL、14个库文件及若干cpp、obj、lib、pdb与工程配置覆盖CEF运行时、头文件、链接库、预编译与Debug/Release构建产物并附sln、vcxproj等解决方案结构便于直接对照工程配置与依赖布局。已有1558人学习下载。读者可从中了解CEFRenderWidgetHost子类化、MFC与CEF消息循环同步、browser_subprocess_path与multi_threaded_message_loop等启动参数以及JS调用C的注册与回调思路用作搭建、编译和排错参照。1. VS2010 CEF3 90.6.0 的真实场景老 MFC 程序要接一套前端界面产线上还挂着 Win7 32 位的机器主程序是十年前用 VS2010 写的 MFC 窗体报表、图表、配置页全用CDC一根线一根线画改一次样式要重编一次。客户提出拆掉配置页换成用现代前端编写的页面但整套 MFC 采集逻辑不能重写。这就落到 VS2010 CEF3 90.6.0 的典型用法MFC 只保留主框架和 C 业务界面交给内嵌的 Chromium 内核再用 JS 调 C 把数据接回去。这份资源就是围绕这个问题整理的 C 工程CEF3 外壳、MFC 宿主窗口、JS 与 C 双向调用的桥接层。适合仍在维护老 MFC 项目、又不能直接升级工具链的 C 开发者如果你想要 Qt 版本或 cefsimple 那种极简示例这份东西反而偏重。2. 工程落地VS2010 与 CEF3 90.6.0 的目录、工具链与依赖老项目第一件事是摆位置。CEF 的发行包不能完整塞进工程目录Debug、Release、Resources、cefclient全混在一起编译一次要半小时传送代码还容易漏文件。我一般把它拆成三块头文件、静态库/导入库、运行时资源只有运行时资源跟着 exe 走。2.1 把 CEF3 90.6.0 发行包拆成「头文件 / 库 / 运行时」解压 Windows 32 位发行包实际用得上的只有下面几类。libcef_dll_wrapper在发行包里是源码工程不是现成的 lib必须自己编一次这是新手第一处卡住的地方。文件/目录用途是否随 exe 发布include/cef_*.h全部 CEF 头文件约 60 多个否仅编译期libcef.lib导入库负责链接libcef.dll否libcef_dll_wrapper.libC 封装层需从libcef_dll/源码编译产出否libcef.dll真正的内核几十兆是icudtl.datICU 字符集数据缺了中文全变方块是*.pakresources / chrome_100_percent 等内核界面与资源包是v8_context_snapshot.binV8 快照旧分支里叫snapshot_blob.bin是locales/*.pak语言包至少留zh-CN.pak、en-US.pak是swiftshader/软件渲染回退老显卡没有可用 OpenGL 时靠它是打包发布时最容易漏icudtl.dat和locales本地调试正常装到客户机上白屏日志里只有一句 resource not found。2.2 VS2010 工程配置include、lib 与运行时库必须对齐配置项摊开说改动在项目属性的三处配置项值说明附加包含目录$(ProjectDir)cef\include别用绝对路径组里换机器就断附加库目录$(ProjectDir)cef\lib放libcef.lib与自编的 wrapper附加依赖项libcef.lib;libcef_dll_wrapper.lib顺序无所谓但两个都要运行时库多线程 DLL (/MD或/MDd)CEF 官方二进制是/MD编的改成/MT会在初始化时崩堆字符集使用 Unicode 字符集CEF 全程 UTF-16/UTF-8多字节字符集下CString转CefString必乱码/MT那一项是最容易出问题的地方。表现是CefInitialize返回 true但一创建 browser 就0xC0000005堆栈落在libcef.dll里看不出任何业务代码。原因是主程序和 DLL 用了两套 CRT跨模块new/delete的堆不是同一个。真正难的是工具链匹配。CEF 90 的 wrapper 源码里大量使用std::unique_ptr、override、移动语义MSVC10 的 C 编译器基本编不过。我的做法是把 wrapper 单独用高版本编译器编成静态库对 VS2010 主工程只暴露一层纯 C 函数头文件里不出现任何 C 类。这样主工程照旧用 VS2010 编 MFC跨模块传的只有int、char*、wchar_t*。// cef_bridge.h —— 唯一允许 VS2010 主工程 include 的桥头文件 // 里面不能出现 CefRefPtr / std::string / 任何 STL 与 CEF 类型 #ifdef __cplusplus extern C { #endif // 返回值0 成功非 0 为错误码主工程只认这个 int __stdcall CefBridge_Start(void* parentHwnd, const wchar_t* url, const wchar_t* appDir); void __stdcall CefBridge_Resize(int width, int height); void __stdcall CefBridge_Shutdown(void); // JS 发起查询后Browser 进程把结果回灌给主工程处理 typedef void (__stdcall *CefQueryCallback)(int queryId, const char* utf8Json); void __stdcall CefBridge_SetHandler(CefQueryCallback cb); void __stdcall CefBridge_Reply(int queryId, const char* utf8Json); #ifdef __cplusplus } #endif参数说明上parentHwnd是 MFC 传下来的窗口句柄用void*接收再在桥内部转HWND避免主工程引入windows.h之外的 CEF 依赖url一律传file:///绝对路径或http://127.0.0.1:port相对路径在快捷方式启动的场景下解析出的工作目录是system32这是踩过的坑utf8Json必须 UTF-8主工程这一侧用CStringA中转不要直接用CStringW强转。如果团队能统一到高版本编译器直接用 wrapper 源码加进工程更省事上面的 C 桥就没有必要了。但只要存在「主工程不许动、只能加文件」的约束这层 C 桥就是唯一选择。2.3 目录结构让 MFC 代码和 CEF 代码物理分开MfcCefDemo/ ├─ app/ MFC 主工程VS2010/MD │ ├─ MainFrm.cpp/.h 主框架窗口CEF 只挂在这里 │ ├─ CefHost.cpp/.h CEF 生命周期薄封装不含业务 │ └─ Bridge.cpp/.h C 桥的调用方全部 try/catch 兜住 ├─ cef/ │ ├─ include/ CEF 头文件 │ ├─ lib/ libcef.lib / libcef_dll_wrapper.lib │ └─ bin/ libcef.dll、locales、*.pak、*.bin ├─ cefbridge/ 高版本编译器编的桥 DLL/静态库 └─ web/ 前端页面构建产物直接丢进来目录分开以后「CEF 升不升级」和「MFC 业务改不改」就是两条独立的线。业务侧的人只碰app/和web/内核升级只动cef/和cefbridge/。这个划分在后期维护里的价值远大于多出来的两个文件夹。3. 窗口嵌入一个 CefBrowser 从 CreateBrowser 到 MFC 子窗口的完整链路CEF3 是多进程架构Browser 进程是主程序本身Render 进程负责解析执行 JSGPU 进程负责合成。嵌进 MFC 时主程序里跑的是 Browser 进程窗口句柄也只在这一侧有效。理解这一点后面所有「拿不到窗口」「回调不在主线程」的问题都能自己推出来。3.1 初始化顺序CefExecuteProcess 必须在最前面CefExecuteProcess是子进程分支CEF 启动 Render 进程的方式是把主程序 exe 再拉一遍靠命令行参数区分。所以它必须出现在_tWinMain的第一行在任何 MFC 窗口、任何全局对象构造之前。// MfcCefDemo.cpp —— 进程入口顺序错一步后面全白屏 int APIENTRY _tWinMain(HINSTANCE hInst, HINSTANCE, LPTSTR lpCmdLine, int nCmdShow) { CefMainArgs mainArgs(hInst); // 关键这是子进程分支。返回 0 说明当前进程是 Render/GPU 进程 // 处理完就该退出绝不能继续往下走 MFC 的初始化 CefRefPtrCMfcCefApp app new CMfcCefApp(); int exitCode CefExecuteProcess(mainArgs, app.get(), nullptr); if (exitCode 0) { return exitCode; } // 到这里才是真正的 Browser 进程 CWinApp theApp; if (!AfxWinInit(hInst, nullptr, ::GetCommandLine(), 0)) { return 1; } return theApp.Run(); }参数说明CefMainArgs在 Windows 上就是HINSTANCE与 MFC 的AfxGetInstanceHandle()是同一个值CefExecuteProcess的第三个参数是CefRefPtrCefRequestContext普通场景传nullptr。如果这一行忘了写或者写在AfxWinInit之后现象是主窗口正常出来但页面一直转圈日志里反复出现「The renderer process could not be launched」。初始化参数集中在CefSettings每一行都对应一个具体的坑void CMfcCefApp::InitCef(const CString appDir) { CefSettings settings; settings.no_sandbox true; // 老环境权限受限开沙箱会启动失败 settings.multi_threaded_message_loop true; // CEF 自己跑消息循环MFC 不用喂泵 settings.log_severity LOGSEVERITY_INFO; settings.windowless_rendering_enabled false; // 子窗口模式必须关 settings.remote_debugging_port 9222; // 调试期打开见第6章 // 路径全部拼绝对路径。相对路径在计划任务/服务方式启动下必错 CefString(settings.browser_subprocess_path).FromWString(appDir L\\MfcCefDemo.exe); CefString(settings.resources_dir_path).FromWString(appDir L\\cef\\bin); CefString(settings.locales_dir_path).FromWString(appDir L\\cef\\bin\\locales); CefString(settings.cache_path).FromWString(appDir L\\cache); CefString(settings.locale).FromASCII(zh-CN); if (!CefInitialize(CefMainArgs(AfxGetInstanceHandle()), settings, this, nullptr)) { AfxMessageBox(_T(CEF 初始化失败检查 libcef.dll 与 bin 目录是否完整)); } }multi_threaded_message_loop是真的省事但它带来一个副作用所有 CEF 回调都不在 UI 线程上。回调里直接碰 MFC 控件或CWnd成员必然出问题得用PostMessage或std::function投递回主线程。这一条后面还会提到。3.2 SetAsChild把 CEF 的子窗口挂到 MFC 的 HWND 下面CefWindowInfo有两种用法一种是顶层窗口CEF 自己造窗口一种是子窗口挂在别人下面。MFC 嵌套要的是后者一个函数就够void CCefHost::Create(HWND hParent, const CRect rc, const CString url) { CefWindowInfo info; // SetAsChild 之后CEF 会在 hParent 下建一个自己的子 HWND // 这个 HWND 才是页面真正绘制的地方后面 resize 拿到的是它 info.SetAsChild(hParent, CefRect(rc.left, rc.top, rc.Width(), rc.Height())); CefBrowserSettings bs; bs.javascript STATE_ENABLED; bs.local_storage STATE_ENABLED; // 前端要存表单草稿之类的本地数据 bs.webgl STATE_ENABLED; bs.image_loading STATE_ENABLED; bs.javascript_close_windows STATE_DISABLED; // 不让页面自己关窗口 // CEF 90 的签名带 extra_info 和 request_context普通场景都传 nullptr CefBrowserHost::CreateBrowser(info, m_client, CefString(url), bs, nullptr, nullptr); }参数上要注意CefRect用的是父窗口客户区坐标不是屏幕坐标。如果 MFC 那边传的是GetWindowRect的结果页面会整体偏移到父窗口外面去现象是「窗口里有块白色的地方但看不到内容」。m_client是CefClient的实现类它挂着一堆 handler生命周期、加载、消息、JS 绑定是整个嵌入方案的核心对象后面几章会逐个展开。创建是异步的CreateBrowser返回时 browser 对象还不存在。真正的对象在CefLifeSpanHandler::OnAfterCreated里拿到那里才是可以安全调用GetHost()的时机void CCefLifeSpanHandler::OnAfterCreated(CefRefPtrCefBrowser browser) { // 此时 browser 才有效把它交给宿主保存 m_owner-m_browser browser; if (!m_owner-m_messageRouter) { // Browser 进程侧的 router 在这里建生命周期跟 browser 对齐 CefMessageRouterConfig cfg; cfg.js_query_function cefQuery; cfg.js_cancel_function cefQueryCancel; m_owner-m_messageRouter CefMessageRouterBrowserSide::Create(cfg); m_owner-m_messageRouter-AddHandler(new CQueryHandler(m_owner), true); } }AddHandler的第二个参数true表示这是第一个匹配的 handler可以拦截消息如果同一套接口后面还要挂别的 handler就得考虑false和顺序问题。3.3 尺寸跟随WM_SIZE 里改的是 CEF 的子 HWNDMFC 的OnSize拿到的是父窗口新尺寸要同步给 CEF 的子窗口。窗口模式下不是调WasResized()那是无窗口渲染模式的接口而是直接对子 HWND 做SetWindowPosvoid CMainFrame::OnSize(UINT nType, int cx, int cy) { CFrameWnd::OnSize(nType, cx, cy); if (m_cef.m_browser m_cef.m_browser-GetHost()) { HWND hwnd m_cef.m_browser-GetHost()-GetWindowHandle(); if (hwnd ! nullptr) { // 不激活、不改 Z 序只改大小否则会抢焦点导致输入框失焦 ::SetWindowPos(hwnd, nullptr, 0, 0, cx, cy, SWP_NOZORDER | SWP_NOACTIVATE); } } }如果这台机器是高分屏还要额外通知一次屏幕信息否则页面按 96 DPI 渲染字会糊void CMainFrame::OnDpiChanged() // 由 WM_DPICHANGED 触发 { if (m_cef.m_browser m_cef.m_browser-GetHost()) { m_cef.m_browser-GetHost()-NotifyScreenInfoChanged(); } }老机房多数是 100% 缩放这行代码平时不显眼换到一台 125% 的笔记本上就是救命的那一行。3.4 消息循环两种集成方式怎么选MFC 有自己的CWinApp::Run消息泵CEF 也需要泵来派发任务两者只能有一个主导。两种做法我都用过差别很实际方式配置优点代价CEF 自带线程multi_threaded_message_loop true一行配置搞定不用改 MFC 的 Run所有回调在非 UI 线程碰控件要投递消息外部泵external_message_pump true 定时器里调CefDoMessageLoopWork()回调都在主线程直接操作控件要自己控节奏间隔太长会卡顿太短吃 CPU采集类程序一般选后者更稳因为业务代码里到处是直接改控件的地方改成投递消息要改一大片。做法是在主窗口挂一个 10 到 15 毫秒的SetTimervoid CMainFrame::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent IDT_CEF_PUMP) { // 外部泵模式MFC 空转时帮 CEF 派发一次任务 CefDoMessageLoopWork(); } CFrameWnd::OnTimer(nIDEvent); }注意这个定时器只在external_message_pump true时才需要两个模式混用会导致任务被派发两次现象是页面偶发重绘闪烁、按钮点一次触发两次。3.5 关闭顺序先关 browser再 Shutdown退出崩溃是这套方案里出现频率最高的问题。正确的顺序是四步主窗口OnClose里调CloseBrowser(true)等OnBeforeClose回调把m_browser置空最后调CefShutdown()。void CCefLifeSpanHandler::OnBeforeClose(CefRefPtrCefBrowser browser) { // 引用必须在这里置空。放在 DoClose 里置空会导致析构过早 // 后面 CEF 内部还会访问这个对象直接访问违例 m_owner-m_browser nullptr; m_owner-m_closeEvent.SetEvent(); // 通知主线程可以往下走了 } void CMainFrame::OnClose() { if (m_cef.m_browser) { m_cef.m_browser-GetHost()-CloseBrowser(true); // true 表示允许在关闭前执行 JS ::WaitForSingleObject(m_cef.m_closeEvent, 3000); // 最多等 3 秒别无限等 } CefShutdown(); // 必须在 browser 全部关闭之后 CFrameWnd::OnClose(); }CloseBrowser(true)的那个true是force_close表示不询问页面直接关。生产环境里前端如果挂了beforeunload传false会一直等用户确认用户把窗口关了之后进程反而退不出去。4. JS 调 C从 cefQuery 到 Render 进程消息路由的完整链路MFC 壳搭好只是第一步真正干活的是双向调用。前端要点一个按钮去查设备序列号或者打开一份报表数据在 C 这边中间隔着进程边界。CEF 提供了三条路选错了后面全是坑。4.1 为什么主链路选 CefMessageRouter 而不是裸 V8 Handler三条路的差别用一张表说清楚方案调用形态跨进程适合场景CefMessageRoutercefQuery异步回调自动处理90% 的业务请求尤其是要访问 MFC 数据的CefV8Handler自定义对象同步返回值需自己SendProcessMessage前端要拿到值立即参与计算且数据在 Render 进程内可得CefRegisterExtension注入全局 JS 函数需自己处理老代码改造必须保持原有函数签名主链路我选第一条。原因是cefQuery天然异步、天然跨进程、天然带请求 ID前端拿 Promise 包一层就很好用CefV8Handler那个同步返回值看着舒服实际只能返回 Render 进程里已有的数据想拿 MFC 里的硬件信息还是得发消息返回时数据还没到最后只能返回一个占位值第一次用的人基本都会在这翻车。4.2 Browser 端 HandlerOnQuery 里做参数校验和分流OnQuery是所有 JS 请求的入口这里做两件事校验参数、决定同步回包还是异步回包。class CQueryHandler : public CefMessageRouterBrowserSide::Handler { public: explicit CQueryHandler(CCefHost* owner) : m_owner(owner) {} bool OnQuery(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, int64 queryId, const CefString request, bool persistent, CefRefPtrCallback callback) override { // request 是前端传的原始字符串约定用 JSON编码必须是 UTF-8 const std::string req request.ToString(); const std::string action JsonPeek(req, action); if (action getDeviceInfo) { // 同步就能拿到结果直接 Success 回包 callback-Success(BuildDeviceInfoJson()); return true; // true 已处理不再往下传 } if (action exportReport) { const std::string path JsonPeek(req, path); // 路径校验放在 C 侧前端传什么都不可信 if (path.empty() || path.find(..) ! std::string::npos) { callback-Failure(403, path not allowed); return true; } // 耗时操作必须异步否则阻塞这一帧页面表现为点了没反应 m_owner-PostExportTask(queryId, path, callback); return true; } if (action getHardwareCode) { // 走这条的是需要在别处复用的数据先查缓存 callback-Success(m_owner-GetCachedHardwareCode()); return true; } return false; // false 会让 router 回一个 NOT_IMPLEMENTED 给前端 } private: CCefHost* m_owner; };几个参数的门道queryId是 router 分配的请求序号异步回包时必须原样带上否则前端收到的结果对不上号persistent为 true 表示前端用的是cefQueryCancel可取消的持续查询只要不是做事件订阅一般忽略它Failure的错误码不要用负数CEF 内部保留了一部分负数错误码用 400 起的区间和 HTTP 状态码对齐日志里好认。4.3 前端封装别让业务代码直接碰 cefQuery裸调window.cefQuery的代码长这样回调套回调参数是字符串拼接出错只有一个 code。页面一多就没人愿意写。包一层 Promise 之后顺手很多// web/bridge.js —— 全站唯一的原生调用出口 function callNative(action, payload) { return new Promise((resolve, reject) { // 页面可能在 CEF 还没注入 router 时就开始执行这里必须判空 if (typeof window.cefQuery ! function) { return reject(new Error(native bridge not ready)); } const request JSON.stringify(Object.assign({ action: action }, payload || {})); window.cefQuery({ request: request, onSuccess: function (resp) { // C 侧统一回 JSON解析失败就当纯文本处理别让页面白屏 try { resolve(JSON.parse(resp)); } catch (e) { resolve(resp); } }, onFailure: function (code, msg) { reject(new Error(code : msg)); } }); }); } // 组件里这样用看起来就跟普通接口请求一样 // const info await callNative(getDeviceInfo);request里除了业务字段action是必填的分发键。约定好这个字段之后服务端加接口只需要在OnQuery里多一个分支前端不用改封装层。另外JSON.stringify出来的中文是 UTF-8 直出CefString::ToString()拿到的也是 UTF-8两边正好对上不需要额外转码。4.4 另一条路用 CefV8Handler 在 Render 进程挂原生对象有些场景前端就是要一个同步的全局对象比如window.nativeApi.getVersion()立即拿到版本号拼到请求头里。这时候用CefV8Handler但必须清楚它跑在哪个进程。// CefRenderApp.cpp —— 这段代码运行在 Render 进程摸不到 MFC 的 HWND void CRenderApp::OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) { CefRefPtrCefV8Value global context-GetGlobal(); CefRefPtrCefV8Value api CefV8Value::CreateObject(nullptr, nullptr); CefRefPtrNativeBridge handler new NativeBridge(); // 挂一个只读的 window.nativeApi api-SetValue(getVersion, CefV8Value::CreateFunction(getVersion, handler), V8_PROPERTY_ATTRIBUTE_NONE); global-SetValue(nativeApi, api, V8_PROPERTY_ATTRIBUTE_READONLY); } bool NativeBridge::Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList args, CefRefPtrCefV8Value retval, CefString exception) { if (name getVersion) { // 版本号在启动时通过命令行参数 or 消息同步过来的这里直接给 retval CefV8Value::CreateString(1.0.0); return true; } if (name requestHardware) { // 需要 MFC 数据的只能发进程消息结果从 window 回调回来 CefRefPtrCefProcessMessage msg CefProcessMessage::Create(req_hardware); browser-GetMainFrame()-SendProcessMessage(PID_BROWSER, msg); retval CefV8Value::CreateBool(true); // 这里只能表示已发出 return true; } return false; }OnContextCreated是在CefRenderProcessHandler上实现的不是CefClient。这一点很多人第一次找半天找不到。SendProcessMessage的PID_BROWSER表示发往主进程主进程在CefClient::OnProcessMessageReceived里收收完再想办法把结果送回页面。绕这一圈的成本就是前面说主链路选cefQuery的原因。4.5 C 反向调 JSExecuteJavaScript 的时机比写法重要采集到数据要推给页面用的是ExecuteJavaScript。写法很简单难在什么时候调。void CCefHost::PushToJs(const CString json) { if (!m_browser || !m_browser-GetMainFrame()) { return; } // json 必须是合法 JSON字符串里的双引号要提前转义 // 否则下面拼出来就是语法错误控制台一片红但业务代码毫无反应 CString script; script.Format(_T(window.__onNativeData window.__onNativeData(%s);), json); // 第三个参数是脚本 URL排查时能从控制台看出来源0 表示不等待结果 m_browser-GetMainFrame()-ExecuteJavaScript(CefString(script), CefString(Lnative://push), 0); }window.__onNativeData 这半句是必须的。页面可能还在加载函数还没定义直接调会抛undefined is not a function而且这个错误只在页面控制台里出现C 侧一点感觉没有。同理不要在OnLoadStart里推数据要在OnLoadEnd之后或者由前端在mounted之后主动cefQuery(ready)报个到C 收到报到再开始推。后者更可靠我现在的项目全用这种方式。5. 排查与避坑VS2010 配 CEF3 时最容易翻车的五处这一节的内容全是踩出来的按发生阶段分。每条都是「现象 → 原因 → 解决」遇到对应现象直接对照。5.1 编译链接阶段第一处wrapper 编不过满屏 C11 报错。现象把libcef_dll源码加进 VS2010 工程编译报std::unique_ptr未定义、override无法识别、std::move找不到几百条错误。原因CEF 90 对应的 Chromium 90 分支早就切到 C14 了wrapper 源码里大量使用移动语义和overrideMSVC10 的编译器不支持不是配置问题改stdafx.h也没用。解决不要试图在 VS2010 里编 wrapper。用高版本编译器单独编出一个静态库对它写一层extern C接口见 2.2VS2010 主工程只链接那个库不接触任何 CEF 的 C 类型。如果项目允许换编译器那更简单直接统一到高版本但要注意 MFC 那部分代码里的老 API 可能需要小改。第二处链接时报一堆unresolved external symbol符号名里带CefRefPtr。现象编译过了链接失败错误里全是cef::CefRefPtr...相关的未解析符号。原因libcef.lib只是导入库真正的 C 封装在libcef_dll_wrapper.lib里只加前者不够或者加了后者但两边运行时库不一致一个/MD一个/MT链接器直接忽略。解决确认附加依赖项里两个 lib 都在把主工程和 wrapper 的运行时库都设成「多线程 DLL」用dumpbin /directives libcef_dll_wrapper.lib看一下里面的/DEFAULTLIB标记能在符号层面确认到底是不是/MD。5.2 进程与资源第三处本地跑得好好的装到客户机白屏日志只有一行 resource not found。现象开发机上一切正常拷到现场机器窗口出来了但内容是白的debug.log里只有Failed to load resource之类的记录。原因发布目录只拷了 exe 和libcef.dllicudtl.dat、*.pak、locales/全漏了。CEF 找不到这些文件不会弹窗只会静默降级成白页。解决把发布目录列成检查清单libcef.dll、icudtl.dat、全部*.pak、v8_context_snapshot.bin、locales/zh-CN.pak、swiftshader/一个不能少同时把CefSettings.resources_dir_path和locales_dir_path显式写成绝对路径不依赖默认搜索逻辑。工程里加一个启动自检缺文件直接弹框提示缺哪个比到现场排查省太多时间。第四处主窗口一闪就没了日志里渲染进程起不来。现象双击 exe进程出现几百毫秒然后消失日志写The renderer process could not be launched。原因CefExecuteProcess没写在_tWinMain的最前面或者browser_subprocess_path没设CEF 默认拿CefMainArgs里的模块路径去拉子进程而那个路径在某些启动方式下不是 exe 自己。解决两件事一起做——把CefExecuteProcess挪到_tWinMain第一行CefSettings.browser_subprocess_path显式指向自己的 exe 绝对路径。另外把no_sandbox打开受限账户下沙箱初始化失败也是同样的现象。5.3 交互与退出第五处点关闭按钮偶发崩溃堆栈落在 CefShutdown 里。现象十次里有那么一两次点右上角关闭程序卡一下然后弹出「已停止工作」dump 里堆栈最顶层是libcef.dll!CefShutdown。原因两个可能一是m_browser的引用没在OnBeforeClose里置空CefShutdown执行时 CEF 内部还在访问已经析构的对象二是 MFC 主窗口先销毁子 HWND 跟着没了但 browser 还活着。解决关闭顺序严格按 3.5 那四步走m_browser只在OnBeforeClose里置空CloseBrowser之后用一个事件等最多 3 秒再CefShutdown。等不到也别硬等先写日志再退出至少能留下现场。另外页面里如果有setInterval在跑关闭前最好让它停掉渲染进程在收尾时会更快。6. 二次开发把示例工程改成能长期维护的 CefWindow 封装示例工程能跑起来和能维护中间隔着一层封装。我把它拆成四件事。第一件是把 CEF 相关的代码从CMainFrame里抽干净做成一个不依赖 MFC 的CCefHost类。它只认HWND和CRect不认CWnd。这样以后要是换成 Win32 或者别的壳这个类能直接搬走。MFC 那一侧只保留三行创建、OnSize转发、关闭。第二件是把OnQuery里的 if-else 换成路由表。现在一个action一个分支接到二十个接口就没人看得懂了。做法是维护一个std::mapstd::string, std::functionvoid(const std::string, CefRefPtrCallback)注册时一行一个加接口不用改OnQuery。改造项现状建议请求分发OnQuery里 if-else路由表 注册函数单文件不超过 200 行桥接口直接 include CEF 头extern C隔离主工程不感知 CEF 类型接口契约前后端各自记忆一份 JSON 字段表放进web/docs/字段改名两边同改崩溃恢复渲染进程挂了就白页OnRenderProcessTerminated里重建 router 并Reload()第三件是补上渲染进程崩溃的兜底。Chromium 的渲染进程不是不会挂本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑