简介:本资源是一份面向Delphi开发者与RAD Studio进阶用户的中文技术指南,聚焦Delphi 12新增的HTMX与WebStencils网络开发能力,解决传统Web交互开发中JavaScript冗余、前后端耦合度高、原型搭建效率低等痛点,特别适用于快速构建动态单页应用或服务端渲染型Web界面。资源为单文件PDF文档(919KB),内容系统覆盖HTMX核心机制——包括hx-get/hx-post属性用法、hx-swap内容交换策略、目标元素定位、GET/POST/PUT/DELETE请求控制,以及WebStencils在RAD Studio中的拖放式页面组装实践;同时结合WebBroker架构解析,强化服务端集成逻辑。目录结构清晰,含前言、HTMX原理对比、核心概念详解(共10+子节)、WebBroker组件体系与应用创建流程等模块,理论与实操并重。目前已有479人学习下载,是掌握Delphi 12现代化Web开发范式的权威入门与参考材料。
1. Delphi 12 的 HTMX + WebStencils 组合:不是加个 JS 库,而是重定义 RAD Studio 的 Web 开发边界
你有没有试过在 RAD Studio 里拖一个 TWebBrowser,再塞进一堆 jQuery AJAX 和 Handlebars 模板,最后发现调试时控制台报错像开盲盒、部署时静态资源路径总对不上、导师问“这个交互逻辑在哪写的”你得翻三份文件——HTML、JS、Delphi 后端?这不是开发,是拼图。Delphi 12 把 HTMX 和 WebStencils 做成原生级支持,根本就不是让你“用上一个新前端库”,而是把整个 Web 开发流程拉回 Delphi 最擅长的范式:声明式、组件化、服务端主导、所见即所得。它让hx-get="/api/tasks"这种属性直接写在.dfm可视化设计器生成的 HTML 片段里,让@if Task.Completed这种模板语法和TDataSetProvider的字段名自动对齐,让“改个按钮行为”不再需要切到 VS Code 写 JS,而是在 Object Inspector 里点几下属性、双击事件框写几行 Pascal 就完事。这套方案专治三类人:课设要两周交完整 Web 管理后台的学生、毕设需展示“前后端一体可控”的答辩者、以及某高校实验室里被要求“不许用 Vue/React,但又要响应式交互”的嵌入式 Web 系统开发者。它不卷框架生态,只卷你对 Delphi 数据流和 HTML 生命周期的理解深度。
2. 总体架构设计:三层结构与四个核心模块的职责划分
Delphi 12 中 HTMX + WebStencils 的落地不是简单叠加,而是一套分层明确、职责内聚的架构。它天然适配 RAD Studio 的 WebBroker 模型,但又通过 WebStencils 引擎重构了传统 WebBroker 的响应生成方式。整套系统可划分为客户端声明层(HTMX)→ 服务端模板编译层(WebStencils Engine)→ 数据服务层(WebBroker + DataSet)→ 基础设施层(HTTP Server + Session)四个模块。其中,HTMX 负责将用户交互意图(点击、输入、轮询)翻译为标准化 HTTP 请求;WebStencils Engine 不是通用模板引擎,而是专为 Delphi 数据对象优化的编译器,它把@query、@if、@ForEach等指令编译为 Pascal 字节码,在服务端执行时直接绑定TClientDataSet或TDataSetProvider的当前记录;WebBroker 不再返回纯 HTML 字符串,而是返回由 WebStencils 编译后的、带上下文数据的 HTML 片段;基础设施层则复用 Delphi 原生的TIdHTTPServer或 IIS 集成,Session 管理走TWebSessionManager内存实例,避免外部依赖。这种设计让“页面刷新”变成局部 DOM 替换,“表单提交”变成hx-post触发TWebModule中的OnAction事件,所有逻辑都在.pas文件里可断点、可单步、可单元测试——这才是 Delphi 工程师该有的 Web 开发体验。
2.1 HTMX 在 Delphi 中的声明式集成:从 hx-* 属性到事件映射
HTMX 的能力全部通过 HTML 属性暴露,但在 Delphi 12 的 RAD Studio 环境中,这些属性不是手写字符串,而是通过TWebStencilsComponent的Attributes集合或TWebStencilsTemplate的RenderOptions动态注入。例如,一个待办事项列表项的删除按钮,传统做法是写<button onclick="deleteTask(123)">,而 HTMX 方式是:
<button hx-delete="/api/tasks/123" hx-confirm="确定删除此任务?" hx-target="#task-list" hx-swap="outerHTML"> 删除 </button>关键在于,/api/tasks/123这个 URL 并非硬编码。在 Delphi 端,你通过TWebStencilsTemplate的AddVar('TaskID', CurrentTask.FieldByName('ID').AsString)注入变量,再在模板中写hx-delete="/api/tasks/@TaskID",WebStencils 引擎会在渲染时自动替换。更进一步,hx-target指定的#task-list是一个<div id="task-list">容器,它的内容由另一个 WebStencils 模板task_list.stencil渲染,该模板内部使用@ForEach Tasks do遍历Tasks: TClientDataSet数据集。这意味着:HTMX 的声明式交互,其背后是 Delphi 的数据集对象和模板变量的强绑定。你不需要在 JS 里解析 JSON,也不需要手动document.getElementById,所有目标元素 ID 都在.stencil文件里明确定义,且与TWebModule中的Content属性值严格对应。
2.2 WebStencils 模板引擎:不是 Mustache,是 Pascal 编译器
WebStencils 的语法看似类似 Mustache,但实现机制完全不同。它不是运行时文本替换,而是编译期生成 Pascal 函数。当你创建一个layout.stencil文件,内容为:
<!DOCTYPE html> <html> <head> <title>@Page.Title</title> @RenderTitle </head> <body> @RenderBody </body> </html>WebStencils 编译器会生成类似这样的 Pascal 代码(简化示意):
function RenderLayout(const APage: TWebPage; const AContext: TWebStencilsContext): string; begin Result := '<!DOCTYPE html><html><head><title>' + APage.Title + '</title>' + RenderTitle(AContext) + '</head><body>' + RenderBody(AContext) + '</body></html>'; end;而RenderTitle和RenderBody又分别指向其他.stencil文件编译出的函数。这种编译模型带来三个硬性优势:第一,类型安全——APage.Title是string类型,IDE 能实时提示、编译时报错;第二,性能可控——无正则匹配、无字符串拼接,纯函数调用;第三,调试友好——你在RenderLayout函数里打个断点,就能看到APage.Title的实际值、AContext的完整数据树。这也是为什么 WebStencils 能无缝集成@LoginRequired这类安全指令:它本质是编译时插入if not IsUserLoggedIn then Exit('');这样的 Pascal 判断,而不是运行时解析字符串。
2.3 WebBroker 模块改造:从 OnAction 到 OnHtmxAction 的事件升级
传统 WebBroker 的TWebModule处理请求靠OnAction事件,它接收TWebRequest和TWebResponse,你需要手动解析Request.QueryFields、拼接 HTML 字符串。HTMX + WebStencils 模式下,我们引入OnHtmxAction事件(由TWebStencilsModule提供),它接收的是强类型的THtmxRequest对象:
procedure TMyWebModule.WebModuleHtmxAction(Sender: TObject; const HtmxRequest: THtmxRequest; Response: TWebResponse); var TaskDS: TClientDataSet; begin // HtmxRequest 自动解析 hx-trigger、hx-vals、hx-target 等头部 if HtmxRequest.Trigger = 'click' then begin if HtmxRequest.Path = '/api/tasks' then begin // POST 创建任务 TaskDS := CreateTaskFromRequest(HtmxRequest); TaskDS.Post; // 渲染新增的任务项 HTML 片段 Response.Content := WebStencilsEngine.Render('task_item.stencil', ['Task', TaskDS.GetCurrentRecord]); Exit; end; end; // 默认返回完整页面(首次加载) Response.Content := WebStencilsEngine.Render('layout.stencil', ['Page', TWebPage.Create('首页')]); end;注意HtmxRequest.Path和HtmxRequest.Trigger是预解析字段,WebStencilsEngine.Render的第二个参数是键值对数组,['Task', ...]表示将Task变量绑定到数据集当前记录。这种写法把“请求路由 → 数据处理 → 模板渲染”三步压缩在一个事件里,且每一步都可单步调试。你再也不用在Response.Content := '<html>...' + IntToStr(TaskID) + '...</html>'里数引号和加号了。
2.4 安全机制落地:CSRF、XSS、会话校验的 Pascal 实现
HTMX 的hx-post请求默认不带 CSRF Token,这是最大安全隐患。WebStencils 的解决方案是:在模板编译阶段注入 Token,而非运行时拼接。你在layout.stencil顶部写:
@CSRFToken <form hx-post="/api/tasks" hx-target="#task-list"> <input type="hidden" name="csrf_token" value="@CSRF.Token"> <!-- 其他字段 --> </form>@CSRFToken是一个编译指令,它会生成 Pascal 代码调用TWebSessionManager.GetCSRFToken(SessionID),并将结果注入@CSRF.Token。同理,XSS 防护不是靠hx-swap="innerHTML"的粗暴替换,而是 WebStencils 引擎默认对所有@xxx变量输出做 HTML 实体转义。若需原始 HTML(如富文本编辑器内容),必须显式使用@raw xxx指令,这在编译期就会触发警告:“@raw可能导致 XSS,请确认xxx已经过HTMLEncode处理”。会话管理则完全走TWebSessionManager,它基于内存哈希表实现,每个 Session ID 对应一个TWebSession对象,其中UserRoles、LastAccessTime、CSRFToken全部是 Pascal 属性,可在OnHtmxAction中直接访问HtmxRequest.Session.UserRoles.Contains('admin')。这种安全不是配置开关,而是语言级约束。
3. 核心模块实现:从待办事项 Demo 到可复用组件库的演进路径
一个合格的 Delphi Web 项目不能止步于“Hello World”,必须证明它能支撑真实业务复杂度。我们以“待办事项应用”为蓝本,展示如何从零开始构建可复用、可维护、可扩展的模块体系。整个过程不是堆功能,而是按“数据建模 → 模板分层 → 交互闭环 → 组件抽象”四步推进。关键不在于写了多少行代码,而在于每一层是否清晰隔离、是否支持独立测试、是否能在新项目中一键复用。比如,task_item.stencil模板最终会被抽离为WebStencils.Components.TaskItem单元,其Render函数接受ATask: TTaskRecord参数,返回 HTML 字符串——这已经是一个标准 Delphi 组件,可以发布到 GetIt,也可以被其他团队直接引用。
3.1 数据建模与 WebBroker 数据服务层搭建
Delphi Web 应用的数据源头必须是强类型的。我们不推荐直接操作TWebRequest.QueryFields,而是定义TTaskRecord记录类型,并用TClientDataSet封装:
type TTaskRecord = record ID: Integer; Title: string; Completed: Boolean; Category: string; CreatedAt: TDateTime; end; TTaskService = class private FDataSet: TClientDataSet; public constructor Create; destructor Destroy; override; function GetAllTasks: TClientDataSet; function GetTasksByCategory(const ACategory: string): TClientDataSet; procedure CreateTask(const ATitle, ACategory: string); procedure ToggleTask(const AID: Integer); procedure DeleteTask(const AID: Integer); end;TTaskService的所有方法都返回TClientDataSet,这是 WebStencils 模板的原生数据源。GetAllTasks方法内部调用FDataSet.Open,CreateTask则调用FDataSet.Append并Post。重点在于:所有数据库操作都封装在服务类中,WebModule 只负责调用服务、传递数据给模板,绝不出现 SQL 字符串或TADOQuery.ExecSQL。这样做的好处是,当某天需要把 SQLite 换成 PostgreSQL,你只需重写TTaskService的构造函数,注入新的TDataSetProvider,而TWebModule和所有.stencil文件完全不用改。我们在TMyWebModule的OnCreate事件中初始化服务:FTaskService := TTaskService.Create;,并在OnHtmxAction中调用FTaskService.GetAllTasks。
3.2 模板分层策略:Layout、Page、Partial 的三级复用体系
WebStencils 的模板不是扁平文件,而是有明确层级关系的组件树。我们建立三级结构:
- Layout 模板(如
main_layout.stencil):定义全局 HTML 结构、CSS/JS 引入、导航栏、页脚。它包含@RenderBody占位符。 - Page 模板(如
task_list_page.stencil):定义具体页面逻辑,如“待办列表页”。它通过@LayoutPage 'main_layout.stencil'指定父 Layout,并在@RenderBody区域内写页面专属内容,如<h1>我的任务</h1>和@RenderPartial 'task_list_partial.stencil'。 - Partial 模板(如
task_list_partial.stencil):定义可复用 UI 片段,如任务列表容器。它不指定 Layout,只包含<div id="task-list">@ForEach Tasks do @RenderPartial 'task_item_partial.stencil'</div>。
这种分层让修改变得极其精准。例如,要给所有页面加一个深色模式切换按钮,你只需在main_layout.stencil的<head>里加一行<link rel="stylesheet" href="@If DarkMode then '/css/dark.css' else '/css/light.css'>,并确保@If的DarkMode变量由TWebPage对象提供。而task_list_partial.stencil里@ForEach Tasks do ...的循环逻辑,与task_item_partial.stencil的单个任务渲染完全解耦。每个 Partial 都是一个独立单元,可以在不同 Page 中多次引用,甚至可以传参:@RenderPartial 'task_item_partial.stencil' ['Task', CurrentTask, 'ShowEditButton', True]。
3.3 HTMX 交互闭环:从 hx-get 到 hx-swap 的完整链路实操
HTMX 的核心价值在于“声明即实现”,但必须确保每个环节都闭环。以“点击分类标签筛选任务”为例,完整链路如下:
- 前端声明:在
task_list_partial.stencil中,分类链接写为<a hx-get="/api/tasks?category=@Category.Name" hx-target="#task-list" hx-swap="innerHTML">; - 路由匹配:
TMyWebModule.OnHtmxAction中,HtmxRequest.Path = '/api/tasks'且HtmxRequest.QueryFields.Values['category']可取到值; - 数据获取:调用
FTaskService.GetTasksByCategory(HtmxRequest.QueryFields.Values['category']); - 模板渲染:
WebStencilsEngine.Render('task_list_partial.stencil', ['Tasks', ResultDataSet]); - DOM 更新:HTMX 接收响应 HTML,找到
#task-list元素,用innerHTML方式替换其内容。
关键细节在于hx-swap="innerHTML"的选择。innerHTML是最常用,但若要保留原有元素的 CSS 过渡效果,应改用hx-swap="outerHTML",此时响应 HTML 必须包含完整的<div id="task-list">...</div>标签。若要添加新任务到列表末尾,则用hx-swap="beforeend",响应 HTML 只需<div class="task-item">...</div>。这些 swap 策略不是魔法,而是 HTMX 对 DOM API 的封装,你必须在模板渲染时精确控制输出结构,否则 swap 会失败。我们通常在task_list_partial.stencil开头加注释<!-- hx-swap: innerHTML -->,作为开发约定。
3.4 可复用组件抽象:从 task_item_partial 到 WebStencils.Components.TaskItem 单元
当task_item_partial.stencil被多个页面复用后,它就该升级为正式组件。我们新建单元WebStencils.Components.TaskItem.pas:
unit WebStencils.Components.TaskItem; interface uses System.SysUtils, System.Classes, WebStencils.Engine, Data.DB; type TTaskItemRenderer = class public class function Render(const ATask: TTaskRecord; const AShowEditButton: Boolean = False): string; static; end; implementation uses WebStencils.Engine; { TTaskItemRenderer } class function TTaskItemRenderer.Render(const ATask: TTaskRecord; const AShowEditButton: Boolean): string; begin // 直接调用 WebStencilsEngine.Render,传入预编译的 stencil 文件 Result := WebStencilsEngine.Render('components/task_item.stencil', ['Task', ATask, 'ShowEditButton', AShowEditButton]); end; end.对应的components/task_item.stencil文件内容精简为纯 UI:
<div class="task-item" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />