☰
TMS WEB Core 实战:Delphi Web 前端开发与部署
2026/11/3 11:39:06 网站建设 项目流程

简介:《Delphi Web前端开发教程——基于TMS WEB Core框架》是一份面向Delphi/Lazarus开发者及Web前端入门者的PDF教程,适合希望用Object Pascal快速构建网站、Web应用以及Windows、macOS、iOS、Android、Linux跨平台App的初中高级读者。内容从Web开发基础与框架选型讲起,比较IntraWEB、uniGUI与TMS WEB Core的特点,再进入TMS WEB Core环境配置、项目设置、自动化版本管理及Lazarus安装,并覆盖普通Web应用、渐进式Web应用、Electron与Miletus桌面程序的创建流程。教程还涉及Pas2JS编译、RTL运行时库、预处理器、命令行编译、页面设计与绝对定位等实践主题,可帮助读者理解纯HTML/JS输出、部署分发和调试排错思路。资源包共1个PDF文件,约3.84MB,目录完整、章节清晰,已有5029人学习。

1. Delphi 写前端这件事,TMS WEB Core 到底卡在哪条缝里

给一个做 ERP 的团队看代码时,我印象最深的一幕是:后端和桌面端全是 Object Pascal,唯独 Web 端另起一套 Node 工具链,两拨人两套心智模型,改一个字段要同步两次。这类团队最常问的就是能不能用 Delphi 直接把 Web 前端也写了。这份《Delphi Web 前端开发教程——基于 TMS WEB Core 框架》回答的正是这个问题,它把 TMS WEB Core 从环境配置、页面设计、组件使用一直到数据库接入的整条链路串了一遍,是一份完整的企业内训材料。它适合已经会 Object Pascal、想把手伸进浏览器的开发者,也适合刚接触 Pascal 但不想被 JS 工具链劝退的新手;只要免费社区版加组件试用版就能跑通全流程。

2. 环境与项目骨架:从装组件到第一个可调试页面

很多人对 Delphi 做 Web 的第一反应是"服务端渲染那一套",所以第一次看到 TMS WEB Core 编译出来的东西会愣一下:没有 exe,没有 dll,一堆 .js 和 .html 直接丢到静态服务器就能跑。这个认知差如果不先解决,后面的环境配置会一直在错误的心智模型里打转。

2.1 编译链路先搞明白:Pas2JS 把 Pascal 送到哪儿去了

TMS WEB Core 的核心是 Pas2JS 这个 Pascal 到 JavaScript 的转译器。你在 IDE 里写的是标准的 Object Pascal 单元,编译时 Pas2JS 把它翻译成等价的 JS,再和一份 RTL 运行时库(同样被翻译成 JS)一起打包。浏览器拿到的最终产物是纯 HTML/JS/CSS,服务端不参与任何业务逻辑运算。

这条链路直接决定了三件事。第一,调试模型变了,你的断点最终落在浏览器里,IDE 里的调试是借助浏览器调试协议做的转发,链路中间多了一跳,配置不对就断不上。第二,所有 RTL 里能用的函数是"被重新实现过"的子集,StrToInt、Format、TStringList这些常用东西都在,但牵扯到 Windows API、注册表、文件系统绝对路径的调用一律没有,写代码时脑子里要有一根弦。第三,部署变成纯静态资源分发,这是它相对传统方案最大的优势,也是最容易被忽略的坑——很多人编译完只拷了 index.html,忘了同目录下的 js 分包。

常见做法是先在 IDE 里跑一次默认模板,打开浏览器开发者工具,切到 Network 面板,把加载的文件列表从头到尾看一遍,记住产物结构,后面部署时心里才有底。

2.2 装什么、配什么:最小可用环境清单

教程里给的门槛很低,一份免费社区版 IDE 加组件试用版就够。实际动手时,我一般按下面这个顺序走,每一步都有它的理由:

