简介这份资源面向需要在VS2010环境下做桌面端Web集成的C开发者重点解决将CEF3 90.6.0嵌入MFC窗口并打通JavaScript与C双向调用的问题。包内共629个文件以269个h头文件、168个pak资源包、28个dll动态库、14个lib静态库及9个cpp源码为主另有obj、pdb、tlog等编译中间产物压缩包约457.22MB属于可直接编译运行的完整工程。资源包含BWQ.sln解决方案与vcxproj工程文件覆盖CEF运行时、头文件、库文件及MFC项目配置便于读者对照理解附加包含目录、附加依赖项、消息循环同步与CefMessageRouterBrowserSide等关键环节。目前已有1556人学习下载适合具备一定MFC与C基础、希望快速搭建CEF3嵌入环境并研究JS调用C实现思路的开发者参考。1. 从一次编译失败说起为什么 VS2010 配 CEF3 90.6.0 值得单独写一篇有个做工业上位机的朋友找我说他们项目卡在 VS2010 上动不了——客户现场一堆 XP 和 Win7 老机器升级工具链等于重做整个项目。现在需要把 CEF3 嵌进 MFC 窗口让本地页面能调 C 接口。他按网上教程折腾了两天要么编译报LNK2001要么程序起来直接闪退连窗口都出不来。这个场景其实很常见。VS2010 是 2010 年的工具链CEF3 90.6.0 是 2021 年的版本中间隔了十一年。用老编译器去接新版本的 Chromium 内核坑不在“能不能装”而在“装完能不能跑起来”。我前后帮三个团队处理过类似环境每次翻车点都不一样但根子就那么几个平台工具集选错、字符集没对齐、libcef.dll 放错位置、CefInitialize 参数没配对。这篇记录的是我最近一次完整走通的流程环境是 VS2010 SP1 CEF3 90.6.0 MFC 对话框程序目标是把浏览器嵌进窗口并且让页面里的 JS 能调到 C 侧的函数。如果你也在维护老项目、又必须用新内核可以照着走一遍。2. 环境准备VS2010 和 CEF3 90.6.0 的版本对齐2.1 确认 VS2010 的补丁级别和平台工具集VS2010 必须打 SP1 补丁没打 SP1 的版本在编译 CEF3 的 lib 时会出现MSB6006报错。检查方法很简单打开 VS2010帮助菜单里看版本号带 SP1 的会显示10.0.40219.1 SP1Rel。如果还是 RTM 版本先去装 SP1。平台工具集在项目属性里确认。右键项目 → 属性 → 配置属性 → 常规 → 平台工具集选v100。这个选项在 VS2010 里默认就是 v100但如果你从别的机器拷贝了 vcxproj 文件过来有可能被改成 v142 之类的编译时会提示找不到工具集。字符集也要提前定好。CEF3 的 C API 用的是 UTF-8 和 UTF-16MFC 默认可能是多字节字符集。我一般把项目字符集改成“使用 Unicode 字符集”避免后面字符串转换来回折腾。位置在项目属性 → 配置属性 → 常规 → 字符集。2.2 CEF3 90.6.0 的目录结构和必须的文件CEF3 90.6.0 的发布包解压后目录结构大致是这样cef_binary_90.6.0_windows32/ ├── cmake/ ├── include/ │ ├── cef_app.h │ ├── cef_browser.h │ ├── cef_client.h │ ├── cef_v8.h │ └── ... ├── libcef_dll/ │ ├── cef_v8_impl.cc │ └── ... ├── Release/ │ ├── libcef.dll │ ├── libcef.lib │ ├── cef.pak │ ├── cef_100_percent.pak │ ├── cef_200_percent.pak │ ├── icudtl.dat │ ├── resources/ │ └── locales/ │ ├── en-US.pak │ └── zh-CN.pak └── Debug/ └── ...必须用到的文件分三类。第一类是链接用的libcef.lib放在 Release 或 Debug 目录下。第二类是运行时依赖包括libcef.dll、icudtl.dat、cef.pak、cef_100_percent.pak、cef_200_percent.pak以及locales目录下的语言包。第三类是 C 头文件和libcef_dll里的 wrapper 源码这些需要加进 VS 项目一起编译。注意32 位和 64 位的包不能混用。VS2010 项目如果是 Win32 平台就必须用 windows32 的 CEF3 包用 64 位包会在链接阶段报LNK1112。2.3 把 CEF3 源码和库挂进 VS2010 项目新建一个 MFC 对话框程序项目名假设叫CefMfcDemo。然后按下面步骤把 CEF3 挂进去。第一步把 CEF3 的include目录加到附加包含目录。项目属性 → C/C → 常规 → 附加包含目录填入D:\cef_binary_90.6.0_windows32\include第二步把libcef_dll目录下的源码加入项目。在解决方案资源管理器里右键项目 → 添加 → 现有项选中libcef_dll下所有.cc和.h文件。注意libcef_dll里有一个wrapper子目录里面的文件也要一起加进去。第三步配置附加库目录和附加依赖项。项目属性 → 链接器 → 常规 → 附加库目录填入D:\cef_binary_90.6.0_windows32\Release项目属性 → 链接器 → 输入 → 附加依赖项填入libcef.lib libcef_dll_wrapper.liblibcef_dll_wrapper.lib需要你自己编译libcef_dll源码生成。如果不想单独编译也可以把libcef_dll的源码直接加进当前项目一起编译这样就不需要额外的 lib 文件。我一般选后者少一个编译步骤。第四步把运行时文件拷到输出目录。编译成功后把libcef.dll、icudtl.dat、cef.pak、cef_100_percent.pak、cef_200_percent.pak和整个locales目录拷到 exe 所在目录。这一步漏掉任何一个程序启动时都会直接退出而且不一定有报错弹窗。3. 初始化 CEF3从 CefInitialize 到消息循环改造3.1 继承 CefApp 和 CefClient 的最小实现CEF3 的初始化需要两个核心对象CefApp和CefClient。CefApp负责进程级回调CefClient负责浏览器实例的回调。最小实现如下// SimpleApp.h #pragma once #include include/cef_app.h class SimpleApp : public CefApp, public CefBrowserProcessHandler { public: SimpleApp() {} virtual CefRefPtrCefBrowserProcessHandler GetBrowserProcessHandler() override { return this; } virtual void OnContextInitialized() override; private: IMPLEMENT_REFCOUNTING(SimpleApp); };// SimpleClient.h #pragma once #include include/cef_client.h class SimpleClient : public CefClient { public: SimpleClient() {} virtual CefRefPtrCefLifeSpanHandler GetLifeSpanHandler() override { return this; } virtual void OnAfterCreated(CefRefPtrCefBrowser browser) override; private: IMPLEMENT_REFCOUNTING(SimpleClient); };IMPLEMENT_REFCOUNTING宏是 CEF3 引用计数机制的基础漏掉会导致对象被提前释放表现为浏览器窗口创建后立刻崩溃。OnContextInitialized是浏览器进程初始化的回调在这里创建第一个浏览器窗口。OnAfterCreated在浏览器对象创建完成后触发可以在这里保存 browser 指针。3.2 在 MFC 的 InitInstance 里启动 CEF 消息循环MFC 有自己的消息循环CEF3 也需要消息循环两者必须整合。常见做法是在InitInstance里初始化 CEF然后用CefRunMessageLoop替代 MFC 的Run。// CefMfcDemo.cpp 的 InitInstance 片段 CefMainArgs main_args(GetModuleHandle(NULL)); CefRefPtrSimpleApp app(new SimpleApp()); // 命令行参数处理子进程需要走这个分支 int exit_code CefExecuteProcess(main_args, app.get(), NULL); if (exit_code 0) { return exit_code; } CefSettings settings; settings.multi_threaded_message_loop false; settings.no_sandbox true; CefString(settings.locale) zh-CN; CefInitialize(main_args, settings, app.get(), NULL); // 创建主窗口 CMainFrame* pFrame new CMainFrame(); pFrame-Create(NULL, _T(CefMfcDemo)); m_pMainWnd pFrame; pFrame-ShowWindow(SW_SHOW); CefRunMessageLoop(); CefShutdown();CefExecuteProcess必须放在CefInitialize之前它的作用是判断当前进程是不是 CEF 的子进程。如果是子进程它会执行子进程逻辑并返回退出码主进程继续往下走。漏掉这一步程序启动时会创建一堆子进程然后卡死。multi_threaded_message_loop设为false表示 CEF 和 MFC 共用主线程消息循环。如果设为trueCEF 会自己起一个线程跑消息循环MFC 这边需要额外处理线程同步复杂度更高。我一般先用false跑通之后再考虑要不要改。no_sandbox设为true是因为 VS2010 环境下沙箱初始化经常失败表现为浏览器进程起不来。关掉沙箱对内部工具来说可以接受如果是面向公网的产品需要另外处理沙箱配置。3.3 创建浏览器窗口并嵌入 MFC 对话框在OnContextInitialized里创建浏览器窗口void SimpleApp::OnContextInitialized() { CefRefPtrSimpleClient client(new SimpleClient()); CefWindowInfo window_info; CefRect rect(0, 0, 800, 600); window_info.SetAsChild(GetActiveWindow(), rect); CefBrowserSettings browser_settings; CefBrowserHost::CreateBrowser(window_info, client.get(), http://localhost/index.html, browser_settings, NULL); }SetAsChild把浏览器窗口设为 MFC 窗口的子窗口GetActiveWindow()返回当前活动窗口句柄。如果 MFC 对话框还没创建完成这里可能拿到空句柄浏览器窗口会变成独立窗口而不是嵌入。稳妥做法是在对话框的OnInitDialog里触发浏览器创建而不是在OnContextInitialized里直接创建。CefBrowserSettings里可以设置webgl、javascript等开关默认都是开启的。如果页面需要访问本地文件还要加--allow-file-access-from-files启动参数。4. JS 调用 C注册 V8 扩展和消息路由4.1 用 CefRegisterExtension 注册 JS 可调用的原生函数CEF3 提供两种 JS 调 C 的方式V8 扩展和消息路由。V8 扩展适合简单的同步调用消息路由适合异步和复杂参数传递。先看 V8 扩展的写法。// 在 OnContextInitialized 里注册扩展 CefRefPtrCefV8Value object CefV8Value::CreateObject(NULL); CefRefPtrCefV8Value func CefV8Value::CreateFunction(nativeCall, new NativeCallHandler()); object-SetValue(nativeCall, func, V8_PROPERTY_ATTRIBUTE_NONE); CefRefPtrCefV8Value retval; CefRefPtrCefV8Exception exception; CefRegisterExtension(v8/native, var native {}; native.call function(msg) { return nativeCall(msg); };, object);CefRegisterExtension的第一个参数是扩展名第二个参数是 JS 代码第三个参数是暴露给 JS 的对象。JS 侧调用native.call(hello)就会触发NativeCallHandler的Execute方法。NativeCallHandler需要继承CefV8Handler并实现Executeclass NativeCallHandler : public CefV8Handler { public: virtual bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) override { if (name nativeCall arguments.size() 1) { CefString msg arguments[0]-GetStringValue(); // 处理 msg比如写日志或更新 UI retval CefV8Value::CreateString(received: msg.ToString()); return true; } return false; } IMPLEMENT_REFCOUNTING(NativeCallHandler); };Execute的返回值表示是否处理成功。返回true时retval会作为 JS 调用的返回值。如果返回falseJS 侧会收到undefined。4.2 消息路由方式CefMessageRouter 的配置V8 扩展的局限是只能在浏览器进程内调用如果 C 侧需要跨进程通信就要用消息路由。CEF3 的CefMessageRouter封装了这套机制。// 在 SimpleClient 里添加消息路由 CefRefPtrCefMessageRouterBrowserSide message_router_; SimpleClient::SimpleClient() { CefMessageRouterConfig config; config.js_query_function cefQuery; config.js_cancel_function cefQueryCancel; message_router_ CefMessageRouterBrowserSide::Create(config); message_router_-AddHandler(new SimpleMessageHandler(), false); }JS 侧调用cefQuery({request: hello})就会触发SimpleMessageHandler的OnQuery方法。SimpleMessageHandler需要继承CefMessageRouterBrowserSide::Handler并实现OnQuery。class SimpleMessageHandler : public CefMessageRouterBrowserSide::Handler { public: virtual bool OnQuery(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, int64 query_id, const CefString request, bool persistent, CefRefPtrCallback callback) override { if (request hello) { callback-Success(world); return true; } return false; } };OnQuery返回true表示已处理callback-Success把结果返回给 JS。如果返回false消息会继续传给下一个 handler。persistent参数表示是否保持回调通道用于需要多次返回结果的场景。4.3 两种方式的取舍和常见配置错误V8 扩展适合简单同步调用代码量少但只能在浏览器进程内使用。消息路由适合异步和跨进程场景配置稍复杂但扩展性更好。我一般先用 V8 扩展跑通基本调用再根据需求决定要不要上消息路由。常见配置错误有三个。第一是CefRegisterExtension在OnContextInitialized里调用太早此时 V8 上下文还没准备好注册会失败。正确做法是在OnContextCreated回调里注册。第二是消息路由的js_query_function名字和 JS 侧调用的名字不一致导致 JS 报cefQuery is not defined。第三是AddHandler的第二个参数设为true时handler 会在所有浏览器实例间共享如果 handler 里保存了 browser 相关状态会出现串数据。5. 避坑记录VS2010 CEF3 90.6.0 最常见的五个翻车点5.1 编译报 LNK2001无法解析的外部符号现象链接阶段报大量LNK2001符号名里带Cef前缀。原因libcef_dll_wrapper.lib没有正确链接或者libcef_dll源码没有加入项目。解决确认附加依赖项里有libcef.lib和libcef_dll_wrapper.lib或者把libcef_dll目录下所有.cc文件加入项目一起编译。如果用的是预编译的 wrapper lib确认它的平台和当前项目一致。5.2 程序启动闪退无报错弹窗现象双击 exe 后进程一闪而过事件查看器里能看到异常退出记录。原因运行时文件缺失最常见的是icudtl.dat或locales目录没拷到 exe 旁边。解决把 CEF3 发布包 Release 目录下的libcef.dll、icudtl.dat、cef.pak、cef_100_percent.pak、cef_200_percent.pak和整个locales目录全部拷到 exe 所在目录。注意locales目录本身也要拷不是只拷里面的 pak 文件。5.3 浏览器窗口不显示MFC 窗口空白现象MFC 主窗口出来了但里面没有浏览器内容一片空白。原因SetAsChild的父窗口句柄传错了或者浏览器创建时机太早MFC 窗口还没完成初始化。解决把浏览器创建移到 MFC 对话框的OnInitDialog里用GetSafeHwnd()获取当前对话框句柄传给SetAsChild。如果还是不行检查CefBrowserHost::CreateBrowser的返回值确认浏览器对象是否创建成功。5.4 JS 调用 C 无响应控制台报 undefined现象页面里调用native.call(test)控制台提示native is not defined或nativeCall is not defined。原因V8 扩展注册失败或者注册时机不对。解决把CefRegisterExtension移到OnContextCreated回调里。如果用的是消息路由确认js_query_function的名字和 JS 侧调用的名字完全一致包括大小写。另外检查CefMessageRouterConfig是否在SimpleClient构造时正确初始化。5.5 中文乱码页面显示问号现象页面里的中文显示为乱码或问号。原因CEF3 默认 locale 是 en-US没有加载中文语言包或者 HTML 页面没有声明 UTF-8 编码。解决在CefSettings里设置CefString(settings.locale) zh-CN确认locales目录下有zh-CN.pak。HTML 页面头部加meta charsetutf-8。如果 C 侧往页面传中文用CefString的 UTF-16 构造不要用std::string直接传。6. 装完怎么验证从窗口嵌入到 JS 调用的完整检查清单验证分三步走每一步都有明确的通过标准。第一步确认浏览器窗口嵌入成功。编译运行后MFC 主窗口里应该出现一个浏览器区域加载http://localhost/index.html或本地 HTML 文件。如果页面能正常渲染说明 CEF3 初始化和窗口嵌入都没问题。这一步的检查点是窗口大小是否跟随 MFC 窗口调整、页面滚动是否流畅、右键菜单是否正常弹出。第二步验证 JS 调用 C 的 V8 扩展通道。在页面里加一段测试代码// 测试 V8 扩展调用 if (typeof native ! undefined typeof native.call function) { var result native.call(ping); console.log(native.call returned: result); document.getElementById(result).innerText result; } else { console.log(native object not available); }如果页面上的result元素显示received: ping说明 V8 扩展通道正常。如果显示native object not available回到第 4 章检查CefRegisterExtension的注册时机和参数。第三步验证消息路由通道。在页面里调用// 测试消息路由 if (typeof cefQuery function) { cefQuery({ request: hello, onSuccess: function(response) { console.log(cefQuery success: response); }, onFailure: function(error_code, error_message) { console.log(cefQuery failed: error_code error_message); } }); } else { console.log(cefQuery not available); }控制台输出cefQuery success: world表示消息路由正常。如果报cefQuery not available检查CefMessageRouterConfig的js_query_function是否设为cefQuery以及message_router_是否在SimpleClient构造时正确创建。我一般还会加一个压力测试在页面里连续调用native.call一百次观察是否有内存泄漏或崩溃。CEF3 的引用计数如果处理不当高频调用下容易出问题。用任务管理器看进程内存如果持续上涨不回落检查IMPLEMENT_REFCOUNTING是否漏加或者CefV8Value的引用是否在Execute返回后正确释放。从那以后我每次配 CEF3 环境都会先把locales目录和icudtl.dat拷到输出目录再编译编译完先跑一个空白页面确认窗口能出来再逐步加 JS 调用逻辑。这套顺序帮我省了不少来回折腾的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?