ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Delphi 老兵用 TMS WEB Core 写 Web:环境搭建、组件模型与前后端通信实战

Delphi 老兵用 TMS WEB Core 写 Web:环境搭建、组件模型与前后端通信实战

简介:这份PDF教程面向希望用Delphi切入Web前端开发的初学者与进阶开发者,围绕TMS WEB Core框架讲解如何以Delphi或Lazarus快速构建网站、Web应用及跨Windows、Mac、iOS、Android、Linux的App。内容从Delphi/Object Pascal的历史与现状讲起,对比IntraWEB、uniGUI与TMS WEB Core三款Web框架的特点,再深入配置TMS WEB Core环境、项目设置与自动化版本管理,并覆盖Pas2JS编译器、RTL运行时库、预处理器、命令行编译、页面设计与调试等关键环节,帮助传统Delphi程序员理解100%纯HTML/JS应用的开发思路。资源为单个PDF文件,压缩包约3.84MB,已有5000余人学习。目录结构清晰,涵盖框架简介、快速入门、组件技能与部署分发等模块,适合作为系统学习TMS WEB Core、查漏补缺的参考读物。

1. 当 Delphi 老兵开始写 Web:TMS WEB Core 到底解决了什么

一个做了十几年 Delphi 桌面端的人,第一次被要求「把现有系统搬到浏览器里」,大概率会经历三个阶段:先想用 uniGUI 或 IntraWeb 糊一个,再想干脆重写成 Vue + 后端 API,最后发现两条路都要把积累多年的业务逻辑推倒重来。TMS WEB Core 出现的意义,恰恰是给这批人第三条路——用 Object Pascal 写前端,编译成 JavaScript,在浏览器里跑,同时保留 Delphi 的 IDE、调试器和 VCL 式的组件思维。它不是把 Delphi 变成 Web 框架,而是把 Web 前端变成 Delphi 能编译的目标平台。这篇文章面向的是有 Delphi 基础、想低成本切入 Web 交付的一线开发者,从环境搭建、组件模型、前后端通信到部署排错,把这条链路走通一遍。

2. 环境搭建与第一个可运行页面:从 IDE 到浏览器

2.1 安装 TMS WEB Core 与编译器链路

TMS WEB Core 的安装包通过 TMS 官方的订阅渠道获取,安装时会向 Delphi IDE 注册一套新的工程类型和一组组件面板页。安装完成后,在 Delphi 的File > New > Other里能看到TMS WEB Core分类,里面包含TMS Web Application、TMS Web Core Web Application等模板。这里有个容易忽略的点:TMS WEB Core 依赖一个 Pascal-to-JavaScript 的编译器(pas2js),它随安装包一起部署,但需要确认 IDE 的库路径里已经包含TMSWEBCore的源码目录,否则编译时会报找不到WEBLib.Controls之类的单元。

安装后建议先做一次环境自检,确认编译器可用:

# 在 Delphi 安装目录下查找 pas2js 编译器可执行文件 # 常见路径形如: # C:\Program Files (x86)\Embarcadero\Studio\<版本>\TMSWeb\Compiler\pas2js.exe # 用 --version 确认编译器能独立运行 pas2js.exe --version

这段命令的作用是确认 pas2js 编译器本身没有损坏。如果这一步就报错,后面所有编译问题都不用查了,直接重装 TMS WEB Core。参数上没什么可调的,--version只是探活。真正需要关注的是 IDE 里Tools > Options > TMS WEB下的编译器路径配置,如果团队里多人协作,建议把这个路径写进项目组的统一环境说明,避免有人用旧版本编译器产出不一致的 JS。

2.2 用 TMS Web Application 模板跑通最小页面

新建TMS Web Application后,IDE 会生成一个包含Project1.html、Project1.dpr和主窗体单元的结构。主窗体的类型是TWebForm,上面可以放TWebButton、TWebEdit、TWebLabel这类组件。写一个最小交互:按钮点击后把输入框内容回显到标签上。