步骤做什么关键点
1装 Delphi 社区版(或 Lazarus)装完先确认能正常编译一个 VCL/FMX 空项目,排除 IDE 本身的问题
2装 TMS WEB Core 组件包装完在组件面板里能搜到TWeb前缀的控件才算成功
3检查库路径项目选项里的 Search Path 要包含组件的 Source 目录,否则WEBLib.*单元找不到
4新建一个 TMS WEB 应用选模板时不要一上来就挑 PWA 或 Electron,先用最普通的 Web 应用验证链路
5编译并本地预览IDE 会拉起一个本地调试服务器,端口可以在项目选项里改

第三步是最容易翻车的地方。组件装了但库路径没配全,报的错往往是"找不到单元 xxx",看起来像组件没装好,实际是路径问题。判断方法很简单:新建一个空项目,uses里手动加上那个报错的单元,如果编译过,说明是项目级路径问题而不是安装问题。

2.3 四种项目类型怎么选:别一上来就挑最花的

教程里给了四种工程模板,新手很容易被 Electron 和 Miletus 吸引,结果第一天就卡在打包上。选型其实看一个维度就够——你的产物最终要跑在什么壳里:

项目类型产物形态适合场景不建议的场景
Web 应用纯静态 HTML/JS内部管理后台、对外业务站点需要直接读写本地文件
PWA静态资源 + Service Worker需要离线缓存、可安装到桌面/手机团队不熟悉缓存更新策略
Electron 桌面应用静态资源 + 桌面壳需要托盘、多窗口、本地文件访问对安装包体积敏感
Miletus 桌面应用静态资源 + 轻量桌面壳想要桌面体验但不想背 Electron 的体积需要复杂原生能力

我的建议是:新项目一律从"Web 应用"起步,把业务逻辑和界面写完、跑通,再决定要不要套壳。因为套壳只是外面加一层容器,业务代码不用改,先套壳后开发等于给自己凭空加了一层调试障碍。

2.4 第一次编译:命令行跑通比 IDE 点按钮更有价值

IDE 里按 F9 能跑,但真到流水线上还是要走命令行。TMS WEB Core 的项目本质还是.dproj,用 IDE 自带的 msbuild 就能编:

# DELPHI_BIN 指向你机器上 IDE 的 bin 目录,不同版本路径不同,自己换 export DELPHI_BIN="/path/to/delphi/bin" # 编译 Release 配置,产物会落到项目选项里指定的输出目录 "$DELPHI_BIN/msbuild" DemoWeb.dproj \ -p:Config=Release \ -p:Platform=Win32 \ -t:Build \ -v:minimal # 编译完先看产物目录里有哪些文件,这一步别省 ls -la ./output/

这三个参数里,Config=Release决定了代码压缩和 source map 的开关,正式发布一定要用 Release;Platform对 TMS WEB Core 来说只是编译宿主平台,不影响产物,用 Win32 就行,别在这上面纠结;-v:minimal是我个人习惯,日志太吵的时候看错误反而慢。

编译完别急着部署,先在本地起个静态服务器验证一遍:

# 在产物目录里起一个最简静态服务,Python 自带就够 cd ./output python3 -m http.server 8080 # 浏览器打开 http://localhost:8080 看 Network 面板 # 重点关注:有没有 404 的 js 文件、控制台有没有红色报错

这一步的价值在于把"编译成功"和"能跑"分开验证。编译成功只说明转译没报错,能不能跑起来还取决于资源路径、模板文件、运行时初始化顺序,用静态服务器跑一遍能提前暴露其中大半问题。

3. 页面设计:绝对定位、HTML 模板与主题机制

TMS WEB Core 的界面层有两个来源,一个是 IDE 里拖控件摆位置,一个是外部的 HTML 模板。这两套东西可以混用,但混用的规则不搞清楚,改半天页面不动是很常见的事。

3.1 绝对定位和相对定位:两套布局模型的实际边界

