☰
Delphi桌面应用嵌入Chrome内核:CEF4Delphi核心原理与工程实践
2026/10/20 18:14:09 网站建设 项目流程

简介:面向Delphi开发者的CEF4Delphi组件源码包,对应Chromium 71.0.3578.20版本,定位是在桌面应用中嵌入Chromium浏览器并实现与Web内容的深度交互。这份源码包是Delphi程序员在VCL或FireMonkey项目中集成现代Web引擎的桥梁,既包含与CEF进程通信的核心代码,也提供与Delphi平台特性结合的封装层,可用于自定义界面、加载本地或远程网页、调用JavaScript等。压缩包共525个文件、约1.72MB,以211个pas单元源码为主,另有50个dfm窗体定义、49个dproj/dpr工程文件、47个bat构建脚本和45个inc配置头文件,覆盖从核心库到工程构建的完整链路;还附带html示例页面、fmx跨平台界面、png图标与res资源等辅助文件,便于按模块对照学习。已有204人浏览学习。借助这套源码,可以深入理解浏览器控件在Delphi中的嵌入与生命周期管理、CEF多进程架构、JavaScript桥接和跨平台部署等关键问题;由于版本号对应特定Chromium功能集与安全更新,也方便研究者核对API变化并排查兼容性,适合希望自主定制浏览器行为或系统研究CEF与Delphi集成细节的中高级开发者。

1. CEF4Delphi:这个 71 版本组件包到底解决了什么

做 Windows 桌面端的时候,最烦的不是业务逻辑,而是“塞一个能稳定跑网页的内核”。CEF4Delphi 这个开发组件源码包,对应的是 Chromium 71.0.3578.20,把 CEF 的 C 接口翻译成了 Delphi 可以直接调用的单元,省掉了手工维护头文件、进程调度和消息循环的脏活。它解决的核心问题是:用 Delphi 写壳,用网页写界面,两头不折腾。适合手里有老 Delphi 工程、又不想为了内嵌浏览器把整个系统重写一遍的开发者,也适合做工具软件、仿真平台、内部管理系统这类需要大量前端交互的项目。

2. 选型与架构边界:和自封装、WebView2 差在哪

2.1 为什么是 CEF4Delphi,而不是自己改写 C API

直接拿 CEF 原始 SDK 在 Delphi 里跑,是很多人一开始的计划,然后基本都放弃了。原因不复杂:CEF 暴露的是纯 C 结构体和回调函数指针,光是把几十个接口映射成 Pascal 类就是个体力活;再加上 CEF 是多进程架构,每个进程都要走一遍初始化,谁在主进程、谁在渲染进程,这个分流逻辑写错一次,后面全是黑匣子。CEF4Delphi 的价值在于把这条硬路铺平了:源码包里的单元文件已经把接口定义、类封装、消息循环和资源处理都整理好,你只需要在项目里引入需要的单元,按它的方式创建浏览器。

用 TWebBrowser 这类老组件也不是不行,但它的内核依赖操作系统自带的 Web 控件,版本不跟随你的发布包走。同一段 CSS,在一台机器上正常,换一台老系统就乱掉,这是血泪经验。WebView2 的思路更现代,但它的运行时是独立安装包,离线环境里分发很被动,而且对老系统版本要求不低。CEF4Delphi 走的是“二进制随应用走”的路子,版本、能力、行为都由你手里的 CEF 二进制决定,这也是很多工业软件愿意为它买单的原因。

还有一个现实理由:源码包里的组件不是抽象接口,是有实际类的。你可以在 Delphi 的窗体设计器里直接放下浏览器面板,拖拽一个浏览器组件,然后设置默认地址,不需要在 FormCreate 里写几百行手工创建代码。对于团队里有大量传统 Delphi 开发者的项目,这个学习成本比“从 C API 包一层自己维护”低太多。

2.2 进程模型:为什么打开一个窗口会看到一堆同名进程

CEF 的内核从不开单进程。打开一个带网页的窗口,任务管理器里通常会出现好几个同名 exe:主进程负责窗口外壳和菜单,渲染进程负责解析 HTML、跑 JS,还有 GPU 进程处理合成,网络进程处理请求。这里面最容易犯的错误是——你的程序只有一个入口,但启动后它会分身。所以工程入口必须能判断“我是主进程还是子进程”,判断错了,整个界面起不来。

