Intraweb与EasyUI整合实战:Delphi后台快速开发指南
发布时间:2026/10/17 13:18:59 锦皓数字建站

简介围绕Intraweb与EasyUI协同开发整理的资料包面向使用Delphi语言构建Web应用的开发者旨在解决服务端业务逻辑与前端交互界面如何高效配合的难题。Intraweb的核心是组件模型允许程序员把桌面开发经验直接迁移到Web环境提供按钮、表格、表单等服务器控件EasyUI作为基于jQuery的UI库则带来数据网格、下拉树、日期选择器等现成组件。两者通过AJAX与JSON数据交换即可实现不刷新页面的排序、过滤和编辑操作从而打造响应流畅的现代化界面。压缩包大小为4.22MB内含EasyUI 1.4版API中文CHM帮助文档以及IW集成示例代码方便查询组件属性、事件和方法也能参考示例完成前后端对接配置。已有479人学习适合已经了解Delphi或IW基础、希望扩充前端技能并提升项目交互质量的开发人员。学习时既能借助帮助文档掌握数据网格、树形菜单等组件的用法又能从示例代码中理解IW中如何注册静态资源、绑定数据源并处理异步请求最终在熟悉的Delphi环境下构建出专业水准的Web应用。1. Intraweb与EasyUI为什么这套源码值得拆开看很多人以为用Intraweb做Web应用就得被它那套自带控件拖着走页面每次操作都要回发交互体验停留在老派时代。我第一次拆这个源码包之前也是这么想的直到看见他们用EasyUI把表格、弹窗、树全做了才发现这两套东西可以配合得非常顺。这个源码包做的事情很简单Intraweb在后端负责登录、Session管理和数据库访问EasyUI在前端负责数据展示和鼠标交互中间用JSON通信。适合手里有Delphi功底、又不愿意把整个前端重写成Vue或React的开发者。你不需要先学脚手架和打包工具打开工程就能看到一条从数据库到页面的完整数据链路。2. 项目结构与技术栈从IW组件到前端JS的打通方式2.1 源码包目录每个目录都在护航哪块功能先看目录。源码包最上层的src目录内容如下src/ ├─ AppResources/ │ ├─ easyui/ │ ├─ css/ │ └─ js/ ├─ Backend/ │ ├─ BackendServer.pas │ └─ JSONHelper.pas ├─ Forms/ │ ├─ MainForm.pas │ ├─ LoginForm.pas │ └─ GridDemoForm.pas ├─ www/ └─ ServerController.pasAppResources里放的是EasyUI压缩包和项目自用样式、脚本编译后同步到www目录供浏览器访问。Backend目录没有窗体文件只有两个单元BackendServer.pas负责承接前端发来的所有JSON请求JSONHelper.pas封装了数据集到JSON的转换。Forms目录下的三个IWForm是Intraweb的门面LoginForm处理登录MainForm作为主布局壳GridDemoForm是列表演示页。最后那个ServerController.pas是Intraweb应用的入口配置很多兼容性补丁都写在这个文件里。我在本地跑通这套源码后第一感觉是目录职责分得很清楚。如果按老派Intraweb习惯很可能会把生成JSON的代码也塞到某个IWForm的OnClick里那样页面一多就乱套。这个源码把“页面承载”和“数据输出”拆成两个层次后面想扩展一个接口无非在BackendServer.pas里加一个分支不碰IWForm。还有一点需要注意www目录下面并不直接放源码而是放构建时生成的静态资源副本。Intraweb独立运行模式会以当前工程目录作为应用根如果遗漏www前端访问JS就会404。源码包里有一个简单的同步脚本就是把AppResources下的东西复制到www再写一个版本号文件方便前端强制刷新缓存。2.2 技术选型为什么是Intraweb负责后端EasyUI负责前端EasyUI算是一个前端UI库和Vue、React那类完全前后端分离的思路不太一样。它的核心价值是“开箱即用”引入jQuery和easyui的CSS、JS之后写一个table标签就能出表格写一个div就能出树。对Delphi团队来说这套源码选它而不是Angular本质上是照顾后端开发者的前端投入门槛。Intraweb在其中的定位更像是一个应用服务器。它天然解决了登录、Session管理、并发用户控制这些自己用Indy写HTTP服务时最头疼的问题。源码没有把所有HTTP处理责任压给Intraweb而是用ServerController的请求分发机制让特定路径直接走独立处理器。这种混搭模式的好处是不需要让页面上堆满IW控件做数据绑定界面自由度完全由EasyUI控制。实际开发时如果要处理表格列很多、字段类型复杂的页面Intraweb自带控件需要一层层调整DataSource和RenderConfig效率不高。而EasyUI在表格列配置、弹出框、下拉选择器上只需要一个JS对象字面量。这也是源码包能说服我的原因对于后台录入和查询这类业务EasyUI比IW原生控件更贴近“工具型页面”的交互需求。2.3 数据交互JSON是连接两端的唯一协议EasyUI对服务端返回格式极其敏感。datagrid要的格式是{total:10,rows:[...]}combobox要的选项是[{value:1,text:启用}]。源码包的JSONHelper.pas就是围绕这个格式做的统一转换层。我摘一段JSONHelper.pas里的核心函数这段值得抄下来// JSONHelper.pas —— 将当前数据集转换为EasyUI datagrid需要的结构 function TJSONHelper.DataSetToGridJson(ADataSet: TDataSet; ATotal: Integer): string; var Arr: TJSONArray; Obj: TJSONObject; I: Integer; FieldList: TStringList; begin Arr : TJSONArray.Create; try FieldList : TStringList.Create; try ADataSet.GetFieldNames(FieldList); while not ADataSet.Eof do begin Obj : TJSONObject.Create; for I : 0 to FieldList.Count - 1 do Obj.AddPair(FieldList[I], ADataSet.FieldByName(FieldList[I]).AsString); Arr.AddElement(Obj); ADataSet.Next; end; finally FieldList.Free; end; finally Result : TJSONObject.Create .AddPair(total, TJSONNumber.Create(ATotal)) .AddPair(rows, Arr) .ToJSON; end; end;这个函数把任意TDataSet变成前端可直接消费的JSON。代码里有两个值得留意的参数ADataSet必须是已经打开、并且游标在首行的数据集ATotal不能直接用RecordCount因为分页状态下数据集往往只包含当前页要单独把总数传进来。还有一点是AsString会把数字也转成字符串如果要做数字排序最好在转换前判断字段是否为数值类型否则前端排序结果会是字典序。源码里留了注释说生产环境请用类型判断算是作者踩过痕迹。有了这个转换层后面接口部分的代码就特别薄。前端拿到JSON后直接用datagrid的columns里配置的field名对应具体列服务端并不关心前端怎么渲染。两端的约束只有一个字段名必须一致。这也是这套源码中最核心的约定。3. 核心模块拆解一个datagrid的数据从数据库到表格的完整链路3.1 服务端路由注册一个非IWForm的JSON接口在传统Intraweb里页面上的每个按钮点击都会触发一次Session Callback回调里可以拿到TIWRequest但那意味着前端必须严格遵循IW的控件生命周期。这套源码并没有这么做而是在ServerController.pas里手工注册了一个路由把api/grid这类路径交给独立处理器。看这段注册代码// ServerController.pas —— 把特定路径转到后端处理器 procedure TServerController.HandleRequest(ARequest: TIWRequest; AResponse: TIWResponse); begin if ARequest.PathInfo /api/grid then TBackendServer.HandleGridRequest(ARequest, AResponse) else if ARequest.PathInfo /api/menu then TBackendServer.HandleMenuRequest(ARequest, AResponse); end;实际工程里不会只挂一个api/grid还有api/save、api/delete、api/menu等。你可以把所有API都集中到一个入口也可以分开注册。我比较推荐一个入口的写法判断PathInfo走哪个处理器再判断QueryFields里的action参数区分增删改查这样前后端对接时只需要看一个路由文件。这里有一个稍微反直觉的地方代码里用的是ARequest.PathInfo但独立模式下Intraweb请求路径里可能会带/iw/这种前缀。如果PathInfo匹配不上前端加载列表时会返回HTML而不是JSON。源码包的解决方式是在ServerController启动时做一次路径规整把API前缀配置进应用例如把/api映射成/iw/api这样对接IIS或者独立模式都不会出错。3.2 前端初始化EasyUI datagrid与url绑定看过服务端再看前端怎么把表格拎起来。页面入口是GridDemoForm它并不像传统IWForm一样用控件拖表格而是把EasyUI的layout布局放进MainForm的HTML里然后初始化datagrid// www/gridpage.html 中初始化EasyUI表格的片段 $(#dataGrid).datagrid({ url: api/grid?actionlisttableproject, method: post, pagination: true, pageSize: 20, columns: [[ { field: id, title: 编号, width: 80, sortable: true }, { field: name, title: 项目名称, width: 200 }, { field: status, title: 状态, width: 90, formatter: function(value, row) { return value 1 ? 启用 : 停用; } } ]] });url里的tableproject告诉后端取哪张业务表。这样设计的好处是新表只需要后端case分支补一个分支前端页面里的url参数改成新表名即可。method: post比get稳妥因为Intraweb对长查询参数更友好中文参数也不会因为URL编码问题丢字符。pagination: true是EasyUI的客户端分页开关它会在请求里附带page和rows两个参数后端要读取这两个值做分页SQL否则前端翻页只是假动作。columns里formatter是EasyUI最讨喜的功能。它允许你把状态码在前端翻译成中文不用后端在JSON里拼HTML。上面这段代码里的status字段后端返回1或0前端自动显示成“启用”或“停用”。如果要在某个列加链接也能用同样的函数返回a标签不用后端产HTML字符串。3.3 后端处理从SQL到JSON输出的完整函数后端处理api/grid的逻辑在BackendServer.pas拆开看是这样// Backend/BackendServer.pas —— 表格数据请求的处理器 class procedure TBackendServer.HandleGridRequest(ARequest: TIWRequest; AResponse: TIWResponse); var TableName: string; SQL: string; Page, Rows: Integer; DS: TDataSet; begin TableName : ARequest.QueryFields.Values[table]; Page : StrToIntDef(ARequest.QueryFields.Values[page], 1); Rows : StrToIntDef(ARequest.QueryFields.Values[rows], 20); SQL : Format(SELECT * FROM %s LIMIT %d OFFSET %d, [TableName, Rows, (Page - 1) * Rows]); DS : TDataStore.OpenDataSet(SQL); try AResponse.ContentType : application/json; charsetutf-8; AResponse.Write(TJSONHelper.DataSetToGridJson(DS, TDataStore.GetTotalCount(TableName))); finally DS.Free; end; end;TableName来自前端url参数这是一个演示性质的设计请不要在生产项目里直接拿它拼SQL至少要做白名单校验。Page和Rows不传时默认第一页20条。Limit和Offset是分页语法如果数据库是SQL Server需要改成OFFSET FETCH或ROW_NUMBER。TDataStore.GetTotalCount是独立统计查询它和列表查询分离确保total字段永远是对的。这段代码里容易翻车的是ContentType。Intraweb默认响应可能是text/htmlEasyUI会把返回值当作JSON解析一旦拿到HTML报错信息藏在控制台里很模糊。显式指定application/json; charsetutf-8能从根上避掉这个坑。3.4 增删改EasyUI dialog表单提交到IW的完整路径列表只能看不够源码包里也做了增删改闭环。双击一行弹出EasyUI.dialog里面放一个静态表单。点保存时前端用form插件提交// 编辑对话框的保存处理 $(#saveBtn).click(function() { $(#editForm).form(submit, { url: api/grid?actionsaveid currentId, success: function(data) { var d JSON.parse(data); if (d.success) { $(#gridDemoDialog).dialog(close); $(#dataGrid).datagrid(reload); } else { $.messager.alert(错误, d.message, error); } } }); });这里把id放在查询字符串里POST体里放表单字段。后端HandleGridRequest判断到actionsave时从ARequest.QueryFields里取id从ARequest.ContentFields里取字段值。如果id为空就执行INSERT否则更新。返回对象统一带success和message两个字段success必须为true才能触发前端关闭弹窗刷新表格。如果数据库执行失败后端捕获异常后返回{success:false,message:数据库未连接}EasyUI会用消息框提示而不白屏。这里我注意到了一个细节EasyUI的form插件在提交成功后默认会把响应内容当作JSON解析所以后端返回的JSON必须是不带BOM、没有换行干扰的纯字符串。源码包在JSONHelper最后调用Trim再Write就是为了避免换行符把JSON.parse撑爆。4. 运行、调试与避坑从打开工程到排查常见问题4.1 环境准备与启动步骤跑这个源码包比想象中简单但必须先满足两个前提。第一是装了带Intraweb的Delphi版本如果在IDE里找不到TIWServerController这个控件说明组件没装好需要先重装组件包。第二是EasyUI的静态文件不能丢源码包里虽然带了一份但建议你换成自己常用的EasyUI版本方便后续升级。启动过程很简单用IDE打开项目主文件在工程里找到ServerController.pas先确认两处配置// ServerController.pas 中独立模式关键配置 ServerController.StandAlone : True; ServerController.Port : 8888;StandAlone为True时Intraweb启动后自带HTTP服务器不需要额外IIS。Port设置为空闲端口避免和本机其他服务冲突。然后直接按F9运行Intraweb会编译并启动服务浏览器自动弹出入口页如果没有弹出就手动访问http://127.0.0.1:8888/。首次访问看到的是登录页源码包里内置了一个演示账号口令明文存在SystemUserTable.txt里。登录成功后会跳转到MainForm布局里左侧是树形菜单右侧是演示列表页。如果你没有看到EasyUI样式而是裸表格先别着急写代码打开浏览器控制台看JS和CSS有没有404这通常是静态资源目录没有发布导致的。提示如果编译报错说找不到“IW”之类的开头的类先检查Intraweb组件包是否注册进IDE而不是急着改代码。4.2 常见报错排查先排第一个坑EasyUI样式完全没加载页面像老式网页。检查www目录下有没有easyui/easyui.min.css没有就从AppResources同步一份。如果文件在再检查访问路径独立模式下Intraweb的静态资源路径默认挂载在应用根下用/www/easyui/...访问不行就换成/iw/easyui/...不同版本有区别。源码包作者在ServerController.OnBeforeDispatch里加了静态文件映射如果增删了目录记得同步映射代码。第二个坑表格能出数据但中文乱码。表现为“项目名称”显示成“???”。这个锅大概率是响应头字符集而不是数据源。解决方式是在BackendServer所有写响应的地方都带上charsetutf-8同时保证Pascal文件保存为UTF-8签名。如果用的是TFDQuery连MySQL连接字符串里也要指定CharacterSetutf8否则驱动拿出来的中文本来就是坏的。第三个坑登录后第一次打开列表正常休眠一段时间再点刷新表格不再加载。原因刚才提过Intraweb的Session在闲置后过期前端EasyUI的请求被Intraweb重定向到超时页EasyUI拿到HTML后解析失败。这套源码包用定时心跳解决页面里每隔10分钟请求一次api/ping如果心跳失败就弹提示并跳回登录页。这个方案比无限延长Session超时更稳妥。4.3 避坑指南三条值得抄进自己项目的记录再讲三条更具体的踩坑记录全部来自我拆这套源码时实际遇到的现象。现象EasyUI表格的数字列排序结果不按数字大小。原因JSONHelper.DataSetToGridJson里用AsString转字段导致JSON里的数字全部带引号变成字符串。解决在转换时判断字段是否为数值类型数值字段改用TJSONNumber.Create而不是字符串修改后排序和过滤都会正常。现象表格分页后total一直等于当前页行数。原因后端的RecordCount是当前数据集行数不是全表总数尤其做了LIMIT之后。解决分开执行SELECT COUNT(*)和SELECT ... LIMIT并把总数作为ATotal传入DataSetToGridJson。我在源码里看到这个坑的注释作者显然也吃过亏。现象页面闲置后点击EasyUI按钮无反应控制台出现“Session Timeout”。原因Intraweb默认Session超时短而EasyUI的请求是普通HTTP不带IW回调令牌重新激活后会话已经无效。解决启用心跳机制并让BackendServer对未登录请求返回401。前端收到401后统一弹出重新登录提示。不要慌着给所有页面加很长的Session超时那会让内存占用肉眼可见地涨。5. 二次开发改造思路把这个Demo扩展成你自己的后台框架5.1 把固定菜单接到权限表源码里的菜单数据是写在JS里的属于硬编码。你要做的第一个改造一般是让它变成动态的。我建议用菜单表存储菜单信息包含id、parent_id、text、icon_cls、url、order_index、need_role这些字段。前端初始化时直接向api/menu发起请求后端根据当前用户所属角色过滤掉need_role不匹配的菜单节点再组装成EasyUI的tree数据。具体修改点有两处。第一处在BackendServer.pas增加HandleMenuRequest读取菜单表并按角色过滤返回格式为[{id:1,text:系统管理,children:[...]}]。第二处把MainForm里初始化树的JS改成$(#menu).tree({url:api/menu})。改完后新增菜单只需要插一条数据不用改代码重新编译这对交付给客户的项目尤其有用。后端里的大致逻辑可以这么写// 按角色返回菜单JSON procedure TMenuService.BuildMenuForRole(ARoleId: Integer): string; var MenuArray: TJSONArray; begin // 这里从menu_table中取parent_id0的顶级菜单再递归查子菜单 MenuArray : QueryMenus(ARoleId); Result : MenuArray.ToJSON; end;5.2 新增一个业务表的通用流程当你要在这个框架里加一张业务表最笨的办法是复制GridDemoForm改字段但更推荐把整个过程模板化。模板化不是写代码生成器而是把每个模块要做的事拆出来建表语句、数据访问方法、后端路由分支、前端列配置。以订单表为例SQL侧建一个orders表字段自定。数据访问层在TDataStore里新增一个函数// TDataStore 中针对订单表的查询方法 function TDataStore.GetOrderList(APage, ARows: Integer): TDataSet; begin Result : OpenDataSet( SELECT id, order_no, amount, status FROM orders ORDER BY id DESC, APage, ARows); end;后端在HandleGridRequest的case里加一个tableorders分支调用这个方法并设置响应内容。前端新建一个HTML片段复用原来的datagrid初始化把url改成tableorders字段改成订单号、金额等列。全部加起来大概三十分钟能跑通。源码包的价值正在这里你不需要动IWForm结构只新增数据目录和页面片段。如果你要加搜索条件可以再接一个q参数后端在SQL里拼WHERE条件。拼条件时注意参数化不要直接把前端值拼进SQL否则用得越久越难维护。5.3 自定义渲染EasyUI的formatter与IW控件的边界最后聊一下formatter的使用边界。很多Delphi开发者习惯把状态码转换成中文显示原本的做法是在后端拼一个“启用/停用”字符串再输出。这套源码的做法是后端只给原始值前端在formatter里映射好处是接口可以给多个页面复用后端返回结构也更稳定。比如给金额做千分位格式化完全不必上后端// 金额列的formatter每三位加一个逗号 { field: amount, title: 金额, width: 120, formatter: function(value, row) { return parseFloat(value).toLocaleString(zh-CN, { minimumFractionDigits: 2 }); } }如果要在某一列加“编辑”按钮也可以用formatter返回a hrefjavascript:void(0) onclickeditRow( row.id )编辑/a。需要注意这类按钮触发的事件函数必须定义在页面全局作用域里否则点击会找不到。这里的边界是前端负责表现逻辑后端负责数据逻辑不要两头越界。这套源码包本身不大但它的模式能承载一个完整后台。你可以在上面加权限管理、操作日志甚至给JSONHelper增加一套类型判断的优化。只要记得所有前端数据都从独立路由拿IWForm只当容器后面越改越顺。6. 一个我常用的二次开发技巧让EasyUI的每个请求自动带上会话Token在Intraweb里直接用EasyUI的url发起请求和Intraweb自带的Callback完全两套体系。Callback走的是IW自己的协议带IWCS和IWID之类参数而EasyUI发出去的是普通HTTP请求。如果你刷新页面时发现登录失效多半就是这段请求没被Intraweb识别为同一个Session。我在自己的项目里养成了一个习惯在页面入口统一给jQuery挂一个beforeSend拦截器把Intraweb的Cookie手动带进请求头。具体做法是// www/app.js 中统一追加会话标识 $(document).ajaxSend(function(event, xhr, settings) { var sessionCode document.cookie.match(/IW_session([^;])/); if (sessionCode) { xhr.setRequestHeader(X-IW-Session, decodeURIComponent(sessionCode[1])); } });这里的IW_session是Intraweb独立模式下保存SessionID的Cookie名具体名称要看你的ServerController配置。我在源码包里见过有人直接在ServerController.OnRequest里校验这个Header不匹配就返回401。EasyUI收到401会触发onLoadError这时候可以统一弹一个“重新登录”的提示框。这个技巧的价值在于项目的所有EasyUI请求都有了统一鉴权入口不用每个接口单独写Session检查。如果你用的是IIS部署Cookie名可能叫ASP.NET_SessionId只需把正则改一下如果后端加了单点登录把URL里的token参数读出来放进Header也一样适用。从那以后我每次改造这类Intraweb前端项目都强制先把这个拦截器加上避免了后面因为Session问题翻车。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。