绝对定位是 IDE 表单设计器的默认行为。你在设计器里把按钮放在 (120, 48),编译出来就是position:absolute; left:120px; top:48px,跟设计器里看到的完全一致。好处是所见即所得,移植老 Delphi 桌面窗体特别顺手;坏处是屏幕一缩小就露馅,手机上基本没法看。

相对定位则是把控件交给 CSS 流式布局,控件之间靠顺序和容器撑开。教程里对这两种模式的说明比较克制,我的实际经验是:

  • 内部管理后台、固定分辨率的工控界面,用绝对定位,开发效率最高,不用跟 CSS 较劲
  • 任何要上手机、要适配不同屏幕宽度的页面,一律用相对定位
  • 同一个页面里可以混,但要把混用的边界画在"容器"这一层——外层容器用相对定位撑结构,内层某个固定尺寸的小面板用绝对定位,这样不会互相干扰

判断自己有没有踩坑的方法:把浏览器窗口从最大拖到最窄,如果出现控件重叠、文字被裁、横向滚动条,那就是绝对定位用过头了。

3.2 HTML 模板:把设计稿接管过来

当项目里有专门的前端出设计稿时,纯靠 IDE 摆控件就不现实了。TMS WEB Core 支持引入外部 HTML 作为模板,控件通过ElementID属性挂到模板里对应的元素上。这套机制的关键理解点是:模板负责"长什么样",控件负责"干什么",两边通过 id 对齐。

// 假设模板里有一个 <input id="txtAmount"> 和一个 <span id="lblResult"> // 表单上放了 TWebEdit 和 TWebLabel,把它们的 ElementID 设成对应的 id procedure TForm1.WebFormCreate(Sender: TObject); begin WebEdit1.ElementID := 'txtAmount'; // 输入框接管模板里的 input WebLabel1.ElementID := 'lblResult'; // 标签接管模板里的 span // 挂载之后,控件的 Caption / Text 就会直接写到对应元素上 WebLabel1.Caption := '等待输入'; end; procedure TForm1.WebButton1Click(Sender: TObject); var Amount: Double; begin // 注意:Text 拿到的是字符串,模板里如果带了千分位逗号要先清洗 Amount := StrToFloatDef(StringReplace(WebEdit1.Text, ',', '', [rfReplaceAll]), 0); WebLabel1.Caption := FormatFloat('#,##0.00', Amount * 1.13); end;

这段代码里有两个容易被忽略的点。第一,ElementID的赋值时机必须在模板加载之后,放在WebFormCreate里是安全的,放在单元初始化段就会挂。第二,从模板输入框拿到的字符串不保证是"干净"的,模板里如果写了type="number"还好,写的是普通文本框时用户可能粘贴带逗号、带空格的数字,StrToFloatDef会直接返回默认值 0,界面上看起来就是"输了没反应"。这类问题排查起来很费时间,建议所有从界面读数字的地方统一包一层清洗函数。

模板还有一个"自动同步"的机制,教程里单独列了一节。它的作用是让 IDE 里的设计视图和外部 HTML 保持联动,改了模板文件设计视图跟着变。这个功能很方便,但也是后面避坑章节里要说的一个雷点。

3.3 主题、BiDiMode 与实时预览:三件小事但影响不小

主题机制解决的是"一套代码换皮"的问题。TMS WEB Core 的控件很多都暴露了主题相关的属性,改主题相当于换一组 CSS 类,不用动布局代码。实际项目里我一般把主题配置集中到一个设置单元,启动时统一应用,避免每个窗体各写一套。

BiDiMode 是双向文本模式,做多语言(尤其是从右往左书写的语种)时必须开。它的作用不只是把文字方向反过来,还会影响控件内部的对齐、滚动条位置、菜单位置。开启方式是在窗体级别设置,子控件默认继承。这里有个细节:如果某个控件是从 HTML 模板挂上来的,模板里写死的dir属性会覆盖窗体设置,需要在模板里也留出变量或者干脆不写。