CEF4Delphi 在这个问题上用了一个统一入口的套路:同一个 exe,通过命令行参数区分角色。启动时先调用初始化方法,如果当前进程是主进程,它会继续创建窗体;如果是 CEF 拉起的渲染/GPU/网络子进程,初始化方法会直接接管消息循环,然后退出,不让业务代码继续往下走。这个设计的好处是发布部署只需要一个 exe 加资源文件,不需要额外带一堆“renderer.exe”。

消息循环方面也要留意。CEF 的主进程模式有两种:独立消息循环和应用集成消息循环。在 Delphi 环境里,通常走的是“把 CEF 的 work 消息塞进 Application 的 idle 事件”这一路,让两者共用同一线程。你要是自己新开一个线程去跑 CEF 的循环,UI 操作就容易乱套。源码包里已经处理好了这一步,但前提是你不要作死去改线程模型,我见过不少“优化”到半路就崩溃的工程,最后都老老实实回退成默认模式。

2.3 版本 71.0.3578.20 的性能与兼容边界

版本号对应的 Chromium 主版本是 71,这是 2018 年底的内核。说实话,今天用有点复古,但对内部系统、工控界面、离线工具来说是够用的:ES6+ 的大部分语法、Canvas、WebGL、WebSocket 都没问题,常见的管理后台、图表库、地图 SDK 能跑。问题集中在“新特性不保证”上——CSS 新容器查询、较新的 WebGPU、部分前沿 JavaScript API 在 71 上是不存在的。接这个包之前,最好把你目标页面的最低浏览器版本往 71 上对一下。

这个版本的价值不在新,而在稳。Chromium 71 处于当时比较成熟的稳定分支,内存占用、崩溃率、兼容性都已经收敛。对 Delphi 项目来说,老版本内核还有个隐性格优势:对老 GPU、老驱动的容忍度比新内核好,新内核强制走硬件加速反而白屏率高。这里不是鼓励你用老版本,而是说如果资源场景是“稳定显示固定页面”,71 反而是低风险选择。

还必须在选型阶段说清楚一点:这个压缩包是“开发组件源码”,不是“完整浏览器发行包”。源码包负责的层是 Delphi 到 CEF 之间的封装,实际跑起来的 CEF 二进制库文件(比如 chrome 渲染支持库、资源响应文件、区域设置文件)需要你另外准备对应版本的构建产物。部署目录里少了这些文件,程序能编译但起不来,这是新手最容易误判的地方。

另外,Linux 和 macOS 也能用这套封装,但发布复杂度明显上升,大部分团队只跑 Windows。如果你只做 Windows 桌面,那就把精力放在 32/64 位占位上:源码包本身两个平台都能编译,但 CEF 二进制必须是同一架构,32 位壳配 64 位二进制库,启动即失败,这个坑后面单独讲。

3. 工程装配:把源码包接进 Delphi 工程的三件套

3.1 源码包结构:先分清封装、示例和二进制

拿到源码包之后先别急着往工程里拖文件。先扫一遍目录结构,心里要有一个分类:哪些是单元源码,哪些是示例工程,哪些是辅助工具。常见布局下,源码单元集中在source目录,里面是uCEF开头的 Pascal 文件,这部分才是真正要编译进项目的;demos或examples目录是给你抄作业的,里面有小浏览器、JS 交互、多标签页之类的现成案例;如果包里有bin或lib,那可能是别人事先编好的二进制,但很多源码包不带,因为版权协议和体量问题。

我一般会先打开 demo 里的最小工程,尝试完整编译一次。这一步能同时验证三件事:源码里有没有依赖缺失、IDE 版本是否兼容、编译输出的位数是不是目标架构。一次性编译通过的人是少数,多半会报找不到单元、文件路径不对或者控件注册失败。别着急,先把 demo 跑通,再聊你的业务壳子。

这里有一个很容易走错的方向:把整个源码目录一股脑加进项目的 search path。不必要,而且会拖慢编译,还有可能把示例工程的同名单元卷进来。正确的做法是只添加包含核心单元的那一层;如果 IDE 是较新的版本,可以创建一个小组件包,把需要的类注册成设计期控件,但那种模式适合长期维护。只是想快速用起来的话,直接把路径写进Project > Options > Delphi Compiler > Search path就够了。

