Delphi Web前端实战:TMS WEB Core、Pas2JS与部署排错
简介:这份PDF教程面向希望用Delphi转向Web前端开发的程序员,从零基础到进阶开发者都能使用,核心是借助TMS WEB Core框架把Delphi/Object Pascal技能迁移到浏览器端,解决传统桌面开发者缺乏现代Web应用实战路径的问题。内容从Delphi Web开发框架对比讲起,涵盖IntraWEB、uniGUI与TMS WEB Core的选型差异,并延展到环境配置、项目设置、Pas2JS编译原理、RTL运行时库、页面设计与部署分发等模块,还涉及渐进式Web应用、Electron与Miletus桌面应用等场景,配有企业内部培训资料式的目录结构。资源包共1个PDF文件,大小约3.84MB,单文件即为完整教程正文,便于离线阅读与检索。目前已有5029人学习,适合作为Delphi程序员入门Web前端、搭建跨平台应用时的系统参考手册。
1. 一个 TWebButton 就把老 Delphi 程序员挡在了门外
手上这份 PDF 是某公司内部培训用的资料,主题是「Delphi Web 前端开发」,核心围绕 TMS WEB Core 框架展开:前面花了不少篇幅讲 Object Pascal 从 Turbo Pascal 到 Delphi 11 的历史,中间是四个工程模板的创建流程、Pas2JS 编译链、HTML 模板和几十个 TWeb 控件的属性事件,后面接 REST 数据与部署分发。它面向的读者很明确——写惯了 VCL/FMX 的 Delphi 程序员,想用同一门语言直接产出能在浏览器里跑的 Web 前端。
真正卡住人的往往不是语法,而是第一天的环境:社区版装完找不到组件面板、新建工程跑出来一片白、控件拖上去浏览器里根本不显示。这份资料的价值就在这些地方,它把 TMS WEB Core 的选型理由、工程模板、编译产物、模板绑定按可操作顺序摆开,让一个没碰过前端的人能在半天内把按钮点起来。适合手上有存量业务代码、又要交付 Web 界面的团队;纯前端选手看它反而绕远路。
2. 三剑客怎么选:IntraWEB、uniGUI 和 TMS WEB Core 的运行时差别
Delphi 圈子里做 Web 一直有三个绕不开的名字。教程里专门有一节把它们摆在一起比较,但比较的维度如果只看「控件多不多、好不好看」,选型基本会翻车。真正决定项目命运的是运行时模型:你的 Pascal 代码最终跑在服务器进程里,还是跑在用户的浏览器里。
2.1 一条分水岭:代码跑在服务器还是浏览器
IntraWEB 和 uniGUI 属于同一类,本质是一个 Delphi 应用在服务端运行。用户打开页面,服务端为这个会话创建一份窗体对象实例,把界面渲染成 HTML 推给浏览器,用户点按钮触发的是服务端的事件,处理完再把差异推回去。部署物是一个 exe 或者 ISAPI dll,必须有个常驻进程。这种模型的优点是迁移成本极低:一个 VCL 窗体改成 Web 窗体,事件代码几乎不用动,DataSet、事务、权限判断全在服务端,天然安全。缺点是每个在线用户都占一份服务端对象,并发上去了内存和会话管理就是硬成本,而且页面交互依赖那条长连接或者轮询,弱网下体验会打折。
TMS WEB Core 走的是另一条路。它内置 Pas2JS 编译器,把你的.pas源码转译成 JavaScript,最终产物是一堆静态文件:一个 HTML 入口、若干 js、若干 css。浏览器下载完就开始跑,和普通前端项目没有任何区别,托管在任意静态服务器上都行,不需要常驻进程,也不存在「会话对象」这回事。代价是逻辑暴露在前端,所有校验必须在接口层再做一遍;另外并发能力直接取决于静态资源服务器,反而成了最省心的部分。
我一般这样判断:如果这套系统是内网、用户数可控、大量依赖既有 VCL 业务单元和数据集,IntraWEB 或 uniGUI 更划算;如果要做的是对外的、要被搜索引擎收录的、要挂 CDN 的、要发到手机浏览器里的页面,TMS WEB Core 的静态产物模型优势明显。教程里还提到「轻松部署分发」这一点,说的就是后者把整个输出目录拷到服务器即可,没有注册表、没有运行库依赖。
| 维度 | IntraWEB / uniGUI | TMS WEB Core |
|---|---|---|
| 代码执行位置 | 服务端进程 | 浏览器 |
| 部署物 | exe / ISAPI dll | 纯静态 html + js + css |
| 会话状态 | 服务端保留对象实例 | 无服务端会话,需自己管状态 |
| 并发成本 | 随在线用户线性上升 | 静态托管,几乎无状态成本 |
| 逻辑安全性 | 业务逻辑不下发 | 前端代码可见,接口必须二次校验 |
| 调试方式 | IDE 断点跟服务端 | 浏览器 DevTools + sourcemap 回到 Pascal |
| 移动端适配 | 取决于组件渲染 | 原生 HTML/CSS,响应式直接可用 |
这张表建议在选型会上直接摆出来。我见过有团队用 uniGUI 做了一个面向公众的展示站,上线当天并发一上来,服务端会话把内存吃满,最后是加了机器才扛过去;也见过有人拿 TMS WEB Core 去做内部报表系统,结果每个查询都要手写 REST 接口,反而多了一层工作量。工具没有绝对好坏,错的是模型和场景不匹配。
2.2 环境装起来:社区版加试用版的最小组合
教程开篇就说了,学这套东西只需要免费的 Delphi 社区版加上 TMS WEB Core 的试用版,这个组合是能跑通全流程的。商用之前先确认社区版的授权范围,这一点别省。
装完之后验证是否成功的标志很简单:IDE 的组件面板里多出一个 TMS Web 页签,里面能看到 TWebButton、TWebEdit 这些控件;新建项目对话框里出现 TMS WEB Core 的工程模板。如果只有一个空页签,多半是安装时 IDE 版本没对上,或者装完没重启 IDE。
新建一个 TMS WEB Core Web Application,先别管模板和样式,直接在主窗体上放一个 TWebEdit、一个 TWebButton、一个 TWebLabel,双击按钮写第一段代码:
procedure TForm1.WebButton1Click(Sender: TObject); begin // 这段代码不会发给服务器,它最终被编译成浏览器里的 JS 函数 WebLabel1.Caption := 'Hello, ' + WebEdit1.Text; end;这段代码的逻辑说明很短,但背后的东西值得说清楚:WebEdit1.Text读的是浏览器 DOM 里 input 元素的 value,WebLabel1.Caption写的是另一个元素的 innerHTML。整个过程没有一次网络请求,你在 DevTools 的 Network 面板里能看到干干净净,只有在 Console 里能看到 Pas2JS 生成的函数名。这也是排查问题的第一直觉——如果点了按钮没反应,先看 Console 有没有 JS 报错,而不是去服务端找日志。
按 F9 运行时,IDE 会起一个内置的轻量服务器,把输出目录挂上去,然后调起默认浏览器。这里有个参数值得早点改:项目选项里的输出路径。默认输出在项目目录下的子文件夹里,如果你后面要接 CI 或者要往 Nginx 目录同步,最好在项目属性里把它固定成一个明确路径,比如..\deploy\www,省得每次手工找文件。
2.3 四种工程模板的边界:Web、PWA、Electron、Miletus
教程里列了四种创建入口,很多人第一次看会懵:不都是网页吗,为什么要分四个模板。它们的差别在于「最终交付形态」,选错了后面很难改回来。
普通 Web Application 就是标准网页,产物丢到静态服务器,用户用浏览器访问。PWA 模板在此基础上多生成 manifest 和 service worker,让页面可以「添加到主屏幕」、支持离线缓存,适合要发到手机但不想上应用商店的场景。Electron 模板生成的是一个带 Chromium 壳的桌面程序,前端还是你的 HTML/JS,但外面套了一层 Node 运行时,能读本地文件、能调系统能力,产物是安装包。Miletus 是 TMS 自家的桌面方案,用系统自带的 WebView 承载页面,包体比 Electron 小很多,代价是某些浏览器特性受系统 WebView 版本限制。
| 模板 | 产物 | 适合 | 明显限制 |
|---|---|---|---|
| Web Application | 静态 html/js | 对外网站、内部管理系统 | 需要浏览器,无本地能力 |
| PWA | 静态文件 + manifest + SW | 移动端轻应用、离线可用 | 缓存策略不当会长期读到旧版本 |
| Electron | 桌面安装包 | 需要读写本地文件、串口等 | 包体大,内存占用高 |
| Miletus | 原生桌面可执行文件 | 想要小体积桌面壳 | 依赖系统 WebView 版本 |
我的经验是:先按普通 Web Application 把功能跑通,等业务稳定了再决定要不要套壳。因为 Electron 和 Miletus 都只是外壳,核心代码一行不改,但一旦你在开发初期就引入了 Node 依赖,回头想回到纯网页就麻烦了。教程把 Electron 和 Miletus 的创建流程放在很靠前的位置,容易让人误以为这是必经之路,其实对多数项目来说,先出静态产物才是正路。
3. Pas2JS 编译链:从 .pas 到 .js,产物里到底有什么
3.1 一次编译之后,输出目录里多了哪些文件
Pas2JS 不是虚拟机,也不是把 Pascal 打包成某种字节码再解释执行,它是源码级转译:读你的 Pascal 单元,吐出等价的 JavaScript。所以理解产物结构,比理解语言特性更能帮你定位问题。
一次 Debug 构建之后,输出目录大概长这样:
deploy/www/ ├── MyApp.html # 入口页面,负责加载脚本和样式 ├── MyApp.js # 主逻辑,你的窗体、事件、单元全在这里 ├── MyApp.js.map # sourcemap,Debug 构建才有 └── TMSWeb/ # RTL 和控件库的 js/css 资源 ├── rtl.js └── ...逻辑说明:MyApp.html里最关键的一行是<script src="MyApp.js">,路径写的是相对路径。这就解释了为什么很多人本地双击 html 能打开、拷到服务器某个子目录后白屏——相对路径一错,脚本 404,页面自然什么都没有。MyApp.js.map是给浏览器 DevTools 用的,有了它,你在 Sources 面板里打断点,看到的是 Pascal 源码而不是压缩后的 js,这是 TMS WEB Core 调试体验里最值钱的部分,Debug 构建千万别把它删了。TMSWeb目录放的是运行时库和控件实现,它和主 js 是分离的,所以部署时必须整目录拷,只拷 html 和主 js 是常见的翻车原因。
Release 构建会做压缩、去掉断言和调试信息,体积能小一大截,但 sourcemap 通常不生成。我一般的做法是:测试环境永远用 Debug 构建,方便线上直接开 DevTools 对源码;生产环境用 Release,并且在构建脚本里给 js 文件名加版本号,避免用户拿到缓存里的旧文件。
还有一项容易被忽略的参数是编译目标。项目选项里可以指定 ECMAScript 目标版本,默认偏保守,为了兼容老旧浏览器;如果你的用户都在现代浏览器上,把目标抬到较新的标准,生成的代码会干净很多,异步语法也能原样保留。这个开关改动之后一定要回归一遍,尤其是用了第三方 js 库的地方。
3.2 预处理器和条件编译:一份源码两套分支
Pas2JS 支持 Pascal 的编译期指令,这套东西在「同一份业务单元既要给 VCL 用、又要给 Web 用」的场景里非常关键。教程里专门留了一节讲预处理器,但只列了语法,实际怎么组织单元需要自己踩一遍。
我通常把跨端共用的单元写成这样:
unit uCommon; interface {$IFDEF PAS2JS} uses System.SysUtils, System.JSON, Web; {$ELSE} uses System.SysUtils, System.JSON; {$ENDIF} function NormalizeName(const AValue: string): string; implementation function NormalizeName(const AValue: string): string; begin Result := Trim(AValue); {$IFDEF PAS2JS} // 浏览器端额外处理:把不间断空格也去掉 Result := StringReplace(Result, #$00A0, '', [rfReplaceAll]); {$ENDIF} end; end.逻辑说明:PAS2JS这个条件符号是 Pas2JS 编译时自动定义的,Delphi 原生编译器不会定义它,所以同一个文件在两套工具链下走不同分支。参数上要注意两点:一是interface段的uses也必须条件化,很多人只包了实现部分,结果 Delphi 编译时找不到Web单元直接报错;二是不要在两套分支里定义同名但签名不同的函数,条件编译只负责选代码,不负责类型检查。
如果你不确定当前编译环境到底定义了哪些符号,最土也最有效的办法是写一段探测代码,把{$IFDEF}的结果输出到界面上看一眼:
procedure TForm1.WebButton2Click(Sender: TObject); begin WebMemo1.Lines.Clear; {$IFDEF PAS2JS} WebMemo1.Lines.Add('PAS2JS = defined'); {$ENDIF} {$IFDEF MSWINDOWS} WebMemo1.Lines.Add('MSWINDOWS = defined'); {$ENDIF} end;在浏览器里跑一遍,你就知道哪些分支是活的。这招在接手别人代码、或者升级框架版本后行为发生变化时特别管用,比翻文档快。
3.3 命令行编译与版本号自动注入
IDE 里按 F9 很爽,但只要有第二个人参与,或者要发测试环境,就必须有可重复的命令行构建。TMS WEB Core 安装后会带一个命令行编译入口,常见做法是把它写进批处理,参数以本机帮助输出为准,别照抄别人的博客:
#!/usr/bin/env bash set -e PROJ="MyApp.dpr" OUT="../deploy/www" STAMP=$(date +%Y%m%d%H%M) # 编译器路径按本机安装位置替换,其余开关先用 help 确认 "$TMSWEB_COMPILER" "$PROJ" --outdir "$OUT" # 构建完把版本号写进一个静态文件,供前端读取 echo "{\"version\":\"$STAMP\"}" > "$OUT/version.json"逻辑说明:set -e保证编译失败时脚本立刻中断,不会把半成品推上去;时间戳作为版本号是最省事的方案,不需要维护语义化版本。这段脚本真正的价值不在编译本身,而在最后那行——前端页面启动时先拉一次version.json,跟内存里存的版本比对,不一致就提示用户刷新或者强制 reload,这一招能解决掉大半「我这边改了怎么没生效」的工单。
参数层面还有两个点值得注意。输出目录不要用绝对路径里的中文和空格,Pas2JS 的某些中间文件处理在这种路径下会出玄学问题;构建前最好先清空输出目录,否则上一次构建遗留的旧 js 会被一起拷到服务器,浏览器加载哪个全看 html 里的引用,排查起来非常费劲。
4. HTML 模板与控件绑定:绝对定位、相对定位和模板标签怎么配合
4.1 布局三选一:绝对定位、相对定位和主题
教程把页面设计拆成绝对定位、相对定位、主题三块,这个分法很实用,因为它对应三种完全不同的项目阶段。
绝对定位就是 IDE 里拖控件,框架给你生成left、top、width、height的行内样式,所见即所得。做固定尺寸的看板、大屏、仪表盘很快,但换个屏幕分辨率就散架。相对定位把控件交给文档流和 CSS 类,尺寸用百分比或者 flex 控制,适合要适配手机的项目。主题则是引入 Bootstrap 这类样式框架,控件自动套上对应的 class,你只需要在属性面板里选按钮是 primary 还是 danger。
| 方式 | 生成的东西 | 适合 | 后期改动成本 |
|---|---|---|---|
| 绝对定位 | 行内 left/top | 固定尺寸大屏、原型验证 | 高,改布局要一个个挪 |
| 相对定位 | CSS 类 + 流式布局 | 响应式页面、移动端 | 中,需要懂一点 flex |
| 主题 | 框架 class | 需要统一视觉风格 | 低,换主题即换皮 |
我一般会建议:原型阶段用绝对定位抢时间,界面确认之后立刻转成相对定位加主题。转换的时机很重要,等到控件上百个再转,工作量会大到没人愿意做。另外 BiDiMode 这个属性在教程里单独列了一节,做多语言(尤其是从右往左书写的语种)时它控制的是整个布局的镜像方向,别等到上线前才发现所有边距都要反着算。
4.2 把控件塞进 HTML 模板:ElementID 与自动同步
这是 TMS WEB Core 区别于传统 Delphi 开发最明显的一块,也是最容易劝退新人的地方。默认情况下,你拖一个 TWebButton 上去,框架会自己生成一个 DOM 元素插到页面里。但如果你想用设计师给的真实 HTML 页面做外壳,就需要「模板绑定」这条路:模板里先写好占位元素,控件的ElementID指向同一个 id,框架就不再自己创建元素,而是接管你模板里的那个节点。
步骤大致是这样:先把设计好的 HTML 存成模板文件,在窗体的模板属性里挂上去(不同版本文案略有差别,找窗体上跟 HTML 模板相关的属性或者 IDE 的模板向导);然后在模板里给需要交互的位置写上id,比如<button id="btnSubmit">提交</button>;最后在窗体上放一个 TWebButton,把ElementID设成btnSubmit,并把定位方式设为不参与布局,交给模板的 CSS 控制。
这里有个坑必须提前说:控件的Caption会覆盖模板元素里的文本内容。你模板里写的是「提交」,运行起来变成了「TWebButton1」,很多人在这一步怀疑人生。要么把 Caption 改成一致,要么就接受框架接管文本。教程里提到「自动同步 HTML 模板」,意思是模板文件改动后 IDE 侧能同步刷新,这个功能在联调阶段很有用,但同步的只是模板文件本身,控件属性不会跟着变,别指望它帮你做映射。
4.3 从 TWebEdit 到 REST:常用控件的属性、事件与数据接线
教程列了三十多个控件的属性事件,全背下来没意义,抓住几个高频的就够开工。
| 控件 | 关键属性 | 容易踩的点 |
|---|---|---|
| TWebLabel | Caption、ElementID | Caption 支持简单 HTML 片段,模板已有内容时会被覆盖 |
| TWebButton | Caption、OnClick、Enabled | 放在表单里默认是提交按钮,可能触发页面刷新 |
| TWebEdit | Text、ReadOnly、OnChange | 输入法组合输入期间 OnChange 的触发时机和 VCL 不同 |
| TWebComboBox | Items、ItemIndex | ItemIndex 从 0 开始,未选中是 -1 |
| TWebListBox | Items、MultiSelect | 多选取值要走选中项遍历,不是 ItemIndex |
| TWebMemo | Lines、Text | Lines 是 TStrings,行为接近 VCL |
| TWebCheckBox | Checked、AllowGrayed | 三态需要显式开启 |
| TWebProgressBar | Value、Max | 高频刷新会掉帧,建议节流 |
数据这块,教程给的路线是「REST API 优先」:传统数据库加一层 REST 中间层、云数据库直接暴露 REST、或者别的语言写的接口,前端一视同仁。这个思路是对的,因为浏览器端没有数据库驱动可用,一切都要走 HTTP。
uses System.JSON, Web; procedure TForm1.LoadUsers; begin // 用相对路径,和页面同域,省掉跨域配置 WebHttpRequest1.URL := '/api/users'; WebHttpRequest1.Execute; // 异步,立即返回 end; procedure TForm1.WebHttpRequest1Response(Sender: TObject; AResponse: string); var Arr: TJSONArray; I: Integer; Obj: TJSONObject; begin WebListBox1.Items.Clear; Arr := TJSONObject.ParseJSONValue(AResponse) as TJSONArray; try for I := 0 to Arr.Count - 1 do begin Obj := Arr.Items[I] as TJSONObject; WebListBox1.Items.Add(Obj.GetValue('name')); end; finally Arr.Free; end; end;逻辑说明:Execute是异步的,调用完立刻返回,真正的数据在回调里到达,所以回调里更新 UI 是安全的,但在Execute下一行就读结果是错的。参数上,URL 用相对路径是刻意的,这样页面部署在哪个域名下都能用,也绕开了跨域;如果必须请求外部接口,就要做好服务端的 CORS 配置,这个留到排查那章细说。另外ParseJSONValue的返回类型是父类,接口返回结构变了(比如本该是数组返回了对象)时,as TJSONArray会直接抛异常,回调里最好用try..except包一层,给用户一个「数据格式异常」的提示,而不是让页面静默死掉。
5. 白屏、跨域和事件不触发:五条踩坑记录与排查顺序
5.1 五条翻车现场,按现象、原因、解决记录
现象一:IDE 里跑得好好的,部署到服务器打开全白,DevTools 的 Console 里一堆 404。
原因几乎都是路径问题。入口 html 里引用主 js 和TMSWeb库目录用的是相对路径,你把文件拷到一个多级子目录,或者只上传了 html 没上传库目录,引用就断了。解决方式是整目录部署,别手工挑文件;上传后打开 DevTools 的 Network 面板按 404 过滤,缺哪个补哪个;如果是子目录部署,检查 html 里<base>标签或者脚本路径前缀是否需要调整。
现象二:用了 HTML 模板之后,按钮和输入框在页面上完全看不到,但代码不报错。
原因是被模板接管了。控件的ElementID没有和模板里的 id 对上,框架既不生成自己的元素,也没找到目标节点,结果是「存在但不可见」。另一种情况是控件仍然参与绝对定位,被模板的 CSS 覆盖掉了位置。解决办法是逐个核对ElementID和模板 id 的大小写(HTML 的 id 区分大小写),并把绑定到模板的控件定位方式设为不参与布局,位置交给 CSS。
现象三:本地调用接口正常,部署后请求全部失败,Console 提示跨域。
这是静态产物的必然结果:页面在 A 域名,接口在 B 域名,浏览器直接拦。解决方向有三个——接口服务端加Access-Control-Allow-Origin,并且要正确处理预检的 OPTIONS 请求;把接口和页面部署到同一个域名下,用反向代理把/api转到后端;实在不行才考虑后端做一层同域转发。要注意带自定义 header 或者非简单请求时,浏览器会先发 OPTIONS,如果服务端只放行了 GET/POST,预检照样失败。
现象四:本地http://localhost一切正常,部署到 https 站点后接口全部超时。
混合内容。https 页面里请求 http 接口,浏览器会直接阻断或者降级警告。另一种变体是接口地址被硬编码成了localhost或者内网 IP,用户在外网根本连不上。解决办法是把接口地址做成相对路径或者构建期注入的配置项,本地、测试、生产各用一份,代码里永远不出现写死的域名。
现象五:代码改了,重新构建部署,刷新页面还是老样子。
缓存。可能是浏览器缓存了 js,也可能是 PWA 模板里的 service worker 把旧版本牢牢锁住了。排查时先在 DevTools 的 Network 面板勾上禁用缓存刷新一次,如果变正常就确认是缓存问题。长期方案是给 js 文件名加构建哈希,或者用上一章说的版本号文件做主动检测;PWA 项目每次发版必须更新 service worker 的版本标识,否则用户可能几周都拿不到新版本。
5.2 一套固定的排查顺序,照着走别乱
出了问题最怕东一榔头西一棒子。我现在固定按这个顺序走,基本能在一刻钟内定位层次:
第一步,看 Console。有红色报错先解决报错,JS 层面的异常会让后面的所有逻辑停摆。第二步,看 Network。按 404 和 4xx/5xx 过滤,确认是资源没加载还是接口没返回,这一步能区分「前端问题」和「后端问题」。第三步,看页面元素的 DOM 结构,确认控件到底有没有被渲染出来,ElementID绑定的节点上有没有挂上框架生成的东西。第四步才是去看 Pascal 源码逻辑。
这个顺序背后的逻辑是:TMS WEB Core 的问题绝大多数发生在「静态资源」和「DOM 绑定」这两层,而不是你的业务代码。我见过有人为一个按钮不响应查了两天 Pascal 代码,最后发现是模板里 id 拼错了一个字母。把排查顺序固化下来,比记住一百个 API 有用。
6. 一条命令出包:把版本号、缓存和部署验证串成闭环
前面几章拆开讲了编译、模板和数据,实际交付时它们得串成一条链。我现在的习惯是构建脚本一次性干完四件事:清目录、编译、写版本、自检。
#!/usr/bin/env bash set -e OUT="../deploy/www" STAMP=$(date +%Y%m%d%H%M) rm -rf "$OUT" mkdir -p "$OUT" "$TMSWEB_COMPILER" "MyApp.dpr" --outdir "$OUT" echo "{\"version\":\"$STAMP\"}" > "$OUT/version.json" # 自检:入口页是否引用了主脚本,版本文件是否可读 grep -q "MyApp.js" "$OUT/MyApp.html" || { echo "入口页缺少脚本引用"; exit 1; } test -s "$OUT/version.json" || { echo "版本文件为空"; exit 1; } echo "build ok: $STAMP"这段脚本里,先删目录是关键——旧文件残留导致的「改了没生效」我吃过不止一次亏,与其事后排查不如每次从干净状态开始。grep那行自检看着简陋,但它拦住的是一类很典型的事故:模板改动把入口页的脚本引用弄丢了,编译照样成功,部署上去就是白屏。version.json的用途在上一章说过,页面启动时拉一次比对,不一致就提示刷新。
验证环节我还会加一条:构建完成后用curl直接拉一次线上入口页,确认里面引用的脚本路径和预期的版本一致。这一步放在发布流水线的最后,比人工打开浏览器点一遍可靠得多。
血泪经验是:不要相信「这次改动很小,不用重新构建」。Pas2JS 的产物是整体的,任何一个单元的改动都可能影响最终 js,手工拷一两个文件的做法迟早出事。从那以后我每次发版都强制走一遍完整脚本,宁可多等两分钟,也不要在线上对着白屏猜。希望帮到你。
本文还有配套的精品资源,点击获取