实时预览是开发期最实用的功能之一,改完代码不用刷新浏览器就能看到效果。但它依赖 IDE 和浏览器之间的连接,网络环境复杂的时候容易断,断了之后表现是"改了没反应"。判断方法很简单:在浏览器里按 F5 手动刷新,如果手动刷新后效果正确,那就是预览连接断了而不是代码问题。

4. 组件与数据:控件家族怎么用、REST 怎么接

控件这一块教程列得很细,从TWebLabel、TWebButton一路到TWebSignIn,每个都标了属性、方法、事件。但列表式的东西光看记不住,真正要抓的是规律。

4.1 TWeb 控件家族的命名规律与通用属性

所有 Web 控件都是TWeb前缀,后面跟的是它在桌面端 VCL 里的老名字:TWebEdit对TEdit,TWebListBox对TListBox,TWebComboBox对TComboBox。这个命名规律意味着,你过去写 Delphi 桌面界面的经验有八成可以直接搬过来。

真正需要重新学的是那些"桌面端没有对应物"的控件,比如TWebBadge(徽章)、TWebPaintBox(绘图框)、TWebSignIn(登录框)。这几个是 Web 场景特有的,用之前先想清楚它是不是真的比手写 HTML 更省事。

通用的属性事件这一层,我踩过的坑集中在两个:

  • Caption和Text不是所有控件都有,TWebEdit用Text,TWebLabel用Caption,写反了编译能过但界面上什么都不显示
  • 事件里改控件属性,有的控件需要显式调用刷新才生效,尤其是被 HTML 模板接管的那些
procedure TForm1.LoadUserList; var i: Integer; begin // 批量填充列表:先关掉重绘,填完再打开,几十条以上时差别很明显 WebListBox1.Items.BeginUpdate; try WebListBox1.Items.Clear; for i := 0 to High(FUserNames) do WebListBox1.Items.Add(FUserNames[i]); finally WebListBox1.Items.EndUpdate; end; // 默认选中第一项,否则界面上看起来像"没数据" if WebListBox1.Items.Count > 0 then WebListBox1.ItemIndex := 0; end;

BeginUpdate/EndUpdate这一对在桌面端是老生常谈,在 Web 端更重要——每Add一次都会触发一次 DOM 操作,数据量稍微大一点页面就肉眼可见地卡。这种卡顿在开发机上往往不明显,到了低配设备上才暴露,属于典型的"上线才发现"的问题。

4.2 窗体托管:把大页面拆成能维护的块

单页应用写久了,一个窗体上堆几十个控件是常态,这时候就需要拆。TMS WEB Core 提供了两种拆法:一种是TWebFrame,把一组控件封成一个可复用的单元;另一种是把子窗体托管到某个容器控件里。

TWebFrame适合"这块 UI 会在多个页面重复出现"的场景,比如顶部导航、搜索栏、分页条。它的好处是封装彻底,改一处全局生效。缺点是首次加载时会多一份模板和脚本,页面碎片特别多的时候要权衡。

窗体托管适合"主页面 + 若干功能面板"的结构。主窗体上放一个容器,点菜单时动态创建子窗体塞进去:

procedure TFormMain.ShowPanel(const APanelClass: TWebFormClass); begin // 先清掉当前面板,注意 Free 而不是 Hide,否则内存会一路涨 if Assigned(FCurrentPanel) then begin FCurrentPanel.Free; FCurrentPanel := nil; end; FCurrentPanel := APanelClass.Create(Self); FCurrentPanel.Parent := WebScrollBox1; // 容器控件 FCurrentPanel.Align := alClient; FCurrentPanel.Show; end; // 调用侧 procedure TFormMain.mnuOrderClick(Sender: TObject); begin ShowPanel(TFormOrder); end;

Parent的赋值是关键,不设Parent窗体不会真正挂到 DOM 上,表现出来就是"创建了但看不见"。另一个要注意的是Align := alClient,这会让子窗体自动填满容器,配合TWebScrollBox就是标准的可滚动内容区。这套组合在管理后台里几乎处处用得上。