注意分发的关键依赖:不管编译如何成功,最终跑起来都需要 CEF 二进制包里的icudtl.dat、v8_context_snapshot.bin、resources.pak这些资源文件,以及若干.dll。这些不是 Delphi 代码能替代的。你可以理解为:源码包是发动机电控,CEF 二进制是发动机本体,缺一不可。有些工程发布后换一台电脑就白屏,排查到最后就是少了icudtl.dat或者在搜索路径里配置了中文路径。

3.2 Library Path 与工程入口:真正会踩的大坑

装配第一步是配路径。在 IDE 里打开工程选项,找到 Delphi Compiler 下的 Search path,把源码目录加进去。路径分隔符用分号,多个路径顺序取靠前者优先。不要用空格,不要带中文目录,这个组件的资源加载对路径字符很敏感,路径里有非 ASCII 字符轻则日志报错,重则资源加载不出来。

工程入口文件的写法是装配环节最核心的一段。下面是一个典型的 Delphi 入口骨架,我一般把 CEF 的全局初始化放在begin...end.的最前面:

program BrowserShell; uses Vcl.Forms, uCEFApplication, // CEF4Delphi 的全局应用对象 MainForm in 'MainForm.pas' {Form1}; {$R *.res} var GlobalCEFApp: TCefApplication; // 全局唯一实例 begin GlobalCEFApp := TCefApplication.Create; // 以下都是可选项:区域语言、日志位置、子进程开关 GlobalCEFApp.Locale := 'zh-CN'; GlobalCEFApp.LogFile := '.\cef_debug.log'; GlobalCEFApp.LogLevel := 3; // 记录错误和警告 // 关键判断:当前进程是不是主进程 if GlobalCEFApp.StartMainProcess then begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end; GlobalCEFApp.Free; GlobalCEFApp := nil; end.

逻辑说明:TCefApplication对象负责 CEF 的全局生命周期。StartMainProcess是做进程分身的闸门——主进程环境下它会初始化浏览器上下文并返回 True,让 Delphi 的消息循环继续;子进程环境下它会进入 CEF 内部的消息循环,不再返回,因此后面的窗体创建代码在子进程里永远不会执行。参数方面,Locale影响页面navigator.language和浏览器 UI 语言,LogFile只在排错时开,正式发布不设或者设成空路径。LogLevel建议调试时调成 2 或 3,能看到渲染进程的崩溃原因。

3.3 子进程识别:一个好的 StartMainProcess 签名

上面的入口方案之所以能“一个 exe 多个进程”,是因为 CEF 初始化时会在命令行里附加自己的--type=renderer之类的区分参数。CEF4Delphi 内部会解析这些参数,你自己不要手工去判断传参,否则子进程会被误当成主进程跑起来,然后你会看到窗体闪现又退出,或者直接挂掉。

子进程相关的另一个配置在全局配置对象上。比如桌面工具类应用一般会用GlobalCEFApp.RendererProcessCommandLineSwitches?其实不必纠结具体方法名,重要的是理解:有一些开关只能加给子进程,有一些开关只对主进程生效。常见的disable-gpu是加在主进程和 GPU 子进程上的,no-sandbox则影响所有子进程。我的做法是:先不加任何开关,跑通 demo;跑不通再逐个加,避免把问题掩盖在开关组合里。

进程数也需要心里有数。一个浏览器窗口连带网络和渲染进程,正常会看到 4~8 个同名列。如果你关闭了所有窗口,这些进程应该全部消失。如果在任务管理器里发现孤儿子进程残留,第一排查方向是退出清理顺序,第二才是组件本身的问题。进程模型的理解到位之后,后面排错就快得多,因为你知道白屏时该看谁。

4. 最小浏览器实战:从空窗体到能跑本地页面的核心链路

4.1 最小 Delphi 窗体骨架

装配好路径和入口之后,实际页面展示只需要一个窗体、一个容器和一个浏览器实例。我以一个干净的空白窗体为例,最核心的代码就三行逻辑。

