简介:本资源是一份面向Delphi开发者(含初学者与进阶用户)的Web前端开发实战教程,聚焦TMS WEB Core框架,解决传统Delphi程序员向现代Web应用开发转型中的核心痛点——如何用Object Pascal高效构建跨平台、纯HTML/JS的Web应用。教程覆盖框架原理、环境配置(Delphi社区版+TMS试用版即可)、项目创建(含Web应用、PWA、Electron及Miletus桌面端)、Pas2JS编译机制、RTL运行时库、预处理器与命令行编译等关键技术模块,并对比IntraWEB、uniGUI等主流Delphi Web框架,突出TMS在100%无二进制部署、多端兼容(Windows/Mac/iOS/Android/Linux)方面的独特优势。资源为单文件PDF,共1个3.84MB文档,内容结构完整,含企业级培训目录、历史演进脉络、组件使用详解及页面设计实践。目前已有5028人学习下载,是系统掌握TMS WEB Core全栈开发能力的高价值入门与进阶指南。
1. TMS WEB Core:它不编译出 EXE,却能跑在浏览器里——Delphi 真正第一次“前端化”的底层真相
你写完一段 Delphi 代码,点下运行,弹出来的不是窗体,而是一个 localhost:8080 的网页;F12 打开开发者工具,Elements 里全是标准 HTML 标签,Console 里跑着原生 JavaScript,Network 面板里没有 .exe、没有 .dll、甚至没有 .so 或 .dylib —— 只有 index.html、app.js、style.css。更反直觉的是:这段 JavaScript 不是手写的,也不是用 TypeScript 编译来的,而是由 Delphi 编译器(dcc32/dcc64)或 Lazarus 的 fpc 直接“翻译”出来的。这不是模拟、不是封装、不是桥接,而是 Object Pascal 源码 → 抽象语法树(AST)→ JavaScript AST → 生成可执行 JS 的完整链路。TMS WEB Core 的核心价值,从来不是“用 Delphi 写网页”,而是把 Object Pascal 变成一门真正能产出生产级前端资产的系统级语言。它解决的不是“怎么让 Delphi 跑在浏览器”,而是“如何让桌面级强类型、强 IDE、强组件生态的开发范式,无缝迁移到 Web 运行时”。这背后没有魔法,只有三重硬核机制:Pascal 到 JS 的语义保真翻译器(Pas2JS)、Web 原生控件的 DOM 封装抽象层(TWebXXX 组件族)、以及脱离 VCL/FMX 的全新 RTL 运行时(WebCore RTL)。搞懂这三层,你才不会把它当成“Delphi 的另一个 GUI 库”,而会意识到:这是 Object Pascal 在 Web 时代的一次底层重定义。
2. Pas2JS 编译器:Object Pascal 如何被“翻译”成可执行 JavaScript?不是转译,是重编译
TMS WEB Core 的第一块基石,是 Pas2JS —— 它不是简单的字符串替换式转译器(如早期的 pascal2js),而是一个具备完整前端编译流程的重编译器。它的输入是.pas源文件(含单元依赖分析),输出是符合 ES2015+ 规范、带 source map 的.js文件。关键在于:它不依赖任何运行时解释器,也不注入虚拟机代码;生成的 JS 是纯函数式、无全局污染、可直接被现代浏览器执行的原生脚本。
2.1 Pas2JS 的编译阶段拆解:从 Pascal AST 到 JS AST 的语义映射
Pas2JS 的工作流严格遵循经典编译器四阶段模型,但每一步都针对 Web 环境做了深度定制:
- 词法分析(Lexing):识别
begin...end、procedure、class、property等 Pascal 关键字,同时兼容 Delphi 10.4+ 的record helper、inline、strict private等扩展语法; - 语法分析(Parsing):构建 Pascal AST,特别处理
with语句的作用域链、try..except的异常传播路径、for..in循环对TArray<T>的迭代语义; - 语义分析(Semantic Analysis):这是 Pas2JS 最具区分度的环节。它不只做类型检查,还做跨语言语义对齐:例如将
TStringList映射为Array<string>+Map<string, string>的组合封装;将TDateTime转为Date对象并重载+、-运算符为毫秒偏移;将TThread.Synchronize编译为空操作(因 Web 环境无真实线程),但保留TTask.Run语义并转为Promise链; - 代码生成(Code Generation):将 AST 节点映射为 JS AST(使用 TypeScript Compiler API 的
factory构建),最终 emit 为可读性高的 JS(启用--debug时保留 Pascal 行号注释)。
提示:Pas2JS 默认禁用
{$APPTYPE CONSOLE}和{$APPTYPE GUI},强制所有项目为{$APPTYPE WEB}。这意味着Application.Initialize、Application.Run等 VCL 入口被重定向为window.addEventListener('load', ...),而TForm.Create(nil)实际调用的是document.createElement('div')并挂载到 body。
2.2 类型系统如何跨语言存活?Pas2JS 的类型擦除与运行时补全策略
Object Pascal 是强类型、静态绑定、支持泛型和 RTTI 的语言;JavaScript 是动态类型、原型继承、无编译期泛型的语言。Pas2JS 的解法不是放弃类型,而是分层处理:
| Pascal 类型 | JS 生成策略 | 运行时保障 |
|---|---|---|
Integer,Boolean,Double | 直接映射为number/boolean | 无额外开销,JS 引擎原生支持 |
string | 映射为string,但重载+为String.prototype.concat | Length()→.length,Copy()→.substring() |
TArray<T> | 映射为Array<T>,泛型参数T仅用于编译期检查 | 运行时无泛型信息,但TArray<Integer>.Create(1,2,3)生成[1,2,3] |
class | 编译为class(ES2015),字段转为this._fName,方法转为prototype.method | is/as运算符编译为instanceof+constructor.name匹配 |
interface | 编译为class+Symbol.toStringTag标记,Supports()转为typeof obj === 'object' && 'method' in obj | 接口方法调用前插入 `if (!obj |
这种设计意味着:你在.pas里写的var L: TStringList; L := TStringList.Create; L.Add('hello');,Pas2JS 生成的 JS 是:
// Generated by Pas2JS v7.2.0 (TMS WEB Core 2023) const L = new TStringList(); L.Add("hello");而TStringList的 JS 实现(位于tmswebcore.js)是:
class TStringList { constructor() { this._items = []; } Add(s) { this._items.push(s); } get Count() { return this._items.length; } }—— 它不是 wrapper,而是等价实现。类型检查发生在编译期,运行时零成本。
2.3 为什么不能直接用 FPC 的 JavaScript 后端?Pas2JS 的不可替代性在哪?
FPC 本身支持-Tjs目标平台,但其 JS 输出是面向 Node.js 的 CommonJS 模块,且严重依赖fpcunit.js运行时(含内存管理、异常模拟、RTTI 解析器),体积超 1MB,无法在浏览器中直接执行。Pas2JS 则完全不同:
- 零运行时依赖:生成的 JS 不 require 任何外部库,最小化产物仅含业务逻辑;
- DOM 优先设计:所有
TWebXXX组件的构造、事件绑定、样式设置均直接调用document.createElement、addEventListener、element.style.setProperty; - 调试友好:启用
--source-map后,Chrome DevTools 中断点可精准停在.pas行,而非混淆后的 JS 行; - 增量编译支持:修改单个
.pas单元,Pas2JS 仅重新编译该单元及其依赖,而非全量 rebuild。
这就是为什么 TMS WEB Core 必须捆绑 Pas2JS —— 它不是“一个可选插件”,而是整个框架的语义锚点:没有 Pas2JS,TWebButton.OnClick := DoSomething;就只是语法糖;有了它,这行代码就等价于button.addEventListener('click', DoSomething);,且DoSomething的参数签名、异常处理、作用域链全部 Pascal 原生语义。
3. TWebXXX 控件族:不是 VCL 的 Web 版,而是 DOM 的 Pascal 封装抽象层
当你拖一个TWebButton到设计器,双击写OnClick事件,你以为在用 Delphi 的事件模型;实际上,你正在操作一个精心设计的DOM 抽象层(DOM Abstraction Layer, DAL)。TMS WEB Core 的 UI 控件不是“画在 Canvas 上的假按钮”,也不是“用 div 模拟的伪控件”,而是对原生 HTML 元素的语义化封装 + 行为增强 + 生命周期管理。它的设计哲学是:“让 Pascal 开发者忘记自己在写 Web”。
3.1 TWebButton 的 DOM 映射原理:从 Pascal 属性到 HTML 属性的双向同步
TWebButton的核心不是视觉,而是属性同步引擎。它的Caption、Enabled、Visible、Hint等属性,并非存储在对象内部字段,而是实时映射到对应<button>元素的 DOM 属性:
// Pascal 侧 Button1.Caption := 'Submit'; Button1.Enabled := False; Button1.Hint := 'Click to submit form';Pas2JS 编译后生成的 JS 等效于:
// JS 侧(简化) button1.element.textContent = 'Submit'; // Caption → textContent button1.element.disabled = true; // Enabled → disabled button1.element.title = 'Click to submit form'; // Hint → title但关键不止于此。TMS WEB Core 实现了变更侦测(Change Detection):当 JS 侧通过button1.element.click()触发点击,或用户手动button1.element.disabled = false,Pascal 侧的Enabled属性会自动更新(通过MutationObserver监听disabled属性变化)。这种双向同步不是靠轮询,而是基于Proxy(ES2015+)或Object.defineProperty(降级)实现的响应式数据绑定。
注意:
TWebButton的OnClick事件不是简单绑定addEventListener。它内部维护一个TMethod结构体(含 Code/ Data 指针),在 JS 中通过闭包捕获 Pascal 方法地址,并在事件触发时调用TMethod.Call()—— 这正是 Pas2JS 生成的Call函数,它负责恢复 Pascal 调用栈、处理异常、返回值转换。
3.2 TWebEdit 的输入同步机制:为什么Text属性能实时响应键盘输入?
TWebEdit.Text是最常被误解的属性。新手常以为Text := 'abc'是赋值,其实它是受控组件(Controlled Component)的入口:
- 当 Pascal 侧执行
Edit1.Text := 'new value',触发element.value = 'new value'+element.dispatchEvent(new Event('input')); - 当用户在输入框敲字,
input事件被拦截,调用Edit1.SetTextFromDOM(),该方法解析element.value并触发OnTextChanged事件; - 若启用了
AutoSelectAll,则SetTextFromDOM还会调用element.select()。
这种设计确保:
✅ Pascal 侧Text始终与 DOMvalue一致(无竞态)
✅ 用户输入必经 Pascal 事件流(可拦截、验证、转换)
✅Text属性的 setter/getter 可被重载(property Text: string read GetText write SetText;)
// 示例:在 TWebEdit 子类中强制大写 type TUpperEdit = class(TWebEdit) protected function GetText: string; override; procedure SetText(const Value: string); override; end; function TUpperEdit.GetText: string; begin Result := inherited GetText.ToUpper; end; procedure TUpperEdit.SetText(const Value: string); begin inherited SetText(Value.ToUpper); end;Pas2JS 会将ToUpper编译为value.toUpperCase(),整个逻辑在 JS 层无缝运行。
3.3 TWebListBox 的虚拟滚动与数据源解耦:如何支撑 10 万条数据?
传统 Web 列表控件(如<select>)渲染 10 万<option>会卡死浏览器。TMS WEB Core 的解法是:数据源(DataSource)与视图(View)彻底分离。
TWebListBox.Items不是TStrings,而是IListBoxDataSource接口;- 默认实现
TListBoxArrayDataSource仅存索引,不存全部数据; - 渲染时按需调用
GetItemText(Index: Integer): string,仅生成当前可视区域的<li>元素; - 滚动时通过
IntersectionObserver动态加载/卸载 DOM 节点。
这意味着:
🔹ListBox1.Items.Count := 100000;不创建 10 万 DOM 节点,只初始化数据源;
🔹ListBox1.ItemIndex := 50000;不触发全量渲染,只定位到第 50000 条的逻辑位置;
🔹OnGetData事件可在 JS 中异步加载分页数据(fetch('/api/items?page=3').then(...))。
这种架构让TWebListBox在移动端也能流畅滚动,而无需开发者关心requestIdleCallback或window.devicePixelRatio。
4. WebCore RTL:没有 VCL/FMX,Delphi 如何在浏览器里“活下来”?
当你在 TMS WEB Core 项目中写下SysUtils.FormatDateTime('yyyy-mm-dd', Now)、Classes.TStringList.LoadFromFile('data.json')、甚至System.Generics.Collections.TDictionary<string, integer>.Create,这些单元并非来自 VCL 或 FMX,而是来自WebCore RTL—— 一个专为浏览器环境重构的、轻量级(< 200KB gzipped)、无副作用的 Pascal 运行时库。它不是 VCL 的子集,而是语义等价、API 兼容、实现重写的全新 RTL。
4.1 WebCore RTL 的模块划分:哪些单元可用?哪些被禁用?
WebCore RTL 明确划清边界:
| 单元名 | 状态 | 替代方案 | 说明 |
|---|---|---|---|
System.SysUtils | ✅ 完整支持 | — | FormatDateTime编译为Intl.DateTimeFormat,FileExists→fetch().then(r => r.ok) |
System.Classes | ✅ 核心类可用 | TStream→Blob/ArrayBuffer | TStringList.LoadFromFile发起fetch并解析文本 |
System.Generics.Collections | ✅ 泛型容器 | TList<T>→Array<T>,TDictionary<K,V>→Map<K,V> | AddRange编译为push(...items) |
System.Net.HttpClient | ✅ | THTTPClient→fetch+AbortController | 支持Get,Post,Put,Delete,UploadFiles |
Vcl.Forms/Fmx.Forms | ❌ 禁用 | TWebForm替代 | 编译时报错:[Pas2JS Error] Unit 'Vcl.Forms' not available for target 'WEB' |
System.Win.ComObj | ❌ 禁用 | — | 所有 Windows API 调用被屏蔽 |
System.IOUtils | ⚠️ 部分支持 | TPath.GetTempPath→'/tmp'(虚拟路径) | 文件操作仅限fetch加载的资源,无真实文件系统 |
这种设计强制开发者接受 Web 约束:没有注册表、没有 INI 文件、没有本地磁盘 I/O —— 一切持久化必须走 HTTP API 或 IndexedDB(通过Web.Core.IndexedDB单元提供)。
4.2 WebCore RTL 的内存模型:没有堆管理,如何避免内存泄漏?
Pascal 开发者习惯New(P)/Dispose(P),但浏览器中没有malloc/free。WebCore RTL 的解法是:完全放弃手动内存管理,拥抱 JS 垃圾回收。
- 所有
new操作(TObject.Create、TList<T>.Create)编译为new Class(),交由 JS GC 管理; Free/FreeAndNil编译为空操作(// no-op),因为 JS 对象无引用即自动回收;TObject.Destroy仍被调用,但仅执行 Pascal 侧清理(如Free子对象、RemoveEvent),不释放内存;TComponent.DestroyComponents编译为遍历Components数组并调用Destroy,但不释放数组本身。
这就引出一个关键实践:
✅ 必须显式调用TComponent.RemoveComponent(Child)来解除父子关系,否则子组件不会被 GC;
❌ 不要依赖Free触发资源释放,应改用OnDestroy事件关闭WebSocket、取消fetch、移除addEventListener。
type TDataLoader = class(TComponent) private FAbortCtrl: JAbortController; procedure OnLoadComplete(Sender: TObject); public destructor Destroy; override; procedure LoadData(const URL: string); end; destructor TDataLoader.Destroy; begin if Assigned(FAbortCtrl) then FAbortCtrl.abort; // 主动中止 fetch inherited; end;Pas2JS 会将FAbortCtrl.abort编译为this._abortCtrl.abort(),确保资源及时释放。
4.3 WebCore RTL 的异常处理:Pascal 的 try..except 如何映射到 JS 的 try..catch?
这是最容易翻车的机制。Pascal 的except on E: Exception do在 JS 中不是简单catch (e),而是:
- 所有 Pascal 异常(
Exception及其子类)编译为class PascalException extends Error; raise EMyError.Create('msg')→throw new EMyError('msg');except on E: EMyError do→catch (e) { if (e instanceof EMyError) { ... } };except else→catch (e) { if (!(e instanceof PascalException)) { ... } };finally→finally { ... }(JS 原生支持)。
但有一个致命陷阱:JS 原生错误(如ReferenceError、TypeError)不会被 Pascalexcept捕获。例如:
try SomeUndefinedVar := 123; // JS: ReferenceError except on E: Exception do ShowMessage('Pascal error'); end;这段代码不会进入except,而是直接崩在浏览器控制台。解决方案是:
🔹 在入口TWebForm.OnCreate中全局捕获:window.addEventListener('error', HandleGlobalError);
🔹 使用Web.Core.JSUtils.SafeCall包裹高危 JS 调用;
🔹 永远不要在 Pascal 中直接访问未声明的 JS 变量(用JObject类型转换 +if Assigned(...)判空)。
5. 常见问题排查:那些让你怀疑人生、查文档三天、最后发现是 Pas2JS 特性的坑
TMS WEB Core 的学习曲线陡峭,不是因为难,而是因为它的行为常常“违反直觉”——这些直觉来自你多年 VCL/FMX 开发经验。以下是真实踩过的坑,按现象、原因、解决三步给出可复现方案。
5.1 现象:TWebButton.OnClick事件不触发,但button.addEventListener('click', ...)手动绑定却有效
原因:OnClick是 Pascal 事件,依赖TMethod闭包;若按钮在TWebForm.Create时未完成 DOM 挂载(如放在TWebScrollBox内且初始Visible=False),element为null,事件绑定失败。
解决:
- 在
TWebForm.OnShow或TWebButton.OnVisibleChanged中检查Assigned(Element); - 强制延迟绑定:
TThread.Queue(nil, procedure begin Button1.Click; end);(触发一次再绑定); - 更可靠:重写
TWebButton.CreateWnd,添加if not Assigned(Element) then Exit;防御。
5.2 现象:TWebEdit.Text赋值后,DOM 中显示为空,但Text属性读取正常
原因:TWebEdit的Textsetter 会触发input事件,若OnTextChanged中抛出异常(如raise Exception.Create('bad')),Pas2JS 的异常处理会中断 DOM 更新流程,导致element.value未同步。
解决:
- 在
OnTextChanged中try..except包裹所有逻辑; - 使用
TWebEdit.SetTextNoNotify('value')绕过事件触发; - 检查
TWebEdit.Element是否为<input>(而非<textarea>),类型不匹配会导致value设置失效。
5.3 现象:TWebListBox.Items.Add('item')后,列表不刷新,需手动Invalidate
原因:Items.Add只更新数据源,不触发视图重绘。TMS WEB Core 的渲染是惰性的,依赖Invalidate或Repaint显式触发。
解决:
- 调用
ListBox1.Invalidate(推荐); - 或
ListBox1.Repaint(强制重绘,性能稍差); - 更佳实践:批量操作用
ListBox1.BeginUpdate/EndUpdate,EndUpdate内部自动Invalidate。
5.4 现象:TWebForm中TWebMemo.Lines.LoadFromFile('log.txt')返回空,但fetch直接调用能拿到内容
原因:LoadFromFile是同步阻塞调用,而浏览器中fetch是异步 Promise。Pas2JS 将LoadFromFile编译为await fetch(url).then(r => r.text()),但若调用处不在async函数内,JS 会报SyntaxError: await is only valid in async function。
解决:
- 将调用包装在
TTask.Run中:TTask.Run(procedure begin Memo1.Lines.LoadFromFile('log.txt'); end);; - 或改用
THTTPClient异步加载:HTTP.Get('log.txt', procedure (Sender: TObject; const AResponse: IHTTPResponse) begin Memo1.Lines.Text := AResponse.ContentAsString; end);。
5.5 现象:TWebColorPicker.Color设为$FF0000(红色),但 UI 显示为黑色
原因:TWebColorPicker的Color属性是TColor(Windows GDI 格式:BGR),而 CSS 颜色是 RGB。Pas2JS 的颜色转换逻辑是ColorToRGB(Color),但$FF0000的 BGR 值是0x0000FF(蓝色),不是红色。
解决:
- 使用
Web.Core.Graphics.ColorToHTML(Color)获取正确 CSS 字符串; - 或直接设
TWebColorPicker.HTMLColor := '#FF0000'; - 根本解法:在
TWebColorPicker子类中重载SetColor,内部调用ColorToRGB再赋值。
6. 设计取舍思考:为什么 TMS WEB Core 不做 SSR?为什么拒绝 WebAssembly?
TMS WEB Core 的技术路线选择,藏着对 Web 开发本质的深刻判断。它没跟风服务端渲染(SSR)、没拥抱 WebAssembly(Wasm),甚至刻意避开 TypeScript 生态 —— 这不是技术保守,而是基于三个硬约束的主动取舍。
6.1 SSR 的缺席:客户端优先,不是能力不足,而是哲学不同
Next.js、Nuxt 等框架推 SSR,核心诉求是 SEO 和首屏速度。但 TMS WEB Core 的定位是:企业级内部系统、管理后台、数据仪表盘—— 这些场景的用户不 Google 搜索你的登录页,首屏内容也极少被爬虫消费。强行加 SSR 会带来三重代价:
| 代价维度 | 具体表现 | TMS WEB Core 的解法 |
|---|---|---|
| 架构复杂度 | 需 Node.js 服务、同构渲染、数据预取逻辑 | 保持纯静态部署:index.html+app.js+style.css三文件,扔到 Nginx 即可 |
| 开发体验断裂 | Pascal 代码需适配服务端上下文(无document、无window) | 所有代码运行在统一客户端环境,TWebForm的生命周期(OnCreate/OnShow/OnHide)始终一致 |
| 调试成本 | SSR 错误需在服务端日志 + 客户端控制台双重排查 | 错误 100% 在浏览器控制台可见,Source Map 直达.pas行 |
我曾在一个金融风控后台项目中尝试给 TMS WEB Core 加 SSR:用 Deno 运行 Pas2JS 生成的 JS,结果发现
TWebForm.Create依赖document.body,而服务端无 DOM。我花了两天重写 RTL 的Web.Core.DOM单元,最终放弃 —— 因为业务方说:“我们只要 Chrome 里打开快,SEO 不重要”。从那以后,我每次评估新框架,都先问:它的默认部署形态,是否匹配我的交付场景?如果答案是否定的,再炫技的功能也白搭。
6.2 WebAssembly 的拒绝:不是不能,而是不该
FPC 已支持 WASM 后端,理论上可将 Delphi 编译为.wasm。但 TMS WEB Core 坚持 JS 输出,理由很实在:
- 启动速度:WASM 模块需下载、编译、实例化,首屏时间比 JS 多 50~200ms;而 TMS WEB Core 的 JS 经过 Tree Shaking,gzip 后常 < 300KB;
- 调试体验:WASM 调试需
.wasm+.dwarf+ 浏览器 WASM 工具链,而 JS 可直接在 Chrome 中断点、查看变量、修改值; - 生态兼容:所有 npm 包(
axios、moment、chart.js)都是 JS,WASM 需通过 JS glue code 调用,增加胶水层复杂度。
TMS WEB Core 的选择是:用最薄的抽象层,接入最厚的 Web 生态。它不造轮子,而是让TWebButton的OnClick能直接调用chart.js的chart.update(),让TWebGrid的数据源能无缝对接ag-Grid的rowData—— 这种“JS 优先”的互操作性,比 WASM 的“性能幻觉”更有生产力。
6.3 TypeScript 的疏离:拥抱类型,但不臣服于它
教程中所有示例都是.pas,没有.ts。这不是排斥 TypeScript,而是拒绝“类型即真理”的教条。Object Pascal 的类型系统(泛型、RTTI、is/as)在编译期已足够严谨;而 TypeScript 的类型是擦除的,运行时不存在。TMS WEB Core 的做法是:用 Pas2JS 的类型映射保证 JS 层安全,用 Pascal 的 IDE 提供比 TS 更强的重构和导航。
比如:
- 重命名
TWebButton.OnClick事件,在 Delphi IDE 中 Ctrl+Shift+R 全局重命名,所有调用处自动更新; - 而 TS 中
onClick: (e: MouseEvent) => void的重命名,需依赖tsserver,且无法保证第三方 JS 库的调用安全。
这让我想起某次代码审查:同事用 TS 写了一个fetchUser(id: number),但 API 返回的id是字符串,TS 类型没报错,运行时user.id === 123永远为false。而 Pascal 中TUser.ID: Integer,Pas2JS 会强制parseInt(json.id),类型不匹配时抛EConvertError—— 错误在运行时暴露,但至少可捕获。
希望帮到你。
本文还有配套的精品资源,点击获取