4.3 REST 接口接入:三种数据源怎么选

教程把数据方案分成三类:传统数据库走 REST API 封装、云数据库直接给 REST 端点、以及其它语言写的现成 API。对 Delphi 项目来说,这三条路的落地方式其实是一样的——客户端只认 HTTP + JSON,服务端是谁写的不重要。

组件面板里搜 HTTP 能找到请求组件,把它拖到窗体上,配 URL、方法、请求头就行:

procedure TFormOrder.LoadOrders; begin WebHttpRequest1.URL := 'https://your-api-host/api/orders?page=1&size=50'; WebHttpRequest1.Method := 'GET'; WebHttpRequest1.Headers.Clear; WebHttpRequest1.Headers.Add('Authorization: Bearer ' + FToken); WebHttpRequest1.Headers.Add('Accept: application/json'); // 异步发起,回调里处理结果,不要在这里等 WebHttpRequest1.Execute; end; procedure TFormOrder.WebHttpRequest1Response(Sender: TObject); var Data: TJSObject; begin if WebHttpRequest1.Status <> 200 then begin ShowMessage('接口返回 ' + IntToStr(WebHttpRequest1.Status)); Exit; end; // 响应体是字符串,转成 JS 对象再取值,比自己拼解析器稳 Data := TJSJSON.parse(WebHttpRequest1.ResponseText); // 注意:JS 对象的取值方式跟 Delphi 对象不一样,用 [] 而不是 . FTotal := StrToIntDef(VarToStr(Data['total']), 0); end;

这段代码里有几个必须记住的点。第一,请求是异步的,Execute之后代码继续往下走,实际响应在事件里到,任何依赖响应结果的操作都必须挪到事件里面。第二,跨域是绕不过去的问题,服务端必须配 CORS,客户端这边改什么都没用。第三,ResponseText是原始字符串,用TJSJSON.parse转成 JS 对象后,取字段用[],写.会编译过但运行时拿不到值,这个错误特别隐蔽,因为不报错只是返回空。

还有一个实践中的取舍:小项目直接用 HTTP 组件拼 URL 最省事,接口一多就会开始重复,这时候要么封一个统一的请求函数,要么上更完整的数据访问组件,别硬扛。

5. 避坑与排查:编译通过不等于能跑

下面这几条都是真跑起来之后撞的,按发生频率排。

5.1 白屏:编译成功,浏览器一片空白

现象:msbuild 输出 Build succeeded,产物目录里文件也都在,浏览器打开只有白底,控制台可能有一条 "Failed to load resource"。

原因:大多是资源路径问题。项目选项里输出目录设成了相对路径,而 IDE 预览服务器和静态服务器的根目录不一致,导致 js 文件按错误路径去找。

解决:打开浏览器 Network 面板看那条 404 的完整 URL,把它的路径和产物目录的实际结构对一遍,然后回到项目选项把输出目录改成绝对路径或统一的相对路径。改完记得清一次产物再编,缓存会骗人。

5.2 改了 HTML 模板但页面纹丝不动

现象:模板文件明明改完了,浏览器里还是老的样式和结构。

原因:两层,一层是浏览器的静态资源缓存,一层是"自动同步"机制在 IDE 缓存了旧的模板副本。

解决:先强制刷新(Ctrl+F5),不行就关掉浏览器缓存再开 DevTools。还不行就去产物目录里直接看模板文件有没有被拷过去,很多时候是源模板改了但产物没重新生成一次。以后遇到"改了没反应"一律按"缓存 → 产物 → 源码"的顺序查,别一上来就怀疑代码。

5.3 控件事件不触发,点按钮没反应

现象:按钮点下去毫无反应,控制台也没报错。

原因:控件用了ElementID挂到模板元素上,但模板里那个元素本身不是可点元素(比如是个div),或者被另一层元素盖在上面拦截了点击。