// MainForm.pas 的关键片段 uses uCEFChromium, uCEFWindowParent; procedure TForm1.FormShow(Sender: TObject); begin // 1. 让容器先完成尺寸计算 CEFWindowParent1.UpdateSize; // 2. 在容器内创建浏览器,指定起始页 Chromium1.CreateBrowser(CEFWindowParent1, 'https://example.com'); end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin // 3. 关闭前通知浏览器内核,避免线程残留 Chromium1.CloseBrowser(True); end;

逻辑说明:UpdateSize是在窗体显示之后调用的,因为这时候容器才有实际宽度和高度,太早了取到的尺寸是 0。CreateBrowser接收两个参数:承载浏览器画面的父控件,以及初始 URL。CloseBrowser(True)里的布尔值表示是否等待浏览器内部确认销毁,传 True 能减少退出时崩溃的概率。

组件在窗体设计器里也可以直接拖,但设计器拖出来的只是空壳,CreateBrowser那行代码才是真正实例化内核的起点。很多新手在FormCreate里调用CreateBrowser,结果窗口要么空白,要么位置大小不对,原因就是容器还没准备好。我一般统一放在FormShow里,或者用一个OnAfterCreated事件再处理,这是最稳的顺序。

4.2 加载本地 HTML 与外网 URL

内嵌浏览器最常见的需求不是打开公网网站,而是加载打包好的本地页面。加载本地文件的写法有讲究:直接写D:\xxx\index.html不生效,CEF 会把它当成非法 URL,必须显式写成file:///协议形式。

// 加载本地页面 Chromium1.LoadURL('file:///D:/SimulatorUI/index.html'); // 需要跳转到某个接口时 Chromium1.LoadURL('https://api.internal.example.com/dashboard'); // 重新加载当前页 if Chromium1.Browser <> nil then Chromium1.Browser.Reload;

逻辑说明:LoadURL是对浏览器的统一入口,本地文件用file:///前缀,远程地址直接用https://。Browser属性在浏览器创建完成之前是 nil,所以访问前要判空。Reload是ICefBrowser接口提供的方法,不经过组件派发,因此也要先判空。参数方面,如果你的本地页面 Mapbox、百度地图之类的资源,走file:///很可能遇到跨域限制,这时最好换成本地 HTTP 服务或者自定义协议方案,别和 CEF 的本地文件策略死磕。

一个常见的错误是拿LoadURL('file:///D:/page.html#/route')去加载带 hash 路由的单页应用。路径里的反斜杠和空格要进行 URL 编码,否则路由匹配不到。我更推荐把页面目录放在 exe 同级下的assets目录,运行时动态拼接绝对路径,这样换机器不会因为盘符不一样而出问题。

4.3 让 JS 和 Delphi 互相呼叫

桌面壳里不可能只是单方向加载网页,业务上一定有“网页按钮触发 Delphi 保存文件”“Delphi 推送数据到网页”这类双向通信。第一种做法是执行 JavaScript,Delphi 主动往页面里注入调用;第二种做法是拦截页面发起的协议请求,Delphi 侧接收到再处理。

先看 Delphi 主动调 JS:

// 在页面加载完成后调用页面里的全局函数 if Chromium1.Browser <> nil then Chromium1.Browser.MainFrame.ExecuteJavaScript( 'window.updateStatus("idle");', 'pascal-injection', 0 );

逻辑说明:ExecuteJavaScript有三个参数:要执行的 JS 代码、一个标识这段代码来源的字符串、以及行号。第二个参数填什么都行,建议填项目代号,出错时日志里能看出是哪段注入。MainFrame是页面主框架,执行环境是页面全局作用域。这里要注意时序:必须等OnAfterCreated之后才能执行,否则MainFrame还不存在。我习惯在组件的OnLoadEnd事件里再发第一批 JS 调用,保证 DOM 已经可用。

再看页面反推 Delphi,最简单的方案是监听导航拦截:

// 在 OnBeforeBrowse 事件里拦截自定义协议 procedure TForm1.OnBeforeBrowse(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const request: ICefRequest; var aIsRedirect: Boolean); begin if StartsWith(request.Url, 'applink://') then begin // 从 URL 中解析参数,例如 applink://save?name=xxx HandleAppLink(request.Url); browser.MainFrame.ExecuteJavaScript('history.back();', '', 0); Abort; // 终止当前导航 end; end;

逻辑说明:applink://是自定义 schema,页面里只需要写window.location.href = 'applink://save?name=report.pdf'就能把请求发给 Delphi。拦截到之后先解析参数,再让页面返回到原来的位置,最后用Abort阻止 CEF 继续加载一个根本不存在的网络地址。这个方案实现成本最低,适合内部系统;缺点是 URL 长度受限,不适合传大对象。要传大 JSON 就用 CEF 的OnProcessMessageReceived或者本地 WebSocket 中转,这些在源码包的 demos 里有现成模板。

5. 避坑手册:五个最常见的 CEF4Delphi 翻车现场

5.1 白屏还是子进程崩溃,先分清是哪一层

现象:程序启动后窗体正常,但浏览器区域一片白板,没有加载进度,也没有报错弹窗。

原因:CEF 的渲染进程一旦崩溃,主进程往往不会弹错误框,只看界面就是白屏。老显卡驱动与 GPU 进程冲突是头号嫌疑,其次是资源文件缺失导致渲染初始化失败。还有一类情况是主进程成功,但页面本身的 JS 报错导致白屏,这时候 CEf 控制台里会有信息,界面上看不出来。

解决:先把 GPU 加速关掉试一轮。在主进程初始化代码里加两行:

GlobalCEFApp.CommandLineSwitches.Add('disable-gpu'); GlobalCEFApp.CommandLineSwitches.Add('disable-software-rasterizer');

逻辑说明:第一行让 CEF 不创建 GPU 进程,第二行防止它退回软件渲染后仍然出错。加上之后如果白屏消失,说明显卡驱动或渲染上下文有问题,可以换新的 CEF 构建版本或升级驱动。如果加完还是白屏,就去看cef_debug.log,里面有子进程退出码和异常信息,这一步能省掉大量盲目尝试。

5.2 位数不一致:最不起眼却最致命

现象:编译成功,双击 exe 没反应,或者启动的一瞬间出现“找不到指定的模块”,随后进程消失。

原因:Delphi 工程编译成了 Win32,但部署目录里的 CEF 二进制是 x64 的,或者反过来。CEF 的 DLL 依赖关系一旦位数不匹配,加载器直接拒绝启动,这还不是最头疼的——最头疼的是源码包本身可以混编,报错时机和加载顺序有关,有时候在部分机器上能跑一部分才开始失败。

解决:编译前先确认目标平台。在 Delphi 工程选项里选 Win32 就配套放 x86 的 CEF 包,选 Win64 就放 x64 包。部署目录里的libcef.dll保存一下右键属性,能看到文件版本和架构;命令行下也可以用简单的检查方式。我发布前会把主 exe、libcef.dll 还有所有子进程库的位数列一张清单核对,这一步已经成为固定动作。

5.3 中文乱码、字体方块和时区错误

现象:加载本地 HTML 时英文正常,中文全部变成方框;或者页面里取到的时区不是北京时间。

原因:CEF 子进程没有收到正确的区域语言参数。你的 Windows 系统可能是中文环境,但 CEF 独立于系统 UI,默认语言走的是英语路径,中文渲染字体又没有匹配到合适的回退字体,于是出现方块。时区问题同样来自--lang相关参数缺失。

解决:在全局配置里显式设置语言区域和接受语言列表,这两项通常在TCefApplication的实例上直接赋值:

GlobalCEFApp.Locale := 'zh-CN'; GlobalCEFApp.AcceptLanguageList := 'zh-CN,zh,en-US,en';

逻辑说明:Locale决定 CEF 内部加载哪套语言资源,AcceptLanguageList会写进 HTTP 请求头的 Accept-Language,影响网页根据语言做内容切换。改完这两项后,字体方块依旧出现,就要检查系统是否缺少中文字体,或者页面字体栈里只写了英文西文字体。CEF 不会帮你把系统字体虚拟化,它只是按字体回退规则找可选字体。

5.4 退出时崩溃与残留进程

现象:关闭主窗体后,程序偶尔弹出内存访问冲突;或者窗体消失了,但任务管理器里还有好几个同名进程一直占着内存。

原因:退出顺序错了。Delphi 的窗体析构发生时,如果浏览器内核还没有关闭,回调可能被派发到已经释放的 VCL 对象上,这就是一个典型的野指针场景。残留进程通常是CloseBrowser(False)未等待内核销毁,主进程走了,子进程还会坚持一段时间或被异常拉起。