// Unit1.pas procedure TForm1.WebButton1Click(Sender: TObject); begin // WebEdit1.Text 在浏览器端就是 input 的 value WebLabel1.Caption := '你输入的是:' + WebEdit1.Text; end;

这段代码的逻辑很直白,但背后发生的事值得说清楚:WebButton1Click并不是在服务端执行的,它被 pas2js 编译成 JavaScript 事件回调,直接绑定到浏览器 DOM 上。WebEdit1.Text读写的是真实 DOM 节点的 value 属性,WebLabel1.Caption最终落到 innerHTML。也就是说,你写的是 Pascal,跑的是前端。参数层面,TWebEdit的Text属性是双向的,但要注意它默认不触发OnChange的实时同步,如果需要输入即响应,得用OnKeyUp或开启TWebEdit的ImmediateUpdate类属性(不同版本命名略有差异,以组件面板实际属性为准)。

编译运行后,IDE 会启动一个内置的轻量 Web 服务器,把生成的 HTML、JS、CSS 托管在本地端口,浏览器自动打开。这个内置服务器只用于开发调试,不要拿它当生产部署方案。

2.3 工程结构里哪些文件该进版本库

一个 TMS WEB Core 工程编译后会产出大量中间文件,如果全丢进 Git,仓库会迅速膨胀。常见做法是只保留源码和配置:

