资讯动态

VS2010 MFC集成CEF3 90.6.0:双向通信与避坑实践

发布时间:2026/10/29 15:08:46 来源:尧图企业网站定制
简介面向使用 VS2010 开发桌面程序的 C 开发者这份资料围绕 CEF3 90.6.0 在 MFC 窗口中的嵌入与 JavaScript 互调展开适合需要为传统 MFC 项目补充网页渲染、前端脚本与原生代码通信能力的场景也可作为 Chromium 内核集成的实操参考。压缩包共 629 个文件约 457.22MB含 269 个头文件、168 个 pak 资源包、28 个动态库以及工程文件、静态库、编译中间产物和少量源码覆盖 CEF 运行时依赖与 VS 工程构建所需的主要目录组成。已有 1558 人学习说明该主题在 MFC 与 CEF 结合的实际开发中较受关注。读者可获得可打开的解决方案与构建配置理解 CEF 渲染窗口接入 MFC 视图、消息循环协同、JavaScript 调用 C 的工程组织方式并借助现有库文件与资源快速复现实验环境、排查依赖缺失和初始化参数配置问题。1. 在 VS2010 里嵌入 CEF3这套 90.6.0 版 MFC 工程能省掉哪些无用功手上还有 VS2010 工程要维护的人多半遇到过这种局面产品主体是 MFC 桌面程序界面里偏偏要塞一块网页——可能是内嵌的报表页、本地渲染的 HTML 帮助文档或者一个用前端栈写好的配置面板。想用浏览器控件CHtmlView走的是 IE 内核现代 CSS 和 ES6 基本没戏想换 CEF3打开官方文档一看编译工具链、libcef.dll 部署、C 与 JS 双向调用每一项都能单独耗掉一两天。这套基于 VS2010 CEF3 90.6.0 的工程做的就是把这套流程完整走通并留出一个能照着改的 MFC 窗口骨架。C、CEF3、VS2010 三者的版本匹配是这类集成最大的门槛下面把它拆开说明适合需要在不升级主工程的前提下给老 MFC 程序补上现代 Web 渲染能力的开发者。2. 版本匹配与工程结构为什么 90.6.0 配 VS2010 要格外小心CEF3 的发布节奏和 Chromium 绑定每个版本号背后都是一个确定的 Chromium 分支。90.x 对应的 Chromium 内核年代官方构建产物默认已经切到 VS2019 工具链而 VS2010 只支持到 v100 平台工具集C11 特性支持残缺。这意味着“拿官方二进制直接丢进 VS2010 工程”这条路走不通必须先把下面几件事想清楚否则链接期会给你一连串莫名其妙的问题。2.1 libcef 的 ABI 与 CRT 静态/动态链接CEF3 的 C 封装层include/cef_*.h本身是纯头文件加一层薄封装真正的实现在libcef.dll里。跨 DLL 边界传递对象时最怕的就是 CRT 不一致一边用/MT静态链接另一边用/MD堆内存的归属就乱了典型表现是程序退出时在operator delete里崩溃或者CefRefPtr析构时访问违例。常见做法是让 MFC 工程与 CEF3 的 wrapper 保持同一套运行库设置。VS2010 里 MFC 默认是“共享 DLL 中使用 MFC”对应/MD那就要求libcef_dll_wrapper也用/MDRelease和/MDdDebug编译。如果工程历史上是/MT要么改工程属性要么把 wrapper 一起重编成/MT两边必须一致。配置项建议值说明运行库Release多线程 DLL (/MD)与 libcef.dll 官方构建保持一致运行库Debug多线程调试 DLL (/MDd)不要与 Release 混链平台工具集v100VS2010 唯一可选字符集UnicodeCEF3 接口全部走CefString宽窄混用会出乱码目标平台Win32 / x64 二选一必须与 libcef.dll 位数一致这里最需要注意的一点是位数。很多人拿到一份 x64 的 libcef 资源却在 Win32 配置下编译报错信息会是“模块计算机类型 x86 与目标计算机类型 x64 冲突”看起来像工具链问题实际是位数错配。改配置之前先确认libcef.dll的文件属性。2.2 目录布局与依赖文件的放置工程能不能一次性跑起来很大程度取决于目录布局是否规整。CEF3 运行时需要的不只是 dll还有locales/、resources/、swiftshader/等资源目录缺一个就可能白屏或直接退出。推荐的布局如下把二进制和源码分开后面换版本时只动cef_binary这一层。MyMfcApp/ ├── MyMfcApp.sln ├── MyMfcApp/ # MFC 主工程 │ ├── MyMfcApp.cpp │ ├── CefBrowserHost.cpp # 封装的浏览器宿主窗口 │ └── ... ├── cef_binary/ # CEF3 90.6.0 解压目录 │ ├── include/ # 头文件 │ ├── lib/ # libcef.lib / libcef_dll_wrapper.lib │ ├── bin/ # libcef.dll 及资源目录 │ └── libcef_dll_wrapper/ # wrapper 源码工程 └── build_out/ # 最终输出目录exe dll 资源bin/下的内容在部署时要整目录拷到 exe 旁边不是只拷libcef.dll。locales里如果只有en-US.pak中文界面会显示成方块icudtl.dat缺失则可能在任何涉及字符串编码的地方崩掉。这类问题排查起来很费时间因为崩溃点离真正的原因很远。2.3 主工程的编译与链接配置MFC 主工程需要显式做三件事加头文件搜索路径、加 lib 搜索路径、链接libcef.lib和libcef_dll_wrapper.lib。在 VS2010 里右键工程 → 属性按配置逐项设置。要注意 Debug 和 Release 两套配置都要设只设一套是高频失误。// MyMfcApp.cpp - 入口处的 CEF 初始化节选 #include include/cef_app.h #include include/cef_browser.h #include include/cef_client.h // 自定义 App负责把浏览器进程参数交给 CEF class CSimpleApp : public CefApp, public CefBrowserProcessHandler { public: CSimpleApp() {} // 返回浏览器进程处理器CEF 启动时会回调 virtual CefRefPtrCefBrowserProcessHandler GetBrowserProcessHandler() OVERRIDE { return this; } // 子进程渲染、GPU 等的启动参数在这里定制 virtual void OnBeforeCommandLineProcessing( const CefString process_type, CefRefPtrCefCommandLine command_line) OVERRIDE { // 老机器上关闭 GPU 合成能规避一部分白屏问题 command_line-AppendSwitch(disable-gpu); command_line-AppendSwitch(disable-gpu-compositing); } IMPLEMENT_REFCOUNTING(CSimpleApp); }; // MFC 应用初始化必须在主窗口创建前完成 BOOL CMyMfcApp::InitInstance() { CWinApp::InitInstance(); // 初始化 CEFargc/argv 从进程命令行取 CefMainArgs main_args(::GetModuleHandle(NULL)); CefRefPtrCSimpleApp app(new CSimpleApp()); // 返回值 0 说明当前进程是子进程应立即退出 int exit_code CefExecuteProcess(main_args, app.get(), NULL); if (exit_code 0) { return FALSE; } CefSettings settings; settings.multi_threaded_message_loop false; // 与 MFC 消息循环共用主线程 settings.no_sandbox true; // 老工程常关沙箱见避坑章 CefString(settings.locales_dir_path) Llocales; CefString(settings.resources_dir_path) Lresources; CefInitialize(main_args, settings, app.get(), NULL); // ... 正常创建主窗口 ... return TRUE; }这段逻辑里CefExecuteProcess的返回值判断是关键CEF 会把自己分裂成多个进程子进程启动时走的还是同一个 exe靠这个返回值区分角色不判断就会看到多个主窗口弹出。multi_threaded_message_loop false表示 CEF 不单独开 UI 线程而是把消息泵接进 MFC 的Run()这对 MFC 集成是必须的否则界面线程会打架。no_sandbox true在老系统上能避开一部分启动失败但代价是安全性下降如果目标环境可控可以接受。对应的卸载在ExitInstance里顺序不能反先关浏览器窗口再CefShutdown()最后才退 MFC。int CMyMfcApp::ExitInstance() { CefShutdown(); // 必须在所有浏览器窗口销毁之后 return CWinApp::ExitInstance(); }如果程序退出时崩在CefShutdown里八成是还有CefRefPtr没释放或者浏览器窗口没走正常的DestroyWindow。这是这套机制里最容易出问题的一步。3. 把 CEF3 浏览器嵌进 MFC 窗口宿主控件与生命周期初始化通了只是拿到入场券真正难的是让浏览器“长”在一个 MFC 窗口里而不是弹一个独立的顶层窗。CEF3 的做法是创建一个实现了CefWindowInfo描述的子窗口父句柄设成 MFC 控件的HWND然后把窗口大小、位置和 MFC 布局同步。这一步做不好要么浏览器盖在所有控件上面要么拖动窗口大小时浏览器纹丝不动。3.1 宿主窗口类的实现我一般会写一个薄封装类继承CWnd内部持有CefRefPtrCefBrowser和一个CefClient实现。MFC 侧只管创建与布局浏览器侧只管渲染与消息两边通过HWND对接。// CefBrowserHost.h - MFC 宿主窗口 #pragma once #include afxwin.h #include include/cef_client.h #include include/cef_browser.h class CCefBrowserHost : public CWnd { public: CCefBrowserHost(); virtual ~CCefBrowserHost(); // 在指定父窗口中创建浏览器url 为初始页面 BOOL CreateBrowser(CWnd* pParent, const CRect rc, const CString url); // 让浏览器跟随 MFC 布局变化 void ResizeTo(const CRect rc); // 页面前端调用的 C 侧入口 void CallJs(const CString script); protected: afx_msg void OnSize(UINT nType, int cx, int cy); DECLARE_MESSAGE_MAP() private: CefRefPtrCefBrowser m_browser; CefRefPtrCefClient m_client; };CreateBrowser里最核心的是CefWindowInfo的构造BOOL CCefBrowserHost::CreateBrowser(CWnd* pParent, const CRect rc, const CString url) { // 先创建一个普通 MFC 静态窗口作为容器避免句柄竞争 if (!CWnd::CreateEx(0, _T(STATIC), NULL, WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, rc, pParent, 0x1001)) { return FALSE; } CefWindowInfo info; // 父句柄必须是有效的子窗口句柄 info.SetAsChild(GetSafeHwnd(), CefRect(0, 0, rc.Width(), rc.Height())); CefBrowserSettings browser_settings; browser_settings.windowless_frame_rate 30; // 无窗口模式才生效 // 默认启用 JS如需禁用改这里 browser_settings.javascript STATE_ENABLED; m_client new CSimpleClient(this); CefBrowserHost::CreateBrowser(info, m_client.get(), CefString(url), browser_settings, NULL); return TRUE; }SetAsChild的父句柄和尺寸决定了浏览器子窗口的初始位置。这里踩过最多的坑是尺寸传 0rc还没被 MFC 真正布局过就调用浏览器会以一个极小窗口创建之后再ResizeTo也拉不回来。稳妥的做法是在OnCreate之后、OnSize首次触发时再创建浏览器或者至少传一个与父窗口客户区一致的初始矩形。OnSize里同步尺寸void CCefBrowserHost::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); if (m_browser) { // 通知 CEF 调整宿主子窗口坐标是相对本窗口客户区的 HWND hwnd m_browser-GetHost()-GetWindowHandle(); if (hwnd) { ::SetWindowPos(hwnd, NULL, 0, 0, cx, cy, SWP_NOZORDER | SWP_NOACTIVATE); } } }SWP_NOACTIVATE很重要否则每次拖窗口都会抢焦点用户正在输入的表单会失焦。这是很常见的体验问题出问题时又不容易联想到窗口标志。3.2 渲染模式的取舍窗口式还是离屏CEF3 提供两种嵌入方式。窗口式SetAsChild把浏览器作为真正的子窗口渲染由 GPU 进程完成性能好、滚动流畅缺点是它和 MFC 控件的 Z 序处理比较麻烦控件重叠时总在浏览器之下或被遮住。离屏渲染SetAsWindowless则要自己接收OnPaint回调把位图贴到 MFC 的 DC 上灵活但代码量大还要处理输入事件转发。对于“在对话框里塞一块网页”的常见场景窗口式就够了实现成本低。只有当你需要在网页上面浮一个 MFC 控件比如自定义标题栏、悬浮工具条时才值得上离屏。判断标准很简单如果 MFC 侧不需要盖在网页上面选窗口式需要盖就做好自己处理鼠标键盘消息转发的准备那是一个独立的工作量级别。3.3 页面加载完成前别急着做布局有个反直觉的结论浏览器对象创建成功后GetWindowHandle()拿到的句柄可能还不是最终尺寸页面也还没开始渲染。如果在OnAfterCreated里立刻调用 JS 注入或ResizeTo前者容易因为帧还没建好而丢消息后者会得到一个错误的矩形。稳妥顺序是在CefLoadHandler::OnLoadEnd之后再做与页面内容相关的操作尺寸同步则放在CefLifeSpanHandler::OnAfterCreated里做一次之后交给OnSize维护。// CSimpleClient 中的生命周期回调 class CSimpleClient : public CefClient, public CefLifeSpanHandler, public CefLoadHandler { public: // 浏览器创建完成此时窗口句柄有效 virtual void OnAfterCreated(CefRefPtrCefBrowser browser) OVERRIDE { m_browser browser; // 首次同步一次尺寸后续由 MFC 的 OnSize 接管 m_host-ResizeTo(m_host-GetClientRect()); } // 页面加载结束此时再执行 JS 注入最稳 virtual void OnLoadEnd(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, int httpStatusCode) OVERRIDE { if (frame-IsMain()) { // 通知页面 CEF 侧已就绪 frame-ExecuteJavaScript(window.__cefReady window.__cefReady();, frame-GetURL(), 0); } } IMPLEMENT_REFCOUNTING(CSimpleClient); };frame-IsMain()的过滤不能省。页面里的 iframe 也会触发OnLoadEnd不过滤就会出现一个页面加载完被通知好几次的情况前端如果监听这个信号做初始化逻辑就会重入。4. C 与 JS 双向调用消息路由的设计与实现双向通信是这类集成的核心需求也是最容易写乱的部分。CEF3 在 90.x 上提供了两条路一条是老式的window.cefQueryCefMessageRouter一条是较新的CefV8Handler注册原生函数。两条路能解决的问题高度重叠但工作量和调试难度差很多。4.1 路由方案的选择方案接入成本调试难度适合场景CefMessageRouter (cefQuery)低官方有现成实现低请求响应式天然异步页面发起请求、C 返回结果CefV8Handler 自定义对象中需要写 V8 绑定中出错时信息少需要同步返回或暴露多个方法JS 扩展注入低高脚本注入时机难控老工程快速打通大部分项目用CefMessageRouter就够了。它的模型是页面调window.cefQuery({request, onSuccess, onError})C 侧在OnQuery里处理并回调。天然带请求 ID 和异步回调适合“点页面上的按钮让 C 去干一件耗时的事”这种流程。自定义 V8 对象在需要window.nativeApi.getVersion()这种同步调用时更顺手但要自己管理函数注册和返回值构造出错时只有一句Uncaught Error。我一般先用CefMessageRouter把主链路打通只在确实需要同步返回的地方补 V8 绑定。4.2 CefMessageRouter 的注册与处理路由要挂在一个CefMessageRouterBrowserSide实例上这个实例通常在CefClient::GetMessageRouter()里返回并在浏览器销毁时清理。// 1) 创建路由器一般放在 CefClient 实现里 CefMessageRouterBrowserSide* g_router NULL; CefMessageRouterBrowserSide::Handler* g_handler NULL; void SetupMessageRouter() { CefMessageRouterConfig config; config.js_query_function cefQuery; // 页面里调用的函数名 config.js_cancel_function cefQueryCancel;// 取消函数名 g_router CefMessageRouterBrowserSide::Create(config); g_handler new CSimpleQueryHandler(); g_router-AddHandler(g_handler, true); // true 表示首处理器优先 } // 2) CefClient 里把它交给 CEF virtual CefRefPtrCefMessageRouterBrowserSide GetMessageRouter() OVERRIDE { return g_router; } // 3) 请求处理页面发起C 执行并回调 class CSimpleQueryHandler : public CefMessageRouterBrowserSide::Handler { public: virtual bool OnQuery(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, int64 query_id, const CefString request, bool persistent, CefRefPtrCallback callback) OVERRIDE { // request 是页面传过来的字符串通常用 JSON std::string req request.ToString(); if (req getVersion) { callback-Success(90.6.0); // 回传给 onSuccess return true; } if (req openLog) { // 处理完再返回注意这里不要阻塞太久 DoSomethingSlow(); callback-Success(ok); return true; } return false; // 返回 false 交给下一个 handler } };OnQuery的返回值语义要分清返回true表示“我认领了这个请求”必须调用一次callback-Success或callback-Failure否则页面那边的 Promise 永远挂着返回false表示不认领交给后续 handler。这看起来像常识但return true却忘了回调的情况非常常见表现是页面上按钮点了没反应控制台又不报错。persistent参数用于建立长连接式的查询适合需要持续推送的场景但用完必须显式 cancel否则路由会一直持有这个 query。一般的一次性请求传false就行。页面侧的调用// 页面中发起请求 function askNative(cmd) { return new Promise((resolve, reject) { if (!window.cefQuery) { reject(new Error(cefQuery 未注入检查 CEF 初始化是否成功)); return; } window.cefQuery({ request: cmd, onSuccess: function(response) { resolve(response); }, onError: function(code, msg) { reject(new Error(code : msg)); } }); }); } // 使用 askNative(getVersion).then(v { document.getElementById(ver).textContent 内核版本 v; }).catch(e console.error(e));用 Promise 包一层的好处是调用方不用关心 CEF 的回调风格错误也能统一处理。cefQuery未注入的检查不能省调试期见过太多次“页面逻辑没问题只是 CEF 没初始化成功”的情况。4.3 反向调用C 主动执行 JSC 侧需要主动通知页面时用CefFrame::ExecuteJavaScript。这条路不需要浏览器里预先注册任何东西直接执行一段脚本字符串。灵活但也意味着没有返回值、没有错误反馈脚本写错了页面毫无反应。// 通知页面C 侧某个状态变了 void CCefBrowserHost::NotifyJsStateChanged(const CString json) { if (!m_browser) return; CefRefPtrCefFrame frame m_browser-GetMainFrame(); if (!frame) return; // 拼接脚本时把 JSON 当作字符串字面量传入避免注入 CStringW script; script.Format(Lwindow.onNativeState window.onNativeState(%s);, (LPCWSTR)json); frame-ExecuteJavaScript(CefString(script), frame-GetURL(), 0); }第三个参数是起始行号调试时给它传真实行号能让页面控制台的报错定位准确一些平时传 0 无所谓。frame-GetURL()作为脚本的源 URL 传进去控制台里看到的就是页面自身的地址便于区分注入脚本和页面脚本。要注意的是ExecuteJavaScript是异步投递到渲染进程的调用返回不代表脚本已执行。如果后面紧跟一次页面跳转脚本可能丢失。需要严格顺序的场合用CefMessageRouter的 callback 机制或者让页面在OnLoadEnd之后自己来取状态。5. 避坑清单五个能把人耗到半夜的问题下面这些是我在这套版本组合上真实遇到过、并且能稳定复现的问题。每一条都按现象、原因、解决写遇到同类现象时可以直接翻过来对照。5.1 进程一启动就退出日志里只有两行现象InitInstance里调用完CefInitialize程序没有任何窗口直接结束任务管理器里也看不到残留进程。原因CefExecuteProcess的返回值判断写反了或者漏了。CEF 子进程启动时复用的是同一个 exeCefExecuteProcess会返回一个非负值并处理完子进程逻辑此时主进程应该立即返回不再继续建窗口。如果漏掉这个判断主进程逻辑在子进程里也跑了一遍行为就乱了。解决严格按上面第 2.3 节里的写法if (exit_code 0) return FALSE;放在CefInitialize之前。调试时可以在开头打一行process_type日志确认每个进程的角色。5.2 页面白屏但浏览器窗口边框正常现象宿主窗口创建成功能看到白色区域控制台没有任何错误HTML 就是不显示。原因按出现频率排第一是locales或icudtl.dat没有跟随 exe 部署第二是 GPU 合成在不支持的老显卡上失败第三是 URL 用了相对路径但工作目录不对。前两个都是静默失败不会弹出明显错误。解决先用绝对路径的file:///试一次在OnBeforeCommandLineProcessing里加上disable-gpu和disable-gpu-compositing确认bin目录下的资源文件整目录拷到了输出目录。三步依次排除一般能在十分钟内定位。5.3 退出程序时崩溃在 CefShutdown现象关闭主窗口瞬间程序消失调试器断在CefShutdown内部调用栈里能看到CefRefPtr的释放。原因还有未释放的浏览器对象或 client 引用。常见于主窗口OnClose里直接DestroyWindow而浏览器宿主的析构顺序排在CefShutdown之后或者某个 handler 上的循环引用没有断开。解决在ExitInstance里先遍历所有宿主窗口逐个DestroyWindow并置空引用再断言m_browser NULL最后才CefShutdown。接手别人的工程时习惯性在每个CefRefPtr成员上搜一遍赋值点看有没有自引用。5.4 JS 调用 C 时页面报 cefQuery is not defined现象前端代码一切正常浏览器控制台报cefQuery is not defined。原因消息路由没有挂上。可能是GetMessageRouter()返回了空可能是路由器创建了但没在正确的时机AddHandler还有一种容易忽略的情况页面在 iframe 里而路由只对主 frame 生效。解决先确认GetMessageRouter()在浏览器创建前就返回了有效实例再看SetupMessageRouter是否在主进程初始化流程里被调用iframe 场景下需要在该 frame 内单独注入或者改成postMessage转发到主 frame 再走cefQuery。5.5 Debug 配置能跑Release 崩溃现象开发机 Debug 下一切正常切 Release 编译能过运行时在浏览器创建中途崩。原因九成是运行库配置不一致。Debug 用的是/MDdRelease 用了/MT或者混用了静态链接的第三方库跨 DLL 的堆操作在 Release 优化后被暴露出来。另一种可能是libcef_dll_wrapper没跟着切配置链接到了错误版本的 lib。解决把主工程、wrapper 工程的 Release 运行库统一成/MD重新编译 wrapper清一次build_out再链接。养成切换配置时检查“链接器 → 输入 → 附加依赖项”里 lib 路径的习惯Debug 和 Release 的 lib 目录要分开。6. 进阶验证用最小化检查表把集成质量固定下来一套集成做完能跑起来不代表稳定。我后来养成一个习惯每次改完初始化代码或换 CEF 版本都按一张最小检查表走一遍五分钟能筛掉大部分隐患。这张表不复杂但覆盖了版本匹配、进程模型、通信链路和资源部署四个容易出问题的环节。检查项操作通过标准子进程识别启动后在任务管理器看进程数应为多进程且只有一个带主窗口资源完整性断网后打开本地 HTML页面正常渲染中文不乱码双向通信页面按钮触发 C 并回写结果结果在 200ms 内回到 DOM尺寸跟随拖动主窗口改变大小网页内容同步重排无留白退出清理反复开关 20 次无崩溃、无句柄泄漏具体怎么用这张表我一般会写一个极简的本地测试页放在build_out旁边页面里放三个按钮取版本号、触发一次 C 计算、请求一次状态推送。这样每次改完编译只要点三下就能确认链路是通的比翻日志快得多。!-- selftest.html - 放在 exe 同目录用 file:/// 打开 -- !DOCTYPE html html headmeta charsetutf-8title集成自检/title/head body div idout未开始/div button onclickcheckVersion()取版本/button button onclickcheckCalc()触发计算/button button onclickcheckPush()等待推送/button script const out document.getElementById(out); // 封装 Promise统一处理 cefQuery 的缺失和错误 function ask(cmd) { return new Promise((res, rej) { if (!window.cefQuery) return rej(new Error(路由未注入)); window.cefQuery({ request: cmd, onSuccess: res, onError: (c, m) rej(new Error(c : m)) }); }); } function checkVersion() { ask(getVersion).then(v out.textContent 版本 v) .catch(e out.textContent 失败 e.message); } function checkCalc() { ask(calc:12).then(v out.textContent 结果 v) .catch(e out.textContent 失败 e.message); } function checkPush() { // C 侧通过 ExecuteJavaScript 调用这个函数 window.onNativeState function(json) { out.textContent 推送 json; }; out.textContent 等待 C 推送…; } /script /body /html这个页面的价值在于把“通信是否双向都通”拆成了三个可独立验证的动作。取版本验证页面到 C 的调用链触发计算验证 C 里能执行逻辑并成功回调等待推送验证 C 到页面的反向注入。哪一步失败问题范围立刻缩小到对应的那一段代码不用在整条链路上盲猜。还有一点值得单独提build_out目录要当作“可丢弃产物”对待。把 exe、dll、资源全部由构建脚本拷进去每次换 CEF 版本先清空再重建不要手动往里面补文件。手动补的后果是半年后没人说得清哪个 dll 是哪来的换机器部署时必然出问题。这套 90.6.0 的工程我在两三个老项目上复用过每次都是先按上面的目录布局摆好再按检查表跑一遍翻车的次数比早期少了一个量级。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