简介:面向使用Delphi XE10.3开发网页应用的开发者,这份WebCore v1.1.3.0完整源码包提供组件库与工程资源,可在熟悉环境中以VCL/FMX思路搭建响应式界面,并研究组件封装、按项目需要做定制扩展。压缩包共1573个文件、约34.72MB,包含pas源码、dcu编译单元、dfm窗体,以及js、css等前端资源,另有png图片和资源文件,覆盖源码、示例、文档与运行时素材。已有374人学习,适合需要参考完整目录结构、查看组件源码与前端资源组织方式的中级开发者,资源包结构清晰,也可供团队技术选型时翻阅。读者可据此梳理组件从源码到浏览器资源的衔接,排查编译与部署问题,并借助示例工程验证界面、图表、导航等模块,降低从零搭建环境的成本,对理解桌面开发经验迁移到前端也有帮助,减少重复摸索。
1. 一个 rar 里装着整套编译链:WebCore v1.1.3.0 源码包到底能干什么
拿到这个包的时候我先看的不是安装说明,是目录:WebCore v1.1.3.0 的完整源码,配套 Delphi 10.3 的 IDE,里面既有运行时库,也有可视控件的 Pascal 源码,还有一个能把.pas直接压成.js的编译器。起因很具体——手上有一套跑了七八年的桌面业务系统,窗体上百个,客户突然要求“浏览器里能打开,手机上最好也能看”。重写成前端框架,人力按季度算;这条路给的是另一种解法:原来的窗体、事件、业务单元尽量不动,编译产物是 html + js,扔到任意静态服务器上就能访问。
适合谁:写过 Delphi、手里压着存量 VCL 代码想搬上 Web 的人;想搞清楚 Pascal 到 JS 这条编译链怎么落地的人;以及需要一套不依赖 npm 构建、改完刷新就能看到效果的前端方案的人。反过来也得说清楚,如果团队全是前端框架背景、没有 Pascal 语感,这套东西上手会比想象中陡,源码包能给你全部实现,但给不了你对TComponent那套生命周期的直觉。
2. 拆包先认路:pas2js 编译链与五块目录的职责划分
2.1 它凭什么能把 Pascal 跑进浏览器
它不是把 Pascal 翻译成 TypeScript 再交给打包器,也不是输出 Wasm(这个版本没有 wasm 目标),而是走 pas2js 这条路:编译器前端把.pas/.dpr解析成语法树,再降级成浏览器能直接执行的 JavaScript,同时产出.js.map。Pascal 里天生没有的东西——DOM、fetch、localStorage——由JS和Web两个单元以TJSObject的形式暴露出来,你在 Pascal 里写Document.title := '...',落到 JS 就是给document.title赋值。
这条链子有个关键前提:RTL 也被重写了一遍。System.pas、SysUtils.pas、Classes.pas在这个包里是 Pascal 源码,不是黑盒,它们会被一起编成 JS,所以字符串拼接、Format、TStringList才能在浏览器里正常工作。也就是说,你看到的“Delphi 项目能跑”,本质是“一整套用 Pascal 重写的运行时 + 一个编译器 + 一层 DOM 包装”三件套凑齐了。
理解这一点很要紧:这套框架没有虚拟 DOM,控件是直接攥着真实 DOM 节点改属性的,心智模型更接近 jQuery 时代而不是声明式框架。好处是行为可预测、调试时 DevTools 里看到的就是真相;代价是列表渲染、批量更新这类活儿得自己管,别指望框架帮你 diff。
2.2 目录职责对照表
解压之后别急着装,先把五块地方认全。不同小版本的目录名会有出入,认职责比认名字重要:
| 目录 | 里面是什么 | 你要不要动它 |
|---|---|---|
Compiler/ | 编译器可执行文件 + 目标平台运行时(rtl.js之类) | 基本不动,换大版本才碰 |
Core Source/ | RTL 源码:System.pas、SysUtils.pas、Classes.pas等 | 读为主,出诡异问题才进去看 |
Component Library/ | 可视控件源码,如WEBLib.StdCtrls.pas、WEBLib.Forms.pas | 二次开发的主战场 |
IDE/ | 向导、专家、项目模板、设计期包 | 装一次就行 |
Demos/ | 官方示例工程 | 拿来对照自己写错在哪 |
Core Source和Component Library是两回事,很多人第一次装就栽在这:库路径只加了前者,IDE 里能看到TWebForm,但拖控件的时候TWebButton死活找不到。控件单元在Component Library下面,两个路径都得进搜索路径。
2.3 版本对齐这件事,比装环境更先做
Delphi 10.3 的 IDE、源码包版本、编译器版本三者必须同源。设计期包(.bpl那类)是用某个具体版本编出来的,用 10.3 的 IDE 去加载为 10.2 编的包,轻则控件面板变灰,重则打开工程就报错退出。压缩包名字里那个for delphixe10.3不是装饰,它就是在告诉你这套二进制是对着哪个 IDE 编的。
还有一层容易忽略的:运行期源码和设计期包要成对。源码改了属性、加了published成员,设计期包不重新编、不重新Install,IDE 里对象检查器就还是老样子,你会以为改动没生效,其实是包没换。
提示:装之前把旧版本的设计期包先卸干净,
Component > Install Packages里逐个确认没有残留,再装新的。
3. 从解压到第一个页面:环境配置与编译产物剖析
3.1 库路径与设计期包
先把包放到一个没有中文、没有空格的路径下,这条不是洁癖,是血泪经验:编译器走命令行,路径里的空格和中文在某些调用链上会被截断,报出来的错还驴唇不对马嘴。放好之后进 IDE,Tools > Options > Language > Delphi > Library,把Core Source和Component Library下的源码目录都加进Library path,多条路径用分号隔开。
接着装设计期包。在Component > Install Packages里Add指向IDE目录下对应的包文件,装完组件面板上应该出现 Web 分类。装完别急着写业务,先新建一个空工程编译一次——空工程能过,说明编译器、路径、运行时三样都对上了;空工程都过不了,后面全是白折腾。
3.2 一个最小工程的三个文件
向导生成的东西删到最简,剩三个文件就够:工程文件、窗体单元、以及编译出来的 html。
program HelloWeb; uses System.Types, System.SysUtils, System.Classes, JS, Web, WEBLib.Forms, WEBLib.StdCtrls, WEBLib.ExtCtrls, WEBLib.Controls; begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.这五行就是整个应用的入口。Application.Initialize做的是浏览器侧的准备工作,CreateForm把窗体对象建出来(此时还没往 DOM 上挂),Application.Run之后才是真正渲染。顺序不能倒,先Run再建窗体,页面上会一片空白,控制台也不一定报错,这种静默失败最耗时间。
窗体单元里,TForm1继承的是TWebForm而不是TForm,其余写法跟桌面端几乎一样:
unit Unit1; interface uses System.SysUtils, System.Classes, JS, Web, WEBLib.Forms, WEBLib.StdCtrls; type TForm1 = class(TWebForm) btnHello: TWebButton; lblResult: TWebLabel; procedure WebFormCreate(Sender: TObject); procedure btnHelloClick(Sender: TObject); private { 私有字段放这里 } public end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.WebFormCreate(Sender: TObject); begin Document.title := '第一个 Web 窗体'; btnHello.Caption := '点我'; end;Document来自Web单元,是个指向浏览器全局对象的包装,title这种属性在 Pascal 里写、在 JS 里生效。{$R *.dfm}保留着,窗体的设计期布局仍然走 DFM 流式加载那一套,运行时由框架翻译成 DOM 结构——这也是为什么你从桌面端搬过来的窗体,代码不用改就能显示个大概。
提示:窗体创建事件的命名在不同小版本里可能是
WebFormCreate也可能是OnCreate,以对象检查器里能挂上的那个为准,别照抄别人的代码硬套。
3.3 脱离 IDE 手跑一次编译器
IDE 编译失败的时候,图形化的错误列表往往只给一句“编译失败”,真正的原因藏在命令行输出里。所以我一般会手跑一次:
# 前提:WEBCORE 环境变量指向解压后的根目录 "$WEBCORE/Compiler/pas2js" \ -Tbrowser \ -Jirtl.js \ -O- \ -Fu"$WEBCORE/Core Source" \ -Fu"$WEBCORE/Component Library" \ -Fu"./src" \ -o"./www/app.js" \ src/HelloWeb.dpr参数逐个说清楚:-Tbrowser指定目标平台是浏览器,决定生成哪套运行时调用;-Jirtl.js让编译器把运行时库带上,缺了它生成的 JS 一执行就报undefined;-O-关闭优化,产物可读、行号对得上,调试期必开,发布时再去掉;每个-Fu是单元搜索路径,Core Source和Component Library各一条,最后一条指向自己的源码目录;-o是输出文件。
编译通过后得有页面来接:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Hello WebCore</title> </head> <body> <!-- 编译产物由这一行引入,路径要和 -o 的输出保持一致 --> <script src="app.js"></script> </body> </html>charset必须和源码文件编码对齐,viewport不加,手机上控件会小到没法点。页面直接双击打开也能跑,但fetch走file://协议会被浏览器拦,涉及接口调试就起个静态服务器,随便哪种都行。
4. 核心链路拆解:一个按钮从点击到 DOM 更新
4.1 窗体生命周期与挂载顺序
浏览器端的生命周期比桌面端多一层“DOM 什么时候就绪”。WebFormCreate触发的时候对象已经有了,但节点未必挂到文档里,这时候去算控件尺寸,拿到的多半是 0。真正适合做布局、读容器宽度的时机是窗体显示之后:
procedure TForm1.WebFormShow(Sender: TObject); begin // 此时节点已进入文档,offsetWidth 这类测量才有意义 lblResult.Caption := '宽度 ' + IntToStr(lblResult.ElementHandle.offsetWidth) + 'px'; end;ElementHandle是控件背后那个真实 DOM 节点(不同小版本可能叫Handle或Element,对象检查器里能补全出来的那个就是)。拿到它之后,框架没帮你封装的能力全都解锁了:读offsetWidth、加事件监听、调scrollIntoView。代价是这一层脱离了框架的属性系统,改完框架不会感知,别在同一个属性上两头改。
销毁这块要提一句:浏览器有 GC,对象不 Destroy 通常不会像桌面端那样泄漏一大片,但TComponent的 Owner 机制还在,事件回调、定时器、WebSocket 连接这类“挂在全局上的东西”不手动解绑,页面切换几轮之后回调会叠加执行,现象是“点一次按钮请求发了三次”,很难查。
4.2 控件属性怎么变成 DOM 属性
控件上的每一个published属性,最终都落到 DOM 的某个 attribute 或 style 上。Caption对应文本节点,Width/Height对应 style 宽高,ElementClassName是最有用的一个——它直接写进节点的class,意味着你完全可以把样式交给现成的 CSS 框架,而不必跟控件的内联样式打架:
procedure TForm1.WebFormShow(Sender: TObject); begin btnHello.Caption := '提交'; btnHello.ElementClassName := 'btn btn-primary'; // 直接落到 DOM 的 class 上 btnHello.Width := 160; // 框架没暴露的样式,从真实节点上自己加 btnHello.ElementHandle.style.setProperty('letter-spacing', '1px'); end;这里有个取舍值得讲:能用ElementClassName就别去改内联 style。内联样式的优先级高,后面想用 CSS 媒体查询覆盖移动端表现时会覆盖不动,最后只能回来把这些代码一行行删掉。我的习惯是控件的尺寸、颜色一律交给 class,只有运行时算出来的动态值(比如进度条宽度)才写内联。
4.3 事件绑定与异步请求
按钮的OnClick在浏览器里就是个addEventListener('click', ...),差别在于回调体是 Pascal 编译出来的。下面这段是这套框架里最典型的写法——点击、发请求、在回调里更新界面:
procedure TForm1.btnHelloClick(Sender: TObject); var Req: TWebHttpRequest; begin btnHello.Enabled := False; // 先把按钮关掉,防重复提交 Req := TWebHttpRequest.Create(nil); Req.URL := '/api/hello'; Req.Command := httpGET; Req.Execute( procedure(AResponse: string; ARequest: TJSXMLHttpRequest) begin // 回调触发时 AResponse 才是最终内容 if ARequest.Status = 200 then lblResult.Caption := AResponse else lblResult.Caption := 'HTTP ' + IntToStr(ARequest.Status); btnHello.Enabled := True; end); end;Execute是异步返回的,函数体执行完的那一刻AResponse还不存在,所以任何依赖响应结果的逻辑都必须在匿名方法里。这个匿名方法会被编译成 JS 闭包,btnHello、lblResult这些外部变量被捕获进去了,在回调里能直接用——这也是它写起来比手搓XMLHttpRequest舒服的地方。参数上,AResponse是响应体文本,ARequest是原始请求对象,状态码、响应头都从后者取。
老的OnResponse事件写法效果一样,两者选一个就行,混用会出现回调执行两次的情况,尤其是从别人代码里复制粘贴的时候。
4.4 JSON 与类型转换
接口返回的是文本,要变成能遍历的结构得走一层解析。框架里 JS 值统一用JSValue表示,强转是显式的:
var Arr: TJSArray; Obj: TJSObject; i: Integer; begin Arr := TJSArray(TJSJSON.parse(AResponse)); // 顶层是数组,强转成 TJSArray for i := 0 to Arr.Length - 1 do // Length 是属性,不是函数 begin Obj := TJSObject(Arr[i]); // 每个元素强转成对象 lbItems.Items.Add(string(Obj['name'])); // JS 值转 Pascal 字符串必须显式 string() end; end;三个坑点都在注释里了:Length是属性,写成Arr.Length()会报“不是方法”;下标取值返回的是JSValue,不加强转直接访问字段,编译能过但运行时报未定义;Obj['name']拿到的是 JS 值,赋给 Pascal 的string变量时得套一层string(),否则拿到的是个对象引用,显示出来是[object Object]。
4.5 asm 逃生舱的边界
框架没封装到的浏览器 API,用内联汇编直接写 JS:
procedure LogToConsole(const Msg: string); begin asm console.log(Msg); end; end;asm ... end里的内容原样进入生成的 JS,Pascal 的参数按同名变量可见;如果编译报“标识符未定义”,把参数写成this.Msg再试,不同版本的实现细节在这里有差异。返回值走this.Result。
这个东西好用,但边界要守住:asm 块里没法用 Pascal 的异常机制,出错只能靠 JS 的try/catch;对象引用跨边界传递时类型信息会丢,回不到强类型世界;最要命的是调试——.js.map对不上 asm 块里的行,断点打不进去,只能靠console.log。我的规矩是:能用JS/Web单元封装的 API 就不用 asm,实在绕不过去,把它关在一个函数里,别散落在业务代码中。
5. 避坑与排查:五个真会卡住人的地方
下面这几条不是从文档里抄的,是装、编、调的过程中一个坑一个坑记下来的,每条按现象、原因、解决写。
5.1 编译报找不到单元
现象:Fatal: Can't find unit WEBLib.StdCtrls,但文件明明就在磁盘上躺着,路径也手工敲进去过。
原因:绝大多数是库路径只加了Core Source,漏了Component Library;其次是路径写的是相对路径,而 IDE 的工作目录在编译时是另一个位置;还有一种隐蔽情况,路径里带空格,被当成了分隔符。另外 Delphi 的单元查找对大小写不敏感、对文件名敏感,压缩包解出来如果被某些工具改过名,也对不上。
解决:Tools > Options > Language > Delphi > Library里把两条源码路径都加上,用绝对路径,别手打,从资源管理器地址栏复制。加完重启 IDE——库路径有缓存,不重启不生效这种事我遇过不止一次。
5.2 编译通过但页面白屏
现象:命令行编译零错误,浏览器打开一片白,控制台报xxx is not a function或者什么都没报。
原因:八成是 html 里引用的 js 文件名和实际输出名不一致,或者浏览器缓存了旧产物。这套框架默认把整个应用压成一个 js,改完源码重新编译,文件名没变,浏览器很可能还拿着上一版。
解决:DevTools 的 Network 面板勾上Disable cache,或者直接在地址后面挂个时间戳参数。再确认一遍 html 里<script src>的路径和-o的输出路径是同一个文件。如果报的是undefined is not a function,往上看第一行错误,通常是你调用的某个控件方法在编译产物里压根不存在,说明设计期包和运行期源码版本不一致。
5.3 设计期能看到控件,一运行就崩
现象:组件面板上控件齐全,拖进窗体正常,一按运行就弹异常或者直接卡死。
原因:旧版本的设计期包没卸干净,IDE 同时加载了两套同名的包,控件类在内存里出现两个版本,属性内存布局对不上。升级过程中最容易撞上。
解决:Component > Install Packages里把相关的包全部Remove,关 IDE,去注册表或者安装目录确认没有残留的.bpl被自动加载,再装新的。装完新建一个空工程验证,别拿老工程试——老工程的 DFM 里可能已经写入了老版本的属性值。
5.4 中文显示乱码
现象:代码里写的中文,页面显示成问号或者方块。
原因:Pascal 源文件的编码和 html 里声明的charset不一致。源文件存成带 BOM 的 UTF-8,或者 IDE 默认按 ANSI 保存,编译出来的字符串字面量就已经变形了。
解决:统一成 UTF-8 无 BOM,html 里<meta charset="utf-8">,两边对齐。改完记得清一次浏览器缓存,这类问题最容易被缓存误导,明明改对了看到的还是旧的。
5.5 断点不命中,报错行号对不上
现象:DevTools 里下的断点标在灰行上,或者报错位置指到完全无关的一行。
原因:没生成.js.map,或者用了-O-之外的优化参数,产物被压缩、变量被改名。有时候是浏览器没加载到 map 文件,Sources 面板里会提示找不到。
解决:调试期一律-O-,确认输出目录里.js.map和.js同名同级。DevTools 的 Settings 里确认Enable JavaScript source maps打开。实在对不上,就在可疑位置插一句asm debugger; end;,浏览器执行到那里会主动停下来,比猜行号靠谱。
6. 二次开发:改控件源码、加自定义控件与留后路
改这套源码,第一条规矩是不要原地改Component Library里的文件。直接改看着方便,下次换个版本的包覆盖上来,几个月的改动一夜归零,这类事故我经历过一次,之后再没这么干过。正确姿势是把要改的单元复制到自己的源码目录,改单元名(加个前缀避免和原单元冲突),然后在工程里优先引用自己这份。
改属性的最小闭环是这样:先复制WEBLib.StdCtrls.pas到自己目录,找到按钮类,加一个字段、一个published属性、一个 setter,setter 里直接操作 DOM:
type TMyWebButton = class(TWebButton) private FBadge: string; procedure SetBadge(const Value: string); published property Badge: string read FBadge write SetBadge; end; procedure TMyWebButton.SetBadge(const Value: string); begin if FBadge = Value then Exit; // 值没变就别碰 DOM,减少重排 FBadge := Value; ElementHandle.setAttribute('data-badge', Value); end;三点值得说明:published决定它能不能在设计期出现、能不能被 DFM 序列化;setter 里做等值判断不是洁癖,属性赋值在初始化阶段会被调用很多次,每次都写 DOM 会拖慢首屏;真正改视觉的是 CSS,style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />