资讯详情

资讯详情

Delphi嵌入Chrome内核:CEF3与DCEF实战指南

简介资源面向Delphi开发者的Chromium(CEF3)组件集成包用于替代传统WebBrowser控件使Delphi程序获得Google Chrome同内核的HTML5渲染能力兼容DELPHI7、2010、XE2、XE8、XE10等常见版本适合开发WEB终端显示、内嵌浏览器类桌面应用。压缩包共109个文件、大小约16.13MB包含libcef.dll与icudt.dll等9个核心运行库、53个Chromium资源pak文件、10个Pascal单元源文件及dpr/dpk工程文件另附可执行示例、dfm窗体定义、cfg/dof配置和bat脚本结构完整且便于直接套用。已有974人浏览学习。资源内置最新版CEF3组件及多版本适配工程拿到后可按需选取对应Delphi版本的dproj/dpr工程直接替换dll运行库即可集成避免了从源码编译匹配的繁琐过程附带的示例程序也能立即验证运行效果尤其适合需要在Delphi项目中快速嵌入现代浏览器能力的中高级开发者。1. 为什么我扔掉 TWebBrowserCEF3 才是 Chrome 内核该有的样子做 Delphi 桌面端的老开发十有八九都被 TWebBrowser 坑过网页一升级内嵌浏览器就白屏稍微复杂点的 CSS3 动画直接卡成幻灯片想在页面里跑个现代前端框架IE 内核根本不给面子。后来我把目光投向 Chromium(CEF3)——这套开源的 Chrome 内核嵌入式方案才算是把“Delphi 程序里放一个真正的 Chrome”这件事落地了。CEF3 全称 Chromium Embedded Framework 3采用多进程架构渲染能力和 Chrome 浏览器完全同源Delphi 侧通过社区维护的封装库 DCEF 接入可以让你用控件拖拽的方式在窗体上放一个真正的 Chromium 渲染引擎。这篇文章适合被 TWebBrowser 折磨过、想把现代 Web 页面无缝嵌进 Delphi 程序的开发者我会把接入 DCEF、初始化、JS 双向通信、参数调优和踩坑记录从头到尾讲清楚。2. 把 DCEF 接进 Delphi 工程下载哪些文件、怎么初始化第一个窗口2.1 DCEF 与 CEF3 的关系为什么社区方案几乎都绕不开这个库CEF3 本身是用 C 写的嵌入式浏览器框架提供的是 C 语言级别的 API。Delphi 开发者如果直接裸调 CEF3 的 C 接口要处理上千个结构体、指针和回调函数光是把一个窗口绑定到渲染进程就够写几百行代码这不现实。社区里最常见的做法是用 DCEF 这个封装库它把 CEF3 的散装 API 包装成了 Delphi 可以使用的类、接口和可视控件你在 Form 上放一个 TChromium就相当于放了一个浏览器窗口。我第一次看到 DCEF 的时候也怀疑过一个开源封装库能不能跟上 CEF3 的更新节奏实践下来的结论是绝大部分情况下够用。DCEF 面向的开发者群体很固定仓库里长期有人维护新版的 CEF 发布后社区一般会同步更新封装层。我个人的建议是不要追求最新 CEF3 版本而是选择一个你确认能拿到完整 DLL 包、且 DCEF 对应的版本配套稳定的组合工程能顺利跑起来比什么都重要。DCEF 的使用模式有三个阶段第一阶段是全局初始化在工程文件里创建并配置 GlobalCEFApp第二阶段是在窗体上放置 TChromium 控件设置默认 URL第三阶段是通过回调事件实现 JS 和 Delphi 的数据交换。整篇文章的代码都围绕这三个阶段展开你跟着写一遍就能跑。2.2 工程文件布局dll、资源、缓存目录怎么摆CEF3 不是一个静态库它运行时会加载一组 DLL 和资源文件所以工程目录不能只放一个 exe。我一般会在 exe 所在目录下建立几个子文件夹把 CEF3 的依赖文件分类放好这样程序升级或者换机器调试时问题比较容易定位。目录 / 文件作用说明libcef.dll等核心 DLL浏览器内核主程序必须放在 exe 能直接找到的位置Resources\浏览器内置资源包括 .pak 文件、icu 数据等locales\多语言包按需保留全部保留也没问题Framework\框架层文件DCEF 指定的框架目录常见的做法是把libcef.dll、libEGL.dll、libGLESv2.dll等直接放在 exe 同目录Resources和locales可以继续用子目录。DCEF 初始化时通过FrameworkDirPath、ResourcesDirPath、localesDirPath三个属性指定路径如果你的目录结构和默认布局不一致程序启动时会直接报找不到资源的错误这个坑后来我再细说。2.3 最小初始化代码GlobalCEFApp 的启动流程DCEF 的初始化入口是工程文件里的GlobalCEFApp全局对象。你必须在Application.Run之前执行StartMainProcess否则 CEF3 的浏览器进程和渲染进程之间的通信管道无法建立后面创建浏览器控件也会失败。我用 Delphi 10.3 和社区维护的 DCEF 版本跑通过的最小工程如下program ChromiumDemo; {$R *.res} uses System.SysUtils, uCEFApplication, uCEFConstants; var GlobalCEFApp: TCefApplication; begin // 在 Application.Run 之前完成 CEF 全局配置 GlobalCEFApp : TCefApplication.Create; // Framework 目录放 CEF 的核心 DLL 与资源 // 用 ExtractFilePath(ParamStr(0)) 取 exe 目录最稳 GlobalCEFApp.FrameworkDirPath : ExtractFilePath(ParamStr(0)) Framework\; GlobalCEFApp.ResourcesDirPath : ExtractFilePath(ParamStr(0)) Resources\; GlobalCEFApp.localesDirPath : ExtractFilePath(ParamStr(0)) locales\; // cache 目录不配的话有些页面会反复重新加载 GlobalCEFApp.cache : ExtractFilePath(ParamStr(0)) cef_cache\; // 日志级别设成 WARNING平时运行不刷屏 // 出问题再改成 LOGSEVERITY_VERBOSE 看详细日志 GlobalCEFApp.LogFile : ExtractFilePath(ParamStr(0)) cef_debug.log; GlobalCEFApp.LogSeverity : LOGSEVERITY_WARNING; // 这一步阻塞到程序退出相当于接管了主消息循环 GlobalCEFApp.StartMainProcess; // 正常退出后释放全局对象避免句柄泄漏 GlobalCEFApp.Free; end.这段代码的关键点有三个一是StartMainProcess必须放在工程文件中执行不要放在某个 Form 的OnCreate里否则 CEF3 的子进程调度还没准备好浏览器窗口创建时会失败。二是cache属性推荐显式设置不设置时 CEF3 可能无法建立磁盘缓存每次重启程序页面都会重新请求资源体感就是“启动后第一次打开特别慢”。三是日志级别平时开到 WARNING 就能定位大部分问题需要查 JS 错误或者渲染异常时再临时改到 VERBOSE。2.4 一个能跑的 DemoTChromium 控件放置与页面加载初始化完成之后接下来就是放浏览器控件。在 DCEF 中TChromium 是可用的可视控件我一般把它放在窗体客户区代码里设置默认 URL然后调用CreateBrowser。这里特别提醒TChromium 控件的创建时机是窗口创建之后不是设计期就创建底层浏览器对象所以你要在 Form 的OnCreate事件里执行创建逻辑。procedure TForm1.FormCreate(Sender: TObject); begin // 设置加载地址注意这里不要直接调 Load // 用 CreateBrowser 统一创建浏览器内核 Chromium1.DefaultURL : https://example.com; Chromium1.CreateBrowser; end;DefaultURL和CreateBrowser的关系经常被误解。直接调用Chromium1.Load(http://...)也能加载页面但CreateBrowser会在初始化时创建完整的浏览器实例并绑定后续的事件回调是更稳妥的入口。给控件放置好之后你就能看到窗体上出现一个真实 Chromium 内核的浏览器页面可以正常登录、跑 JavaScript、播放 HTML5 视频和 Chrome 浏览器体验一致。3. 与网页双向通信核心 API 调用与回调事件怎么挂3.1 生命周期回调什么时候页面加载好了、什么时候可以注入 JS把浏览器控件放出来只是第一步实际业务里几乎都要跟网页数据交互。但在交互之前你必须先理解页面的加载生命周期。CEF3 里最常用的回调是OnLoadEnd它表示某个 frame 的加载已完成。另一个是OnLoadStart表示开始加载。经验少的开发者经常在OnLoadStart里执行页面初始化脚本结果页面还没解析完脚本操作的元素还不存在注入自然失败。我一般只认OnLoadEnd且只处理主 frame。页面上如果有 iframeCEF3 会对每个 frame 都触发一次加载事件如果不判断frame.IsMain同一个脚本可能被执行好几遍还可能在 iframe 里拿到沙箱化的全局对象导致你的 JS 调用找不到变量。下面是一个安全的注入时机procedure TForm1.Chromium1LoadEnd(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; httpStatusCode: Integer); begin // 只处理主 frame避免 iframe 加载完成时重复执行 if frame.IsMain then frame.ExecuteJavaScript(window.hello 42;, about:blank, 0); end;ExecuteJavaScript的第三个参数是起始行号一般填 0。它的第二个参数是脚本关联的 URL只在调试堆栈信息时才有意义日常可以填about:blank。主 frame 判断一定要放在最前面这是我在真实项目中反复踩过的点页面上挂了一个第三方统计 iframe结果统计脚本每次触发都会把我们的业务变量重置掉。3.2 Delphi 调 JSExecuteJavaScript 的正确时机和写法ExecuteJavaScript可以执行任意 JavaScript 代码包括调用页面内部定义的函数。比如你的页面里封装了一个updateData(obj)你可以通过frame.ExecuteJavaScript(updateData({a: 1}))把 Delphi 端的数据传进去。但要注意这个调用是异步的CEF3 只是把脚本交给渲染进程执行不会等待返回值。所以不要用来做同步取值。如果页面里没有暴露函数怎么办常见的做法是把你需要调用页面功能的接口挂到 window 对象上然后在页面里提前定义。或者反过来让 Delphi 在OnLoadEnd后通过ExecuteJavaScript动态往页面里塞一个全局对象。比如你要通知页面切换语言可以这样注入// 注入语言配置这个要在页面加载完成后再执行 frame.ExecuteJavaScript( window.APP_LANG zh-CN; if (window.onAppReady) { window.onAppReady(); }, about:blank, 0);这种方式的优点是不依赖页面源码只要约定好一个全局函数前端就能收到通知。缺点是如果页面还没有加载到能解析window.APP_LANG的状态执行会静默失败。所以你看到我加了if (window.onAppReady)的判断目的就是为了防止注入时机不完美导致的错误。3.3 JS 调 Delphi注册 V8 扩展与自定义回调函数的常见写法JS 调 Delphi 是另一个方向的通信。DCEF 提供 V8 扩展机制可以在渲染进程里注册一个 JavaScript 函数这个函数被调用时DCEF 会把控制权转回到 Delphi 代码。我接的项目里有一个典型场景网页里点击“导出 Excel”按钮前端需要把数据交给 Delphi 端处理而不是在前端生成文件。DCEF 的注册方式比较固定核心是一个继承自TCefv8HandlerOwn的类然后在Execute方法里按函数名分发。以注册一个showMessage函数为例type TMyV8Handler class(TCefv8HandlerOwn) function Execute(const name: ustring; const obj: ICefv8Value; const arguments: TCefv8ValueArray; var retval: ICefv8Value; var exception: ustring): Boolean; override; end; function TMyV8Handler.Execute(const name: ustring; const obj: ICefv8Value; const arguments: TCefv8ValueArray; var retval: ICefv8Value; var exception: ustring): Boolean; begin // name 是 JS 里调用的函数名 // arguments 是参数数组按位置取 if name showMessage then begin ShowMessage(arguments[0].GetStringValue); Result : True; Exit; end; Result : False; end;注册这个扩展的代码通常写在你代码库的某个初始化函数里调用TCefRegisterExtension完成// 把 TMyV8Handler 的实例注册为 native/myBridge 扩展 // JS 侧通过 window.myBridge.showMessage 调用 TCefRegisterExtension( native/myBridge, var myBridge window.myBridge || {}; myBridge.showMessage function(msg) { native function showMessage(msg); return showMessage(msg); };, TMyV8Handler.Create as ICefv8Handler);这段代码最容易被忽视的地方是第二段字符串里的native function showMessage(msg);声明它必须和你Execute方法里处理的名字一致否则浏览器会报“native function 未定义”的错误。另一个注意点是TMyV8Handler.Create的强制类型转换DCEF 的接口在调用结束后会释放对象如果你自己又持有了引用要注意生命周期不然可能出现释放两次的问题。3.4 传输大文本数据走 URL 还是走扩展函数Delphi 和 JS 之间传字符串用扩展函数是最直观的方式但扩展函数有两个局限一是参数体量大时会有性能损耗传几 MB 的 JSON 数据V8 转换和内存拷贝会产生明显的卡顿二是扩展函数不适合传二进制内容比如图片字节流。我在做某图像处理 Demo 的时候试过用扩展函数把截图 base64 从 JS 传回 Delphi页面一卡就是两三秒。这时更合理的路径是让 JS 把大数据放到一个临时的接口上Delphi 端去拉取或者走自定义 scheme 协议让 JS 发起一个请求Delphi 在请求拦截器里把数据以 body 形式返回。对于大多数业务简单的做法是 JS 把数据写到window上的一个全局变量然后 Delphi 通过ExecuteJavaScript去读取这个变量并转成本地字符串。要注意的是这个读取操作要在OnLoadEnd之后才能做而且数据量越大越建议拆分批次避免渲染进程阻塞。4. 参数调优与进程模型缓存、代理、UserAgent、GPU 开关的取舍4.1 必调参数清单缓存目录、日志级别、语言列表DCEF 暴露了大量参数但不是每个都要动。我整理了一份“新工程建议先抄”的参数表这些参数直接影响程序的启动速度、稳定性和可调试性。参数建议值作用与说明GlobalCEFApp.cache固定路径如cef_cache\保存 Cookie、LocalStorage、HTTP 缓存不设置则每次重启重新加载GlobalCEFApp.LogFilecef_debug.log运行时日志输出定位崩溃和加载失败的关键GlobalCEFApp.LogSeverityLOGSEVERITY_WARNING平时减少日志量排查问题时改LOGSEVERITY_VERBOSEGlobalCEFApp.AcceptLanguageListzh-CN,zh;q0.9,en;q0.8影响页面的语言协商不设置时可能获取不到中文资源GlobalCEFApp.UserAgent按业务自定义后端按 UA 做来源识别时需要主动设置GlobalCEFApp.RemoteDebuggingPort0关闭设为非 0 值可开启 DevTools 远程调试缓存目录我建议在程序第一次启动时就创建好文件夹避免在受限目录里写入失败。日志级别在开发期可以保持VERBOSE但发布版本一定改回WARNING因为完整日志一天能写几百 MB。语言列表的作用很多人不知道如果你要加载的页面根据Accept-Language返回不同内容不配置的话 CEF3 会按系统语言去协商可能拿到英文页面。4.2 进程模型怎么选多进程与单进程的内存对比CEF3 的第三版相比早期版本最大的架构变化就是多进程。默认情况下一个 Delphi 程序启动后会有一个主进程浏览器进程和若干个渲染进程、GPU 进程、网络进程。渲染进程负责解析页面和执行 JS它崩溃了主进程不受影响。这个设计换来的是稳定性代价是内存占用比单进程方案高不少。DCEF 里通过SingleProcess属性控制是否单进程运行。测试环境里为了套简调试有时会开单进程但我的生产经验是保持默认的多进程模式。原因很简单网页是不可控的某个页面里的广告脚本或者复杂动画把渲染进程搞崩了多进程模式下 Delphi 主程序还能继续运行最多是那个页面标签白屏单进程模式下渲染引擎崩溃会直接带走整个应用。如果你确实在意内存不要粗暴地开单进程而是限制页面数量或者在一个页面加载完成后释放不再使用的浏览器对象。多进程下每个页面大约多占用 20-50 MB 内存这是 Chrome 内核的代价合理的业务设计比切换进程模型更能解决问题。4.3 GPU 渲染与离屏渲染什么时候开什么时候关CEF3 默认开启 GPU 加速这是为了流畅渲染 CSS3、视频等视觉密集内容。但 GPU 加速不是免费的在集成显卡的老机器上驱动兼容性问题可能导致花屏、闪屏甚至程序崩溃。我的习惯是稳定优先先把EnableGPU关掉确认页面显示正常后再打开打开之后还要在几台不同显卡的机器上做一轮回归测试。如果你做的不是普通浏览器窗口而是截图、抓取页面数据这类无界面场景适合用离屏渲染模式。通过GlobalCEFApp.WindowlessRenderingEnabled : True打开页面不会出现在系统窗口里而是把渲染结果输出到一张位图上再由你的代码决定怎么展示。离屏渲染的性能上限比普通窗口渲染低尤其是视频播放所以它更适合工具型应用不建议拿来看流媒体。4.4 代理设置走系统代理还是固定代理网页里很多操作依赖外网资源但桌面应用大多数跑在内网环境所以代理设置是 DCEF 参数里另一个常见需求。DCEF 支持走系统代理、直连、固定代理三种模式。默认是系统代理大部分办公环境能正常工作。如果你发现页面加载了但拿不到网络数据先看是不是走的代理不对。我一般这样取舍面向终端用户的软件走系统代理因为用户自己可能开着代理工具面向服务器或者固定网络环境的组件则设固定代理。DCEF 里固定代理的配置涉及ProxyType、ProxyServer、ProxyScheme等属性其中ProxyServer只填地址和端口不要带http://前缀否则解析会失败。需要注意的是代理参数同样要在StartMainProcess之前设置运行时修改不生效。5. 避坑指南DLL 版本错位、白屏、JS 注入失效、内存泄漏的排错记录5.1 编译通过却启动失败DLL 版本对不上现象是Delphi 侧代码编译全部通过双击 exe 后没有任何窗口出现任务管理器里能看到一个进程闪一下就消失或者直接弹 Windows 的“无法启动程序”错误。原因几乎都是 CEF3 的 DLL 文件缺失或者版本和 DCEF 封装不匹配。我遇到过最典型的情况是手动替换了libcef.dll想着“CEF 小版本更新应该没影响”结果程序起不来。后来把完整的 CEF3 发布包按 DCEF 对应的版本号重新解压问题就解决了。解决路径确认你用的 DCEF 版本对应的 CEF3 版本号去下载同版本的完整 CEF3 发行包把Release目录下的所有 DLL 和资源文件按工程目录布局放好不要混搭任何单独下载的旧版 DLL。5.2 页面白屏或空白缓存目录权限与初始化顺序现象是程序能起来窗口和工具栏都在但整个页面区域是纯白或者纯灰。有时候地址栏加载了一部分页面内容然后永远停在空白状态。排查日志时会看到缓存目录相关的警告但程序不崩溃。原因有三类。第一类是缓存目录指向了不可写的系统路径CEF 初始化失败但不会告诉你是谁的问题第二类是Resources或locales目录路径拼错了导致浏览器无法加载内置资源第三类是你在OnCreate里做了比较耗时的操作浏览器创建被阻塞界面一直停在未渲染状态。解决方法是按顺序检查先确认缓存目录能创建文件夹再把资源目录改到 exe 同目录做对比测试最后把CreateBrowser之前的耗时操作挪到浏览器创建完成之后。5.3 JS 注入无效生命周期选错事件现象是最典型的“代码没问题但页面没反应”。你写了ExecuteJavaScript结果页面上该弹出的弹窗不弹该设置的变量没有设置。检查 DCEF 的日志也看不到 JS 报错。原因基本就是执行时机不对。我见过有人把ExecuteJavaScript写进OnBeforeBrowse页面刚发起导航时就执行此时 HTML 文档还没解析DOM 根本不存在。还有人写进OnAfterCreated浏览器外壳创建完成但文档内容还没加载。正确的时机我已经在前面提过OnLoadEnd里判断frame.IsMain然后再执行脚本。另外如果你的脚本里用了window对象请先打印一行日志确认脚本真的被调用了避免被页面内部其他代码覆盖同名对象。5.4 内存只涨不降V8 句柄没有释放现象是程序开着不动内存占用每隔几分钟涨一点十几个小时后涨到几百兆。我也犯过这个错在一次OnPaint回调里创建了TBitmap每帧都创建但没有释放导致 GDI 句柄泄漏内存自然只涨不降。V8 层面的泄漏更隐蔽。注册的 V8 扩展对象如果不实现释放逻辑或者在 JavaScript 里形成了循环引用CEF 的垃圾回收拿不到这些对象内存就会持续增长。解决办法是所有 Delphi 侧创建的TBitmap、字符串列表、接口引用用完必须释放V8 扩展对象避免持有大的 Delphi 对象引用程序退出前显式调用CloseBrowser并释放GlobalCEFApp。内存问题没有银弹只能定期抓任务管理器做对比验证。5.5 日志级别调到哪档才够用排查效率的最大变量现象是出了问题不知道从哪看起日志文件里全是无关信息。解决办法很简单开发阶段把LogSeverity调到VERBOSE过一遍正常流程然后把日志保存起来切换到WARNING后继续跑只有出现异常时日志里才有记录。这样最终用户环境里日志量很小排查时又能看到关键信息。真正需要VERBOSE的场景是页面加载失败比如某个资源 404、JS 语法错误、跨域被拦截。DCEF 的日志会记录网络层和渲染层的详细信息虽然行很多但搜ERROR关键词就能定位。建议形成习惯日志文件每启动一次覆盖一次不要无限追加避免排查问题时看到一大堆历史数据。6. 进阶玩法离屏渲染做截图、渲染进程数量压内存与稳定性6.1 用 OSR 模式把网页画到 TBitmap把网页渲染到一张 Delphi 位图上最常见的需求是网页截图和“无头浏览器”式的页面预览。在开启WindowlessRenderingEnabled之后TChromium 会进入离屏渲染状态页面内容不再输出到系统窗口而是通过OnPaint回调把像素数据交给你的代码。procedure TForm1.Chromium1Paint(Sender: TObject; const browser: ICefBrowser; kind: TCefPaintElementType; dirtyRects: PCefRectArray; const buffer: Pointer; width, height: Integer); var bmp: TBitmap; dest: PByte; begin // 创建一个 32 位位图逐行拷贝像素数据 bmp : TBitmap.Create; try bmp.PixelFormat : pf32bit; bmp.Width : width; bmp.Height : height; // 位图存储是从下往上需要做行翻转处理 dest : bmp.ScanLine[bmp.Height - 1]; Move(buffer^, dest^, width * height * 4); Image1.Picture.Bitmap.Assign(bmp); finally bmp.Free; end; end;这个回调的width和height是渲染缓冲区尺寸缓冲区是 32 位 BGRA 排列。像素行序是自顶向下而 VCL 的TBitmap.ScanLine是按自底向上所以直接Move出来的是垂直翻转的图片。要纠正可以在拷贝时逐行翻转或者用Image1.Picture.Bitmap.Canvas.CopyRect二次翻转我的经验是逐行拷贝更可控。6.2 用远程调试端口连 Chrome DevTools调试内嵌页面时最痛苦的是看不到控制台报错。CEF3 提供了远程调试协议只要在初始化时开启一个端口就可以用 Chrome 浏览器直接连接// 开启调试端口之后用 Chrome 访问 http://127.0.0.1:9222 GlobalCEFApp.RemoteDebuggingPort : 9222;这是我在做页面相关功能时最常用的调试手段。本机只要能打开一个标准 Chrome 浏览器访问127.0.0.1:9222就能看到 CEF3 渲染的所有页面可以查看 DOM、网络请求、Console 报错甚至直接执行 JavaScript。这个端口只在开发环境打开发布版本必须关掉否则任何本地进程都能调试你的程序页面属于安全风险。6.3 控制渲染进程数量一个卡死不再拖垮整个应用多进程架构下每个标签页或 frame 可能对应一个渲染进程。页面开多了渲染进程数量也跟着涨内存压力大而且某些驱动问题会导致进程卡死。我的习惯是限制页面同时打开的数量不让用户在一个窗口里无限打开标签页用完的页面立即释放浏览器实例。真正遇到渲染进程崩溃时不要慌DCEF 会触发渲染进程相关的错误回调程序主体不会挂。我会在代码里捕获这个事件给用户弹一个可恢复的提示然后重新加载当前页面。这个体验比 TWebBrowser 那种“整个窗体白屏死掉”的境地好太多了也是我从 CEF3 架构里感受到的最实在的收益。现在我自己的项目里已经有一整套基于 DCEF 的浏览器封装初始化参数按环境分开配置JS 通信走统一的 V8 扩展接口离屏渲染用于生成业务需要的页面截图。每次新接项目我都是先按这篇文章的流程搭一个最小 Demo验证 DLL 环境和页面加载再逐步加业务逻辑。这个方向踩过的坑希望帮到你至少让你在决定用 CEF3 之前就知道它值不值得投入。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →