ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CEF 90嵌入Windows桌面应用:集成、自定义协议与JS通信实战

CEF 90嵌入Windows桌面应用:集成、自定义协议与JS通信实战 简介面向Windows 32位桌面应用开发者这份基于CEF3框架的CEF 90.5.9定制构建将Chromium 90.0.4430.85的浏览器能力封装成可直接调用的SDK适合在C工程中嵌入Web页面、处理JavaScript回调、实现自定义渲染或扩展网络请求等场景。压缩包共972个文件既有大量头文件和C源文件供二次开发参考也有pak资源、dll动态库、manifest清单、cmake和gypi构建脚本等运行与编译支持整体224.63MB便于32位环境集成。该版本基于2021年4月29日构建支持H.264视频编解码网页播放视频时无需额外解码器在多媒体展示、在线预览等场景体验更完整。已有1089人学习下载包内提供libcef.dll等关键运行时组件及cef.pack资源数据解压后可按官方目录结构快速接入工程省去自行编译Chromium的繁琐流程同时包含v8快照、测试用例和文档便于排查集成问题适合需要稳定内嵌浏览器能力的Windows桌面开发者。1. 别再重复造轮子了CEF 90 这个 32 位包能帮你省下三个月桌面应用里嵌入一个完整的 Chrome 内核听起来是个大工程但 CEFChromium Embedded Framework就是干这个的。这篇文章要拆的cef_binary_90.5.9gd330790chromium-90.0.4430.85_windows32.zip是一个已经编译好的 CEF 3 二进制分发包直接解压就能链接使用省去了从零编译 Chromium 的漫长等待和磁盘空间消耗。它对应的 Chromium 内核版本是 90.0.4430.85带 H.264 硬解支持发布年代在 2021 年。适合谁用如果你在做 Windows 桌面端的混合开发比如把网页端的管理后台、大屏可视化直接嵌进 Win32 或 C 客户端程序里这个包值得看。版本 90 不算新但它的 API 稳定性和对旧系统的兼容性在工业软件、医疗设备、银行柜面系统里反而是一个优势而且太多存量项目卡在这个版本上选它是有现实理由的哪怕到今天依然有大量维护任务围绕这条版本线展开。2. 拆包看结构CEF 二进制包里的每一层都是为运行时设计的先别急着写代码把一个 CEF 二进制包解压后第一件事是搞清楚目录里各文件的角色。很多人上来就用结果运行时崩溃或者白屏根本原因就是资源文件路径没配对。这个 90.5.9 版本的解压目录里核心文件就那么几个每个都对应 CEF 运行时的关键一环。2.1 libcef.dll 是主进程的灵魂v8 快照是渲染进程的地基CEF 的最核心动态库是libcef.dll它承载了 Chromium 的内容模块、网络栈、渲染引擎的宿主逻辑所有 200 多个 C API 函数入口都从这个 DLL 导出。在加载这个 DLL 之前你需要先确认它依赖的 VC 运行库已经安装否则LoadLibrary会直接返回 NULL 且没有明显提示。你可以在工程里显式调用LoadLibrary(Llibcef.dll)来做一次预加载测试这样至少能把运行时环境问题提前暴露出来。另外两个文件值得留意v8_context_snapshot.bin和snapshot_blob.bin。它们存放的是 V8 JavaScript 引擎的堆快照和内置代码快照。简单说V8 启动时要初始化一堆内置对象和函数如果每次启动都现场构建耗时很高这两个快照文件让 V8 能从预热的二进制状态直接恢复大幅缩短渲染进程的启动时间。这个特性在 CE3 中是从 Chromium 90 开始稳定化的。所以你在集成时不要随意删掉这两个文件否则渲染进程会报V8 snapshot is invalid之类的错误。2.2 从文件清单反推工程化集成的完整校验条件一个合格的 CEF 二进制包除了 DLL 和快照之外还应当包含chrome_child.dll、resources.pak、cef.pak、icudtl.dat、libEGL.dll和libGLESv2.dll等关键支撑文件。其中icudtl.dat是 ICU 国际化数据删掉它会导致页面上的中文字符乱码、文字排版异常cef.pak存放 CEF 自身 UI 层的本地化字符串而resources.pak则包含 Chromium 的基础资源比如内置的错误页、密码管理提示框等。你可以用下面这段命令做一个完整性校验确保所有核心文件都在预期位置cd cef_binary_90.5.9gd330790chromium-90.0.4430.85_windows32 for f in libcef.dll chrome_child.dll resources.pak cef.pak icudtl.dat v8_context_snapshot.bin snapshot_blob.bin; do if [ -f $f ]; then echo OK $f; else echo MISSING $f; fi done这段脚本会在解压目录里逐个检查 7 个关键文件是否存在。-f参数在 Linux shell 里用于判断文件是否为常规文件且存在如果某个文件缺失你会立刻看到MISSING标记这样就能在写代码之前排除资源缺失问题。如果你在 Windows 命令行环境下操作可以用if exist替代if exist libcef.dll (echo OK libcef.dll) else (echo MISSING libcef.dll)。为什么要做这个校验因为网上流传的很多 CEF 包是不完整的缺少 ICU 或快照文件时程序能正常编译但运行时会白屏或直接崩溃先校验再开发可以节省大量调试时间。2.3 单元测试文件不是给你写的测试而是 CEF 自检测试套件包内目录里出现了gtest-all.cc、resource_request_handler_unittest.cc、urlrequest_unittest.cc、navigation_unittest.cc、v8_unittest.cc、message_router_unittest.cc这些文件。很多人看到这些测试文件会感到困惑以为自己要基于它们写测试。实际上这些是 CEF 项目自身的单元测试源码被保留在二进制包中服务于开发者验证当前构建环境是否正常。比如resource_request_handler_unittest.cc测试的是请求拦截与修改能力v8_unittest.cc测试的是 JavaScript 原生绑定接口。如果你想验证当前系统上的 CEF 构建是否健康可以尝试编译一个简单的测试工程链接这些文件运行测试套件。不过更常见、也更省力的做法是使用官方提供的cefclient和cefsimple示例工程做跑通验证。测试源码的存在只是用来反推当前构建的源码版本号帮助你确认 API 头文件与测试代码之间的对应关系。3. Windows 32 位下的 CEF 工程搭建与进程模型当文件校验都通过之后下一步就是建工程、配置包含目录、链接库、配置 DLL 搜索路径。这一节我会把 CEF 二进制包集成到 Visual Studio 工程里的完整链路讲清楚包括工程配置里容易踩的坑以及 CEF 多进程模型在应用层必须处理的关键要点。3.1 CMake 集成配置包含目录和链接参数一次到位CEF 官方推荐用 CMake 来构建集成工程官方仓库里的CMakeLists.txt可以直接复用。下面这个示例是实际工程中精简过的最小配置适用于 Visual Studio 2019 及以上版本。核心逻辑是把 CEF 的二进制包通过环境变量CEF_BINARY_DIR传入然后导入目标libcef_dll_wrapper。cmake_minimum_required(VERSION 3.13) project(CefDemo) set(CEF_BINARY_DIR $ENV{CEF_BINARY_DIR}) set(CEF_ROOT ${CEF_BINARY_DIR}/cef_binary_90.5.9gd330790chromium-90.0.4430.85_windows32) list(APPEND CMAKE_PREFIX_PATH ${CEF_ROOT}) add_subdirectory(${CEF_ROOT} cef_binary_build) add_executable(cef_demo WIN32 main.cc ) target_include_directories(cef_demo PRIVATE ${CEF_ROOT} ) target_link_libraries(cef_demo PRIVATE libcef_dll_wrapper )这段 CMake 脚本里add_subdirectory(${CEF_ROOT} ...)会把它自带的 CMake 构建脚本拉入当前工程生成libcef_dll_wrapper静态库目标。target_link_libraries只需要链接libcef_dll_wrapper不需要直接写libcef.dll的导入库路径因为 wrapper 内部会处理对libcef.dll的加载和调用。这里用到的是 CEF 官方的 CMake 约定CEF_ROOT是你解压后的二进制包根目录。采用这种集成方式能确保头文件、导入库、资源文件的路径相对统一避免后续维护时出现找不到符号的链接问题。3.2 初始化入口CefSettings 参数决定你走单进程还是多进程在写main函数时CEF 要求你先设置CefSettings再调用CefInitialize。这个函数就是整个框架的初始化开关。下面给出的是一个可运行的最小初始化代码重点是settings里几个关键字段的写法#include include/cef_app.h #include include/cef_client.h int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow) { CefMainArgs main_args(hInstance); CefSettings settings; settings.no_sandbox true; settings.multi_threaded_message_loop false; void* sandbox_info nullptr; CefRefPtrCefApp app(new CefApp()); CefInitialize(main_args, settings, app, sandbox_info); CefRunMessageLoop(); CefShutdown(); return 0; }在这个代码中CefMainArgs接收 Win32 的实例句柄和命令行参数此后所有进程都会基于这份参数判断自己的角色。settings.no_sandbox true用于关闭沙箱适合在调试阶段直接使用但生产环境建议开启沙箱以提升安全性代价是你需要额外提供一个sandbox.exe辅助进程。settings.multi_threaded_message_loop false表示由 CEF 自己接管消息循环后续调用CefRunMessageLoop()后会阻塞直到最后一个浏览器窗口被关闭。在CefInitialize传入的app对象里可以重写OnBeforeCommandLineProcessing来追加 Chromium 命令行参数比如忽略 GPU 黑名单或指定代理。3.3 多进程模型与命令行参数为什么 CEF 会拉起一堆子进程第一次跑起 CEF 程序的人多半会被任务管理器里冒出的多个同名进程吓到。这是 Chromium 多进程架构的正常表现主进程负责窗口管理和进程调度渲染进程负责 Blink 排版和 V8 执行GPU 进程负责加速合成。你在 CEF 的CefApp里可以重写OnBeforeCommandLineProcessing追加--disable-gpu之类的开关来应对老旧显卡驱动的兼容问题。还有一个常见但很少有人提的参数是--disable-featuresCalculateNativeWinOcclusion。当用户在远程桌面环境或虚拟机里运行32位应用时Windows 的原生窗口遮挡检测会耗费额外 CPU加上这个参数能明显缓解前台窗口卡顿。下面列出几条常用且安全的 Chromium 命令行参数及其适用场景命令参数作用适用场景--disable-gpu禁用 GPU 硬件加速强制软件渲染老显卡或黑屏问题--in-process-gpuGPU 进程并入浏览器进程调试 GPU 崩溃问题时--disable-extensions禁用扩展系统减少 Loading 阶段开销--no-zygote不用僵尸进程模型拉起子进程Windows 下调试进程崩溃现场--disable-ipc-flooding-protection关闭 IPC 限流保护高频 JS 与 Native 通信时用在 32 位进程空间中每一个附加的参数都意味着多占一块虚拟内存所以不建议无脑堆参数。凡是能在CefSettings里完成配置的就尽量不要用命令行覆盖保持最小参数集是长稳运行的前提。4. 在 CEF 90 里处理自定义协议、JavaScript 交互与网络拦截CEF 的价值不在于它能显示 HTML而在于它能打通 Web 世界和桌面原生世界。本章要展开的是三个高频需求自定义协议、JS-Native 通信、网络请求拦截与修改。这三块是 CEF 集成的深度所在也是搜索引擎里大家最常搜的玩法。4.1 自定义 Scheme 注册用 app:// 代替 file:// 实现资源安全加载在默认情况下页面里加载本地图片和脚本时用file://会让页面里的 JS 处于不可控的高权限状态且容易被本地路径注入攻击。CEF 允许注册自定义 Scheme协议比如app://然后把静态资源从本地文件系统中映射到这个协议下。实现方式是在CefApp派生的类中重写OnRegisterCustomSchemesclass MyCefApp : public CefApp, public CefSchemeHandlerFactory { public: void OnRegisterCustomSchemes(CefRawPtrCefSchemeRegistrar registrar) override { registrar-AddCustomScheme(app, CEF_SCHEME_OPTION_STANDARD | CEF_SCHEME_OPTION_SECURE); } CefRefPtrCefResourceHandler Create( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, const CefString scheme_name, CefRefPtrCefRequest request) override { return new AppSchemeHandler(); } IMPLEMENT_REFCOUNTING(MyCefApp); };AddCustomScheme中的CEF_SCHEME_OPTION_STANDARD让该协议支持标准 URL 解析即app://index.html里能正确识别主机名和路径CEF_SCHEME_OPTION_SECURE则把这个协议标记为安全来源页面中 AJAX 请求该协议下的资源时将不受跨域限制。属性CEF_SCHEME_OPTION_CSP_BYPASSING的场景不要轻易开启它会让内容安全策略全部失效除非你的资源全部可信且不包含第三方脚本。在协议注册之外AppSchemeHandler需要继承CefResourceHandler并覆写ProcessRequest、GetResponseHeaders、ReadResponse等方法。其中GetResponseHeaders里要设置 MIME 类型比如.js返回application/javascript.png返回image/png否则 JS 不会执行、图片不会渲染。这里最容易出错的地方是ResponseLength必须返回精确的字节长度否则 WebKit 会认为内容尚未加载完一直转圈等待。4.2 JavaScript 双向通信用 CefV8Handler 和 CefMessageRouter 避开常见内存陷阱CEF 90 中JS 调用 C 最稳定的方式是通过CefMessageRouterBrowserSide。这个类把 C 方法注册成一个 JS 回调页面上只需调用window.cefQuery({ request: ..., onSuccess, onFailure })即可触发 C 侧逻辑。这种基于请求-响应的通信模型比直接绑定 V8 函数更安全因为它把 JS 对象生命周期与 V8 Context 隔离避免在页面跳转时因为框架未释放而崩溃。使用CefMessageRouter时首先在CefApp的OnContextCreated里创建并绑定 JS 回调这是官方推荐做法。由于该回调属于浏览器进程上下文实现类需要持有CefMessageRouterBrowserSide的引用并在OnBeforeClose中调用其OnBeforeClose方法。翻看过包内自带的message_router_unittest.cc就能发现很多边界情况已经被 CEF 自测覆盖比如页面跳转时未完成请求的回调如何处理但前提是你要在自己的实现中正确管理消息路由器的生命周期。如果你做的是桌面客户端混合开发还有一条更高效的路径就是使用CefV8Handler直接暴露一个全局对象。举例来说要让页面里可以调用window.NativeApi.openFile()参考代码如下class NativeApiBridge : public CefV8Handler { public: bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) override { if (name openFile) { // 弹出原生文件对话框 retval CefV8Value::CreateString(OpenFileDialog()); return true; } return false; } IMPLEMENT_REFCOUNTING(NativeApiBridge); };在运行时你通过frame-GetV8Context()进入上下文context-Enter()后用CefV8Value::CreateObject创建全局桥接对象并调用SetValue(openFile, CefV8Value::CreateFunction(openFile, handler), V8_PROPERTY_ATTRIBUTE_NONE)将函数挂载到全局对象。需要特别注意的是这个Enter()和Exit()必须成对出现漏掉Exit()会导致后续同线程上的 V8 操作全部失败这是使用 V8 嵌入时最高频的崩溃来源。4.3 用 ResourceRequestHandler 做请求级别的拦截、改写与重定向CEF 90 系列里拦截网络请求的入口是CefRequestHandler或其子类CefResourceRequestHandler。一个典型需求是拦截页面上某个域名下的所有 API 响应把测试桩数据替换成真实结构。常见做法是在OnBeforeResourceLoad中获取请求对象检查 URL 前缀然后返回RV_CANCEL并手动执行一个新请求或者使用CefResourceHandler回填自定义内容。下面展示的是使用OnBeforeResourceLoad做域名级重定向的代码骨架CefResourceRequestHandler::ReturnValue OnBeforeResourceLoad( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefRequest request, CefRefPtrCefCallback callback) override { const std::string url request-GetURL().ToString(); if (url.find(old-server.com) ! std::string::npos) { std::string new_url url; new_url.replace(new_url.find(old-server.com), 14, new-server.com); request-SetURL(new_url); } return RV_CONTINUE; }这个代码块里的逻辑并不复杂关键点是request-SetURL之后CEF 会在内部把重定向后的 URL 交给后续流程处理而不需要你手动发起另一个CefRequest对象。参数说明里有一个重要边界值得留意OnBeforeResourceLoad默认只处理渲染进程发起的请求如果主进程里有自定义 UrlRequest 发起的网络调用需要传入进程类型的判断此外如果要拦截 POST 的 body则需要同时覆写GetResourceHandler并处理CefPostDataElement。包内自带的urlrequest_unittest.cc就是干这个事情的你可以在里面找到很多对 URL 请求对象做增删改查的细节它是写网络拦截模块时最好的参考素材。5. 编译 CEF 源码级的边界H.264 支持从哪里来以及何时需要自己编译有一行描述必须认真对待2021.4.29支持h264编译。CEF 上游项目默认不包含 H.264/AAC 的专利代码因为涉及授权问题。之前的官方二进制包和分支构建会通过 ffmpeg 的编译开关把 H.264 解码器编进去。你拿到的这个包如果确认包含 H.264 支持就不用再去手工下载解码器省事很多。5.1 验证 H.264 支持是否真的生效判断一个 CEF 构建是否真正支持 H.264可以通过一个实际页面来验证。在程序里加载https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8这种 HLS 测试流然后在浏览器页面里加一个canPlayType检查const video document.createElement(video); const canPlay video.canPlayType(video/mp4; codecsavc1.42E01E); console.log(H.264 support: canPlay);如果返回值为probably说明该构建包含 H.264 解码能力如果返回空字符串说明该构建只支持基础视频编码播放时会走服务端转码或直接失败。另一种快速验证方法是直接播放一个.mp4文件观察渲染进程日志里是否有解码器初始化失败的记录。CEF 的日志文件默认生成在cef.log关键错误信息通常是Decode error或Unsupported codec。5.2 跨架构与跨版本维护的启发顺着 H.264 编译往下走自然就引出关于架构适配的话题。你手里的包是 windows32 版本是因为目标系统或存量驱动只支持 32 位。在实际维护中如果你的程序要奔着 64 位或者 ARM64 走比如国产化终端上跑 ARM64 WindowsCEF 二进制包必须替换为对应架构不能指望 x86 的动态库在 ARM64 模拟层中高效运行。当前社区里的做法是维护多套构建产物打包脚本里按平台拉取不同的 zip再打不同安装包。CEF 90 的 API 和今天主流的 CEF 版本的差异主要在旧接口的废弃上但核心的CefInitialize、CefMessageRouter、CefResourceHandler连写法和参数结构都能保持高兼容度这对于维护旧项目是件好事代码笔记和经验沉淀不会过时。如果是接触一个依赖 CEF 90 的存量工程优先把版本锁死不要随意升级 CEF 小版本因为 Chromium 内核升级伴随 V8 快照格式的变化会导致两个问题同时爆发编译告警和线上页面脚本行为不一致。5.3 进程崩溃定位的两个常用技巧CEF 90 多进程模式下如果你遇到了渲染进程闪退但主进程没退出直接在CefSettings里把log_severity设置为LOGSEVERITY_VERBOSE日志区间会涵盖 GPU、渲染、网络栈三个模块配置代码如下settings.log_severity LOGSEVERITY_VERBOSE; settings.log_file cef_all.log;这样启动后渲染进程的Sandbox初始化情况和 V8 GC 日志都会写入cef_all.log。如果日志里出现了Failed to send GpuControl就基本可以断定是 GPU 进程崩了。这时候加--disable-gpu验证一次如果问题消失说明是显卡驱动兼容性问题让用户升级驱动或者走软件合成。还有更隐蔽的场景是渲染进程崩溃后主进程没有重启失败进程页面显示Aw, Snap!对此 CEF 提供了CefRequestHandler::OnRenderProcessTerminated回调在里面加载一个自定义的错误页提示用户刷新或自动重载能明显改善终端的用户可感知体验。这个回调在长时间运行的自助终端上尤其重要因为你不可能派人到现场按刷新键。本文还有配套的精品资源点击获取
返回列表