解决:在浏览器里用"检查元素",看点击区域最上层到底是什么标签。如果模板里给的是div,要么换成button,要么在样式里加pointer-events和cursor相关设置。另外确认ElementID拼写和大小写,HTML 里的 id 匹配是严格区分大小写的。

5.4 中文变问号或者方块

现象:界面上的中文显示成???或者乱码方块。

原因:Pascal 源文件保存编码不是 UTF-8,或者模板里少了<meta charset="utf-8">。

解决:IDE 里把文件另存为 UTF-8,同时检查模板头部。还有一处容易忽略:通过接口拿回来的中文如果是被服务端以别的编码序列化过的,客户端不做处理也会乱,这种情况下要在请求头里明确Accept-Charset。

5.5 断点打不上,调试全靠 ShowMessage

现象:IDE 里下了断点,跑到那儿直接过去,不停。

原因:Release 配置下 source map 没开,或者浏览器的调试协议连接断了。

解决:调试阶段一律切 Debug 配置,项目选项里把生成 source map 的开关打开,重新编译一次。如果还不行,先在浏览器 DevTools 里确认能不能看到 Pascal 源文件(Sources 面板里会有对应的映射),看不到就是映射没生成,不用在 IDE 那边反复试。

6. 部署验证与体积控制:产物到底该多大

产物能不能上生产,我一般不看编译日志,看三样东西:文件清单、总体积、首屏请求数。做法很土但很有效——把产物目录整个拖出来单独放一个地方,起静态服务,用浏览器的 Network 面板加"禁用缓存"跑一遍,把所有请求按大小排个序。

���张表是我在不同项目里反复验证过的判断标准:

指标关注点异常信号
入口 HTML 体积是否内联了过多东西超过 200KB 就要看一眼里面塞了什么
JS 分片数量是否被拆得过碎首屏加载 20 个以上小文件,HTTP/1.1 下会明显慢
单个 JS 体积是否有未压缩的大块单个超过 1MB 基本可以确认压缩没开
静态资源缓存头是否可控js 没有 hash 命名 + 长缓存,用户永远拿到旧版本

第四条是我踩过最疼的坑。早期部署时 js 文件名是固定的,浏览器缓存一命中,用户端就是新旧代码混跑,报的错千奇百怪,前端这边怎么查都查不出来。后来改成产物文件名带内容哈希,配合Cache-Control: max-age=31536000, immutable,index.html 本身设成不缓存,问题彻底消失。这套做法跟具体框架无关,纯静态产物的项目都适用。

部署本身没什么可说的,Nginx、Apache、对象存储的静态托管,随便挑一个,把产物目录整个扔进去就行。唯一要确认的是 MIME 类型,.js必须是application/javascript,配错的表现是页面白屏且控制台报 "Unexpected token <",因为浏览器把 js 当 HTML 解析了。

最后留一个自查脚本,打包前跑一遍,比人肉翻目录靠谱:

#!/usr/bin/env bash # 在产物目录里跑,快速体检 set -e echo "== 文件总数 ==" find . -type f | wc -l echo "== 总体积 ==" du -sh . echo "== 超过 500KB 的文件 ==" find . -type f -size +500k -exec ls -lh {} \; echo "== 检查入口页是否声明 UTF-8 ==" grep -i "charset" index.html || echo "警告:index.html 没找到 charset 声明" echo "== 检查是否有未带哈希的 js ==" find . -name "*.js" | grep -vE '\.[0-9a-f]{8}\.' || echo "提示:存在不带哈希的 js,注意缓存策略"

这个脚本干的事很有限,但它把"体积、编码、缓存"这三个最容易出事的点压成了一次执行。我现在每次出包前都会跑,跑完看一眼输出,心里有数了才往服务器上传。有次赶进度跳过这一步,结果一个 3MB 的未压缩包直接上了生产,用户反馈"打开要等十秒",回头查了半小时才发现是压缩开关在切配置时被关掉了。从那以后我每次出包都强制走一遍这套体检,再急也不省。希望帮到你。

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

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

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

立即咨询