解决:关闭窗体时主动通知浏览器销毁,而不是等 VCL 回收。我一般在窗体的OnCloseQuery里先关闭浏览器,阻止默认关闭流程:

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin CanClose := False; Chromium1.CloseBrowser(True); // 收到 OnBeforeClose 事件后再真正释放窗体 FCloseConfirmed := True; Close; end;

逻辑说明:CloseBrowser(True)请求 CEF 使用温和的关闭流程,它会等待所有子进程结束。这里要注意区分“用户点 X”和“Delphi 主动释放”,如果条件允许,用OnClose事件里的延时关闭也行。残留进程的根治办法是保证TCefApplication的销毁发生在窗体释放之后,也就是入口程序里的GlobalCEFApp.Free必须在Application.Run返回之后执行,顺序不要颠倒。

5.5 视频无声与音频通道

现象:网页里的视频能播放画面,但完全没有声音;有些视频站点干脆提示音频解码失败。

原因:CEF 开源构建默认不包含 H.264/AAC 的专利解码代码,或者音频输出走了不同的后端。你在开发时用的可能是带私有编解码器的一体化构建,这个源码包只是封装层,并不会自动补齐解码能力。

解决:确认你部署的 CEF 二进制是否标注了带 proprietary codecs,没有的话要么换构建,要么接受无声音。另一个常见原因是音频设备使用 WASAPI 独占模式,CEF 子进程访问不到;可以在全局配置里启用音频降级策略、指定输出设备,或者在页面侧检查audio元素的错误事件。这里没有无损的“代码补丁”方案,组合拳是:带音视频解码的二进制、正确的音频驱动、以及设置GlobalCEFApp.AudioOutputDevice之类的参数。

6. 进阶:离线部署、缓存清理和发布前验证

6.1 把页面资源压缩成离线包

内网系统最怕的就是明明资源在本地,还要打开一个本地 Web 服务器。CEF 支持直接运行自定义协议,但配置略繁琐。我常用的是“运行时释放”方案:把整套页面静态资源打进一个压缩包,程序启动时释放到系统临时目录,再用file:///或相对路径加载。好处是发布文件少,不会被杀毒软件反复扫描 exe 旁边的文件,页面升级也只需要换压缩包。

缓存路径也值得控制。给浏览器指定一个固定的UserDataPath,页面里登录态、LocalStorage 都能持久化;不想留痕就把缓存路径清空:

GlobalCEFApp.UserDataPath := '.\runtime\profile'; GlobalCEFApp.CachePath := ''; // 关闭磁盘缓存 GlobalCEFApp.OffScreenRendering := False; // 保持窗口渲染

逻辑说明:UserDataPath存放 Cookie、IndexedDB 等持久化数据,发布包更新时要注意清理路径下残留的旧版本数据,否则页面升级后可能出现缓存串号。CachePath留空意味着 CEF 只在内存里缓存静态资源,关掉进程就释放。对工具软件来说这个策略干净,代价是每次启动首次加载会稍慢,属于可接受的交换。

6.2 发布前我强制走一遍的验证清单

发布前我会刻意跑一遍容易被忽略的场景:第一次启动冷启动后关掉再打开,第二次启动看 LocalStorage 是否还认识你;换一个没有管理员权限的账号运行;把系统用户名改成带空格的、路径改成中文目录,看页面能否正常加载。这三个场景里最容易翻车的是路径,很多本地资源拼接用的是硬编码字符串,路径一变化就指向不存在的文件。

验证进程完整性也有固定流程:启动程序后打开任务管理器,数一下同名进程数量,正常是主进程加渲染进程加网络进程,细节取决于页面复杂度;然后直接关闭主窗体,十秒后再次查看,残留进程应该为 0。有这个习惯之后,我再也没在发布现场遇到过“演示完退出程序后,后台还挂着一堆进程吃内存”的尴尬。

最后提一句我从踩坑里换来的习惯:任何时候编译出来的 exe,先放一个干净目录里跑,不带源码路径、不依赖 IDE 的任何环境变量,再确认能正常加载页面。以前我总觉得这是多此一举,直到有一次把封装源码里的相对路径带到了部署机器上,整个 UI 白屏了半小时。从那以后每次发布前,我都强制走一遍这个干净目录流程,再交付给使用方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询