文件/目录是否入库说明
*.dpr/*.pas是工程与单元源码
*.html是页面宿主文件,含 JS 引用
*.dproj是工程配置,含编译选项
TMSWeb\输出目录否编译产物,含大量 JS
*.js.map否调试映射,按需生成
.identcache/.dsk否IDE 本地状态

这张表的价值在于避免团队协作时有人把编译产物提交上去,导致每次拉取都产生冲突。我一般会在.gitignore里直接写死输出目录名,而不是逐个文件排除。

3. 组件模型与页面布局:TWebForm 和 VCL 的差异在哪

3.1 TWebForm 不是 TForm,别用桌面思维套

从 VCL 转过来的人最容易翻车的地方,是把TWebForm当成TForm用。两者在生命周期上有本质区别:VCL 的窗体是操作系统窗口,TWebForm最终是一个<div>容器,它的Show、Hide、Close对应的是 DOM 的显示隐藏和路由跳转。更关键的是,TWebForm没有真正的「模态」概念,ShowModal在 Web 端要么被模拟成遮罩层,要么直接不可用,取决于你用的组件版本。

布局上,VCL 的绝对坐标(Left/Top)在 Web 端会变成 CSS 的 absolute 定位,一旦浏览器窗口尺寸变化,页面就会错位。正确做法是优先使用TWebPanel配合Align属性,或者用TWebResponsiveGrid这类响应式容器。我见过一个项目,主界面全部用绝对坐标摆控件,在开发机上看着没问题,用户用笔记本一打开,右侧按钮全跑到屏幕外,这就是典型的桌面思维残留。

3.2 用 TWebResponsiveGrid 做自适应布局的最小示例

下面这段代码演示如何用响应式网格把三个卡片横向排列,窄屏时自动换行:

// 在 FormCreate 里动态构建网格内容 procedure TForm1.WebFormCreate(Sender: TObject); var i: Integer; Card: TWebPanel; begin WebResponsiveGrid1.Columns := 3; // 宽屏三列 WebResponsiveGrid1.Breakpoint := 768; // 小于 768px 时降为单列 for i := 1 to 3 do begin Card := TWebPanel.Create(Self); Card.Parent := WebResponsiveGrid1; Card.ElementClassName := 'card'; // 挂自定义 CSS 类 Card.Caption := '卡片 ' + IntToStr(i); end; end;

逻辑说明:Columns控制宽屏下的列数,Breakpoint是触发降列的像素阈值,这两个参数决定了布局的响应行为。ElementClassName是关键,它把 Delphi 组件和外部 CSS 类关联起来,样式细节交给 CSS 文件去写,而不是在 Pascal 里硬编码颜色和边距。参数调整上,Breakpoint要根据目标用户的主流设备来定,如果用户大量使用平板,768 可能偏小,可以调到 1024。失败时先看浏览器开发者工具里网格容器的实际宽度和 CSS 类是否生效,很多时候是 CSS 文件没被正确引用。

3.3 组件事件在浏览器端的真实执行时机

TWebButton.OnClick、TWebEdit.OnChange这些事件,编译后都是标准的 DOM 事件监听。这意味着它们的执行时机和浏览器事件模型完全一致:OnClick在 mouseup 之后触发,OnChange在 input 失焦或回车时触发。如果你在OnChange里做实时校验,用户输入过程中不会触发,体验上会觉得「没反应」。这时候应该改用OnKeyUp或OnInput(如果组件暴露了的话)。

另一个坑是事件里的异常处理。在 VCL 里,未捕获异常会弹出一个对话框;在浏览器里,未捕获异常只会静默写进 console,用户看到的是「点了没反应」。所以每个事件处理里最好包一层 try-except,把错误信息通过console.error或页面提示暴露出来,否则排查起来就是黑匣子。

4. 前后端通信:TMS WEB Core 怎么和 Delphi 后端对接

4.1 三种通信方式的选型对比

TMS WEB Core 前端和 Delphi 后端之间,常见有三条路:REST/JSON、WebSocket、以及 TMS 自家的 RemoteDB。选型取决于数据实时性和团队技术栈。

方式适用场景实时性实现复杂度
REST/JSON常规增删改查请求-响应低
WebSocket消息推送、协同编辑双向实时中
RemoteDB直接操作数据集准实时中高

大多数业务系统用 REST 就够了。RemoteDB 适合那种「前端要直接翻数据集」的场景,但它对网络稳定性和权限控制要求更高,不适合公网弱网环境。

4.2 用 TWebHttpRequest 调 REST 接口的完整写法

假设后端有一个 Delphi 写的 REST 服务,返回 JSON 格式的用户列表。前端这样调:

procedure TForm1.LoadUsers; var Req: TWebHttpRequest; begin Req := TWebHttpRequest.Create(Self); Req.URL := 'https://your-api-host/api/users'; Req.Command := httpGET; Req.Headers.Add('Authorization: Bearer ' + FToken); Req.OnResponse := HandleUsersResponse; Req.Execute; end; procedure TForm1.HandleUsersResponse(Sender: TObject; const AResponse: string); var Arr: TJSArray; i: Integer; Obj: TJSObject; begin // AResponse 是原始 JSON 字符串,用 TJSJSON 解析 Arr := TJSJSON.parse(AResponse) as TJSArray; for i := 0 to Arr.Length - 1 do begin Obj := Arr[i] as TJSObject; // 把数据填进 TWebResponsiveGrid 或表格组件 AddUserRow(Obj['name'], Obj['email']); end; end;

逻辑说明:TWebHttpRequest是异步的,Execute发出请求后不阻塞,结果在OnResponse回调里处理。Command指定 HTTP 方法,Headers用来带认证信息。解析部分用的是TJSJSON,这是 pas2js 提供的 JS 互操作单元,TJSArray和TJSObject直接映射到 JavaScript 的数组和对象。参数上,URL建议从配置读取而不是硬编码,Authorization头的 token 要处理过期刷新,否则用户会莫名其妙被登出。

失败排查顺序:先看浏览器 Network 面板里请求是否发出、状态码是多少;如果是 401,查 token;如果是 0 或 CORS 错误,查后端是否允许了前端域名。CORS 是 Delphi 后端最容易漏配的一环,需要在响应头里加Access-Control-Allow-Origin。

4.3 后端 Delphi 侧返回 JSON 的注意事项

后端如果用TWebModule或TIdHTTPServer手写 REST,返回 JSON 时要注意字符编码。Delphi 字符串默认是 UTF-16,直接写进响应流可能变成乱码。常见做法是用TJSONObject构造后ToJSON再按 UTF-8 编码写出。另外,日期字段建议统一用 ISO 8601 字符串,不要用 Delphi 的TDateTime浮点数直接序列化,前端解析会疯掉。

5. 避坑与常见问题:那些让我加班到凌晨的细节

5.1 编译通过但页面白屏

现象:IDE 里编译零错误,浏览器打开一片空白,console 里可能只有一行Uncaught SyntaxError。原因通常是某个单元的initialization段里访问了浏览器不存在的对象,或者 HTML 宿主文件里 JS 引用路径写错。解决:先看 console 第一条错误,定位到具体 JS 文件;再检查Project1.html里<script>标签的 src 是否指向了正确的输出目录。我一般会在 HTML 里加一个window.onerror钩子,把错误直接显示在页面上,省得每次开 console。

5.2 中文乱码

现象:页面上的中文变成问号或方块。原因:HTML 文件缺少<meta charset="utf-8">,或者后端返回的 Content-Type 没带 charset。解决:在 HTML 的 head 里显式声明 UTF-8,后端响应头加Content-Type: application/json; charset=utf-8。Delphi 侧写字符串时确认用的是 UTF-8 编码流。

5.3 事件绑定失效

现象:按钮点了没反应,但代码逻辑没问题。原因:组件在FormCreate之前就被创建,或者动态创建的组件没有正确设置Parent。解决:动态组件一定要先Create,再设Parent,最后挂事件;顺序错了,DOM 节点还没插入文档,事件监听就绑不上。

5.4 内存泄漏与组件销毁

现象:页面长时间运行后越来越卡。原因:动态创建的TWebPanel、TWebHttpRequest没有在不用时Free。浏览器端虽然没有传统内存泄漏那么严重,但 DOM 节点和 JS 对象堆积一样会拖慢页面。解决:对一次性使用的请求对象,在OnResponse末尾Free;对动态 UI,切换视图时显式清理旧节点。

5.5 部署后接口 404

现象:本地开发一切正常,部署到服务器后所有 API 请求 404。原因:开发时用的是 IDE 内置服务器加相对路径,部署后前端静态资源和后端 API 不在同一域名或路径下。解决:把 API 基地址抽成配置项,部署时按环境注入;后端确认路由前缀和前端请求路径一致。这个问题没有玄学,就是路径对不上,但每次换环境都要重新确认一遍。

6. 进阶:把调试和构建流程固化下来

走到这里,基本链路已经通了。但要让这个方案在团队里真正跑起来,还得解决两件事:调试效率和构建一致性。

调试方面,TMS WEB Core 支持生成 source map,浏览器开发者工具里能直接看到 Pascal 源码行号。开启方式是在工程选项里勾选生成 map 文件,部署时把.js.map一起放上去(生产环境可以关掉)。我习惯在开发阶段始终开着 source map,断点直接打在 Pascal 代码上,比看编译后的 JS 舒服太多。如果遇到 source map 不生效,先确认浏览器是否启用了「Enable JavaScript source maps」,再看 map 文件里的 sources 路径是否和实际文件结构匹配。

构建一致性方面,pas2js 编译器版本、TMS WEB Core 组件版本、Delphi IDE 版本三者要锁死。我吃过一次亏:本地用新版本编译器构建通过,CI 机器上还是旧版本,产出的 JS 在某个浏览器上行为不一致,排查了一整天才定位到编译器差异。现在的做法是在项目根目录放一个build-env.md,写清楚三个版本号,CI 脚本里加一步版本校验,不匹配直接失败。

还有一个值得固化的习惯:把常用的页面交互封装成可复用的TWebForm基类或辅助单元。比如统一的错误提示、统一的 loading 遮罩、统一的 token 刷新逻辑。这些在 VCL 时代就是基类干的事,搬到 Web 端一样适用。不要每个页面都重写一遍TWebHttpRequest的创建和销毁,那是给自己挖坑。

最后说一个验证方法:每次改完前端逻辑,不要只在 IDE 内置浏览器里测,至少再用一个真实浏览器(Chrome 或 Edge)的隐私模式跑一遍。内置浏览器和真实浏览器在缓存、CORS、安全策略上都有差异,很多问题只有真实环境才暴露。这个习惯帮我省下了至少三次「上线后才发现」的尴尬。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进