介绍

 顶 下

uniGUI是恩巴克德罗 德尔菲和C 生成器的 Web 应用程序框架。 uniGUI功能为开发有状态的 Web 应用程序 (即spa) 提供了一整套可视化控件。 开发过程与开发常规 VCL 应用程序非常相似。 uniGUIWeb 应用程序可以直接在 RAD 工作室 IDE 中运行和调试, 这使得开发过程变得非常简单和简单。 UniGUI将 Web 应用程序开发体验扩展到一个新的维度。 在这个新的维度中, 生产率是首要目标。 uniGUI允许开发人员关注应用程序业务逻辑, 而不是处理 Web 应用程序开发细节, 例如直接使用 HTML、JavaScript、XML 模板和其他 web 技术。 uniGUI将节省许多宝贵的开发时间, 这反过来有助于大大减少项目开发、部署和支持成本。 它使uniGUI成为小型开发团队的完美工具, 这些小组的资源有限, 无法用于开发细节。 uniGUI也是在有限的时间内为大型团队创建企业级 Web 应用程序的合适工具。

部署是 Web 应用程序开发过程中的另一个重要步骤。 使用uniGUI, 部署很简单。 开发人员可以选择任何可用的部署选项, 如Windows 服务、独立服务器或ISAPI 模块。 ISAPI 模块可以使用Microsoft IIS、 Apache web 服务器或任何其他兼容的启用 ISAPI 的 web 服务器来部署。

uniGUI使用Sencha Ext JS库进行客户端渲染。 这些库被认为是创建SPA应用程序的业界领先框架之一。 uniGUI结合了 Ext JS 的威力与德尔斐的 RAD 能力, 以提供最快的方式创建水疗在德尔福。 uniGUI在一组特殊的德尔菲控制中封装了 Ext JS 类, 使开发人员能够创建功能丰富的 web 应用程序, 而无需学习客户端脚本和 web UI 详细信息。 开发人员可以专注于业务逻辑, 而不是处理重复的 UI 设计细节, 这可能是费时费力的任务。

uniGUI application in Delphi 10 Seattle IDE

uniGUI 在德尔菲10西雅图 IDE 中的应用

Web Application running inside a browser

在浏览器中运行的 Web 应用程序


系统要求

 顶 以前 下

支持德尔斐 *版本是涡轮尔斐,德尔斐 2006,德尔斐 2007,德尔斐 2009, 德尔斐 2010, 德尔福 XE-XE8, 德尔福10西雅图, 德尔斐10.1 柏林,

支持C 生成器, 但未使用所有版本进行测试。

uniGUI不需要任何特殊的硬件或 OS 配置。 典型的uniGUI安装需要100-150MB 的硬盘空间。

注 1:运行时环境的要求可能非常不同, 将在部署主题下进行讨论。

注 2:要为C 生成器安装uniGUI , 需要安装RAD 工作室 IDE。 仅安装C 生成器是不够的。 有关详细信息, 请参阅安装说明。

*所有德尔菲必须使用最新的可用更新和服务包安装版本.


安装说明

uniGUI 的安装说明 (德尔菲和 C 生成器 **)

·在安装新版本之前, 请从德尔菲中删除所有设计包, 然后从 Windows 程序添加/删除中卸载 uniGUI。

·使用此新版本重新编译应用程序后, 必须将 "ext" 文件夹重新部署到运行新版本应用程序的 pc 上, 也可以简单地重新安装新推出的 ext JS 运行时程序包, 可以从下载下载网页。

** 生成器注意事项:您需要RAD 工作室 IDE才能为C++Builder安装uniGUI。

1) 从客户门户下载最新的uniGUI安装程序。

您将注意到有两个安装版本:

·FMSoft_uniGUI_{Edition_0.XX.0.YYYY}.exe

o这是将安装在每个开发人员 PC 的开发目的。

·FMSoft_uniGUI_{Edition_runtime_0.XX.0.YYYY}.exe

o这只用于部署, 将安装在任何驻留您的 uniGUI 应用程序的服务器 PC 上。

2) 输入以下信息。 请确保电子邮件地址与在客户门户中注册的电子邮件地址完全相同。

3) 接受许可协议并按下。

4) 在 "备注" 字段中输入您的许可证密钥, 如下所示。

注:从版本0.99.95开始, 许可证密钥直接从激活服务器获得。 您可以轻松地获得您的许可证密钥, 输入您的密码, 并按 "获取许可证密钥" 按钮。 这将用适当的密钥填充许可证密钥区域。 或者, 您可以通过单击蓝色链接获得您的许可证密钥:指向许可证密钥的 Web 链接......。 这将打开一个浏览器窗口, 它将打开一个包含您的键的页面。 可以将密钥粘贴到许可证密钥区域中。

获取密钥后, 可以按 "继续". ..然后继续执行下一步。

还可以在本地文件中保存已获取的许可证密钥, 并在下次重新安装相同版本的uniGUI时使用它。

5) 选择安装文件夹。 默认安装文件夹是[ProgramFiles] \ Fmsoft \ 框架 \。

6) 选择要安装uniGUI的德尔菲版本。 必须确保在安装过程中, 德尔菲没有运行。 当前版本的安装程序不会警告您这一点。

7) 按安装启动, 完成安装过程。

8) 启动Delphi , 并打开项目组为您的Delphi 版本。 uniGUI2016Group (德尔福 XE6).。

** 生成器的附加步骤:而不是Delphi ide 打开的项目在RAD Studio ide.

9) 在项目经理有11德尔福包。 从 SynEdit_Rxxxx 开始构建所有包bpl.。

重要:不选择全部清理或清洁命令的任何uniGUI包.。 这将从 DCU 文件夹中删除所有预编译的 DCU 文件, 您必须再次安装uniGUI .

C++ Builder的附加步骤:在构建包之前, 对于每个软件包, 请转到Options->Linker and Select/Set Generate 所有C++ Builder文件。 (如果计划为 Win64 平台编译, 则必须对64位 Windows 平台重复此步骤)。

Additional step for C Builder installation only.

仅用于 C 生成器安装的其他步骤。

Additional step for C Builder Win64 target only.

C 生成器 Win64 目标的附加步骤。

如果要安装加/整版, 您将在项目组中看到与移动开发相关的三个附加文件:

10) 在生成所有包后, 通过右键单击并选择 "按以下顺序安装" 来安装设计时包:

· SynEdit_D20xx bpl

·uniGUIxxdcl bpl

·uniGUIxxChartdcl bpl

·uniGUIxxmdcl bpl(仅限附加/完整版本)

将安装几个新组件:

Complete/Plus Edition only

仅限完整/加版

C 生成器的其他步骤

·启动新的 C 项目后, 必须禁用链接器-> 动态 RTL。

·在没有资源的情况下创建新的 C 项目 (。RES) 文件。 因此, 项目没有默认图标。 此问题将在下一个版本中修复。

·C 生成器不支持组合 VCL/ISAPI 项目。

·建议您在没有运行时包的情况下构建 C 生成器应用程序。 必须静态链接所有库并创建单个 EXE。 在默认情况下, 还可以使用使用运行时程序包的默认模式。 如果要在此模式下生成应用程序, 请阅读分区运行时包 amp;C 建设者。

C++ Builder64位目标的其他步骤 (c 生成器 XE3 和更高版本)

为了为64位平台编译应用程序, 必须采取一些其他步骤:

·将所有运行时包的目标平台设置为 Win64。

·再次生成所有包, 因此将生成64位编译和链接所需的所有库。


静默安装运行时包

可以使用/静默或/VERYSILENT 命令行参数来静默安装运行时包。 uniGUI 安装程序是用创新设置构建的, 因此可以应用命令行选项:

/SILENT, /VERYSILENT

指示安装程序保持静默或静默。 当安装程序静默时, 向导和后台窗口不会显示, 但安装进度窗口是。 当安装程序非常安静时, 不显示 "安装进度" 窗口。 其他一切都是正常的, 所以例如在安装过程中显示错误消息, 并且启动提示是 (如果您没有禁用 DisableStartupPrompt 或 "/SP-" 命令行选项上面解释)。

http://www.jrsoftware.org/ishelp/index.php？topic=setupcmdline


Sencha 触摸安装

注:此步骤只需要uniGUI 加版. 。 对于其他版本, 可以跳过此步骤。

注:此步骤只需要uniGUI 加版直到修造 1425.。 对于大于1425与 Ext JS 6.5 Sencha 触摸不需要的建设。

uniGUI 加 Sencha 触摸不安装Sencha 触摸文件。 uniGUI 完成是部署 Sencha 触摸的版本。

您必须从Sencha 触摸网站下载。

以下是说明:

请从这里下载Sencha 触摸。 您需要的软件包是一个名为touch-2.4.2的文件夹, 它将被提取在触摸-2.4. 2-商业文件夹。

您必须将touch-2.4.2文件夹复制到Sencha Ext JS所在的同一位置。

例如, 如果文件夹结构如下所示:

..\ uniGUI \ Dcu

..\ uniGUI \ 演示

..\ uniGUI \ 分机-4.2 2.1144

必须将touch-2.4.2文件夹复制到.。 uniGUI

最终结构应如下所:

..\uniGUI\Dcu

..\uniGUI\Demos

..\uniGUI\ext-4.2.2.1144

..\uniGUI\touch-2.4.2


运行演示

演示是一个开始的好地方, 并很快熟悉 uniGUI 的基本知识。 幸运的是, uniGUI 有大量的示例项目来演示 uniGUI 的各个方面, 包括标准和高级功能。 几乎所有的 uniGUI 演示都与所有支持的德尔菲版本兼容。 一些演示可能不会编译在早期版本的德尔福, 因为语言不兼容和缺乏新的功能引入了在德尔福新版本。 运行演示是一个简单的任务, 几乎等同于运行任何 VCL 应用程序 (除了 uniGUI 应用程序将在浏览器中运行)。

要在德尔菲 IDE 中运行演示, 请转到文件 > 打开并查找uniGUI demos的位置。 默认位置位于< 安装文件夹 > \ FMSoft \ 框架 \ uniGUI \ 演示 \...

选择名为AllFeaturesDemo的演示文件夹, 然后打开项目mdemo。

在 IDE 中, 选择project-> 生成 mdemo并生成项目。 成功生成后, 请按 "运行", 您将注意到系统托盘上有一个新图标。

在此阶段, 桌面上可能会出现防火墙警告, 要求允许 uniGUI 应用程序侦听其专用端口。 请授予防火墙所需的权限。

现在, 为了运行一个 Web 会话, 请打开一个兼容的浏览器, 如火狐, Chrome, IE9, 边缘, Safari, 或歌剧。

在浏览器地址栏中, 键入以下 URL:

http://localhost:8077

uniGUI application running

uniGUI 应用程序运行

通过右键单击其托盘图标并从弹出式菜单中选择 "关闭", 可以终止正在运行的应用程序服务器。

如果您的 uniGUI 版支持触摸应用程序, 则可以按照相同的步骤运行移动/触摸演示, 但从其他文件夹中选择演示。

这次请选择并打开mdemo 。\触摸 \ AllFeatures。

您可以在 ide 中打开 Mainm, 以查看移动设计器在 ide 中的外观。 它看起来有点不同于标准的表单设计者模仿移动设备的外观和感觉。

uniGUI Mobile Web application in IDE

uniGUI 移动 Web 应用程序在 IDE 中的应用

现在, 您可以通过生成和运行所选项目来运行新应用程序。

再次, 将出现一个新的托盘图标。

您可以在普通桌面浏览器中测试或调试移动 Web 会话, 或者在移动设备上直接运行它。 在桌面上, 您需要一个完全 HTML5 兼容的浏览器。 幸运的是, 所有的现代浏览器都得到了 HTML5 的广泛支持。

在浏览器地址栏中, 键入上一个 URL:

http://localhost:8077

您将注意到地址将自动重定向到新地址:

http://localhost:8077/m

uniGUI mobile app in Google Chrome browser

uniGUI 移动应用程序在谷歌 Chrome 浏览器

您也可以在移动设备上运行它, 只需打开一个浏览器窗口并键入一个正确的地址即可。 这一次, 您需要输入运行演示的 PC 的 IP 地址, 还要确保您的移动设备和 pc 连接到同一个 LAN。

uniGUI web application running on a smartphone

uniGUI 在智能手机上运行的 web 应用程序


技术概述

众所周知, Web 应用程序正在替换旧的桌面应用程序。 web 应用程序可以从任何现代 web 浏览器访问, 而无需将任何内容部署到客户端设备, 更新起来更容易, 有许多优点。 但是, 在 Web 上实现桌面应用程序所提供的相同的用户界面和响应能力几乎是不可能的, 直到几年前也是行不通的。

第一次尝试将桌面用户体验带到 Web 上,丰富的 Internet 应用程序(RIA), 需要在客户端计算机上安装本地运行时框架或插件。 microsoft Silverlight, 微软 WPF XAML 浏览器应用程序, Flash, 是这一代的一些例子。 大多数这些技术现在都被否决了。

目前, 由于 Web 技术的进步, 如 HTML 5、AJAX 和强大的 JavaScript 库, 有一个新概念,单页应用程序(SPA)。 在许多方面, 它类似于桌面单文档界面(SDI), 它使用单个窗口/页面作为用户界面, 并根据用户操作动态更新它。

uniGUI是一个能够创建和部署有状态的单页应用程序的 Web 应用程序框架。

使uniGUI独一无二的是, 它看起来自然的任何德尔斐用户, 简单的使用任何其他组件库在德尔福市场。 在幕后, 许多事情必须发生, 使水疗工作。

可以创建具有自己的 Web HTTP 服务器的uniGUI SPA, 或者只是一个isapi模块, 它将需要一个 isapi 处理程序来应答来自主机 Web 服务器的请求。 但是对于任何 SPA, 作为uniGUI Web 应用程序框架的一部分, 有必要处理一些典型的任务:

1.服务器单例

2.用户会话

3.登录表单

4.主要形式

5.用于在客户端浏览器中呈现窗体的 JavaScript 代码的自动和动态生成

6.在每个客户端和服务器之间自动处理 AJAX 调用

uniGUI依赖于众所周知的Sencha Ext JavaScript 库中的所有客户端任务。 多亏了 Ext JS, uniGUI生成了一个高端, 视觉上完美, 并完全支持 AJAX 的 Web 前端。

虽然可以创建一个uniGUI SPA 不知道任何关于 JavaScript 或 Sencha Ext js, uniGUI允许利用 JavaScript 编写客户端事件处理程序的 Ext JS 控件。 此高级功能允许开发人员在不与服务器通信的情况下执行客户端屏幕元素之间的交互。 还可以向服务器发送 AJAX 请求。

uniGUI应用程序可以被视为标准的德尔菲 VCL 应用程序, 它使用 Web 浏览器作为表示层。 uniGUI使开发人员能够在开发常规桌面应用程序时创建、设计和调试其德尔菲应用程序, 并允许他们选择任何可用的 Web 部署选项。 这意味着, 一个对 web 技术一无所知的德尔菲开发人员可以开始使用uniGUI从框中开发 web 应用程序。


统一 GUI

uniGUI代表 raphical U型nterface 或单的 GUI . 。 它被称为统一的, 因为它在所有设备中都使用 Web 浏览器提供相同的UI体验。 无论设备、OS、CPU 和显示器如何, 用户体验都将与兼容 Web 浏览器的所有设备相同。 它允许在选择客户端设备时获得极大的自由度。 客户端设备可以是 Windows PC, OSX 设备, Linux pc, 智能手机, 平板电脑, 甚至覆盆子 Pi！

当然, 这个特性不是uniGUI独有的。 这一级别的平台独立性是任何 Web 应用程序都可以提供的, 但uniGUI使您能够创建与桌面应用程序非常接近的 web 应用程序。


uniGUI 应用程序体系结构

下图表示 uniGUI 服务器的内部结构。 每个 uniGUI 服务器都有一个ServerModule的副本, 每台服务器创建一次, 同时根据用户活动动态创建和销毁多个会话。 uniGUI 会话包含一个称为MainModule的专用DataModule , 它为每个会话自动创建。 它还包含一个名为MainForm的窗体, 它是 Web 应用程序的主要入口点。 如果项目包含 LoginForm,它将在MainForm之前激活, 以提供可靠和安全的用户登录方式。 如预期的那样, 每个会话可以包含几个附加的DataModules 和窗体。

每次用户打开 Web 应用程序的新实例时, 服务器都会创建一个新的会话。 会话将保持活动状态, 直到用户注销、关闭浏览器/选项卡或超时。 每个会话都保持运行的 Web 应用程序的完整状态, 以便 uniGUI 会话称为状态。 您可以将每个会话视为您的 Web 应用程序的专用副本, 它与服务器地址空间中的其他会话共存, 但与所有其他对话隔离。 服务器使用唯一的 "" 会话 id "创建每个会话, 它用于正确地将客户端请求与会话关联 (会话 id 始终是 AJAX 请求的一部分)。

如果 uniGUI 应用程序只是一个 isapi 模块, 则正在运行的服务器将实例化一个 isapi 处理程序。 如果没有 Web 服务器, 则运行的服务器将启用其内部 web 服务器。 在这两种情况下, 对活动 Web 服务器的所有请求最终都将转到正确的会话。

.


部署选项

uniGUI支持用于 Windows 平台的所有主要部署选项。

1.Standalone 服务器

最简单的部署方法是独立服务器。 在此模式下, 应用程序服务器与桌面应用程序直接运行。 它也是用于调试应用程序的模式。 运行应用程序可执行文件后, 它将自己最小化为托盘图标, 并将运行, 直到用户手动终止。 请参阅下面的图片:

uniGUI Standalone Server

uniGUI 独立服务器

只需键入即可从浏览器访问独立应用程序:

http://localhost:8077

其中8077是您的应用程序绑定到的专用端口号 (侦听端口)。 它可以很容易地修改在ServerModule。

仅建议使用独立模式进行调试。 因为它作为桌面应用程序运行, 所以在当前用户注销后, 它将立即终止。 此外, 重新启动后不会自动启动。 此模式最好用于调试目的。 当应用程序以调试模式运行时, 您可以设置断点、暂停、转到游标, 并使用德尔菲 IDE 的所有其他高级调试功能来调试应用程序, 就像其他 VCL 应用程序一样。 对于生产环境, 不建议使用此方法, 因为在操作系统重新启动时它不会自动运行。 此外, 它可以很容易地终止未经授权的用户干预。

2。。 Windows 服务

另一个部署方法是Windows 服务。 通过创建 uniGUI windows 服务应用程序, 您可以将应用程序部署为标准 windows 服务应用程序。 此方法是生产环境的首选方法之一。 每次系统重新启动时, Windows 服务都会自动运行。 它将保证 Web 应用程序所需的可用性。 可以从浏览器 (如上面描述的独立服务器) 访问 windows 服务应用程序, 这意味着每个 windows 服务应用程序都需要一个专用端口。

3。。 ISAPI 模块

最后一个可用的部署选项是ISAPI 模块。 该技术是由 Microsoft IIS 服务器引入的。 它可以从早期版本的 IIS 中获得, 而且它是基于 Windows DLL 技术的。 还有其他 web 服务器, 如 Apache, 它也支持加载 ISAPI 模块。 ISAPI 模块不同于前面讨论的许多方面的选项。 最重要的是, 它不包含内置的 Web 服务器, 而不是独立的服务和 Windows 服务。 在 ISAPI 模式下, IIS 服务器是 HTTP 服务器, ISAPI 模块执行其请求。 您可以使用德尔菲 IDE 中的 uniGUI 向导轻松创建 ISAPI 模块应用程序。 编译 ISAPI uniGUI 应用程序将输出 DLL 文件, 而不是 EXE 文件。必须将此 DLL 文件部署到 IIS 服务器 (在 " Web 部署" 部分的ISAPI 模块中详细描述)。 uniGUI dll 支持从iis 5.1开始的所有 iis 版本。 ISAPI 模块使开发人员可以自由地在同一台服务器上部署许多模块, 而无需为每个应用程序选择不同的端口。 它还继承了 Microsoft IIS 中可用的所有高级安全功能的优点。

运行 ISAPI 应用程序与在浏览器中打开以下 URL 一样简单:

http://localhost/appdir/app.dll

如果在同一文件夹下有多个应用程序, 则只需指定不同的 DLL 名称即可调用它们:

http://localhost/appdir/app.dll

http://localhost/appdir/app2.dll

http://localhost/appdir/appaccount.dll


窗体和模块

每个 uniGUI 应用程序都是用一个名为MainForm和两个datamodules、 MainModule和ServerModule的专用窗体创建的。 LoginForm在 uniGUI 也有特殊的意义 。


ServerModule

uniGUI ServerModule

每个 uniGUI 应用程序都包含一个名为 ServerModule 的特殊数据模块, 它是应用程序的核心模块。 它是一个单独的, 这意味着它只在每个应用程序中创建一次。 它主要用于配置各种服务器设置。 ServerModule 将在其他部分更详细地介绍。


MainModule

应用 MainModule

MainModule 可以被视为会话的核心。 它是一个特殊用途 DataModule, 它是在每次创建新项目时自动创建并添加到项目中的。 MainModule 在 uniGUI 应用程序中具有许多重要的作用。 某些角色对开发人员来说是不可见的。 对于开发人员, MainModule 可用于放置会话共享的资源, 如数据库连接、共享变量等。例如, 可以在 MainModule 的公共节中声明公共变量, 然后从会话中的其他窗体访问它们。 下面的示例演示 uniGUI 中用于在会话中的各种窗体之间共享数据的常见做法。 由于每个会话都有其 MainModule 的私有副本, 因此将确保每个表单在其会话中都可以访问其私有数据集。

TUniMainModule = class(TUniGUIMainModule)
private
{ Private declarations }
public
{ Public declarations }
aUserName, aPassword: string;
end;

稍后, 您可以从应用程序中的其他窗体访问这些变量:

procedure TMainForm.UniButton1Click(Sender: TObject);
begin
UniLabel1.Caption :=

UniMainModule.aUserName

' ' +

UniMainModule.aPassword;
end;

还可以参阅uniGUI 应用程序体系结构。


LoginForm

LoginForm 是另一种特殊的表单类型, 仅用于登录目的。 如果应用程序包含 LoginForm (从 TUniLoginForm 继承的任何窗体), 则它将是 Web 会话启动时显示的第一个窗体。 可以使用 uniGUI 向导创建 LoginForm, 方法如下:文件-> 新建-> 其他-> 德尔菲项目-> uniGUI 为德尔菲-> 形式。

uniGUI 向导

Create a Login Form

创建登录窗体

此操作将创建一个与常规窗体外观相同的空白 LoginForm:

A blank LoginForm

空白 LoginForm

Sample LoginForm design

样品 LoginForm 设计

LoginForm 是一个名为 TUniLoginForm 的内置类的后代.每个应用程序只能有一个 LoginForm。 添加 LoginForm 后, 应用程序将在新会话开始时显示此窗体。 您需要添加控件、事件处理程序和所需的所有内容, 以实现所需的功能。 登录行为是使用窗体的 ModalResult 控制的。 如果 LoginForm 返回mrOK,则表示成功登录, 将创建并激活新 MainForm。 当 ModalResult 返回 mrCancel 时, 它将终止会话。 如果我们使用只有两个按钮的假表单 (一个用于成功登录, 另一个用于失败), 下面的代码将执行以下操作:

procedure TUniLoginForm1.UniButton1Click(Sender: TObject);
begin
ModalResult := mrOK; // Login is valid so proceed to MainForm
end;

procedure TUniLoginForm1.UniButton2Click(Sender: TObject);
begin
ModalResult := mrCancel; // Invalid Login exit from app
end;

一旦用户登录并显示 MainForm, 就有两种方法可以终止会话。 您可以终止会话并返回到 LoginForm, 将mrOK作为 ModalResult 返回, 或通过返回mrCancel终止会话。 出于安全原因, 现有会话总是在显示 LoginForm 之前终止, 即每个新登录都启动一个新会话。


MainForm

MainForm是实现 SPA 的单一窗体, 它是在成功登录后创建和显示的 (如果不存在 LoginForm)。 通常, MainForm 是使用菜单或其他导航工具导航到其他窗体的应用程序表单。 创建新项目时自动创建 MainForm。 每个 Web 会话都有自己的 MainForm 的私有副本, 关闭 MainForm 将终止会话。

A blank MainForm

空白 MainForm

MainForm 在单元的初始化 部分注册, 以便 uniGUI 可以将其与其他表单区分开来。

initialization
RegisterAppFormClass(TMainForm);

end.


其他形式

创建新表单的 uniGUI 向导允许创建三种形式:

·Login Form (inheriting from TUniLoginForm)

·Application Form

·Free Form

申请表格与自由形式之间存在一些差异。


申请表

这是应用程序窗体的自动生成代码 (已重命名):

unit _AppForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm;

type
TAppForm = class(TUniForm)
private
{ Private declarations }
public
{ Public declarations }
end;

function AppForm: TAppForm;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function AppForm: TAppForm;
begin
Result := TAppForm(UniMainModule.GetFormInstance(TAppForm));
end;

end.

而不是像这样的变量:

var
AppForm : TAppForm;

这是为 VCL 表单生成的标准代码, 向导生成的函数具有相同的名称和类型。

原始变量在范围内是全局的, 在多用户环境中是无用的。

新函数从会话 UniMainModule 请求实例, 自动使表单本地到每个会话。

但是该函数保留了 VCL 变量的原始意图, 每个会话始终只能访问该窗体的一个实例 (每个会话都是一个单独的)。

该函数还足够聪明, 可以管理窗体的生存期:

·它将创建表单, 如果它不存在

·如果表单已关闭, 将释放该窗体, 并将从托管窗体列表中注销。 (属性FreeOnClose = True)

·如果它已经存在, 将立即返回。

使用应用程序窗体的全局函数

uniGUI 表单是动态对象, 框架自动分配和释放它们。 如上所述, 每个 uniGUI 应用程序表单都与一个函数关联, 它将创建/返回该窗体的实例。 提供此功能的主要目的是提供 VCL 和 uniGUI 之间的语法兼容性。 在 VCL 中, 所有表单都将自动创建, 并且在应用程序启动时将启动所有表单全局变量。 在 VCL 中, 只需编写简单的语法即可轻松地使用表单:

Form1.显示;

同样在 uniGUI 中, 我们提供了类似于上面的简便语法, 允许您访问您的表单:

UniForm1.显示;

必须提醒, 在上面的代码UniForm1是一个函数而不是一个变量。 它返回当前会话的窗体的正确实例, 或者如果尚未创建实例, 它将创建一个新实例。

让我重申, 此函数仅用于执行某些任务, 不应将其视为全局变量, 因为它首先不是变量。

只有在要实例化和显示 uniGUI 窗体时,才应使用此全局函数:

UniForm1.Caption := 'New Caption'; // A new form instance is created and Caption is assigned a new value
UniForm1.Show; // Show function is called for form instance created a previous line

另一个重要的细节是 uniGUI 表单一旦创建就必须显示出来。 无法在一个事件中创建窗体并将其显示在另一个活动中。 如果您尝试执行此操作, 如果不显式调用显示或ShowModal , uniGUI 将自动显示窗体。

用法不正确:

procedure TMainForm.UniButton4Click(Sender: TObject);
begin
UniForm1.Caption := 'New Caption';
end;

正确用法:

procedure TMainForm.UniButton4Click(Sender: TObject);
begin
UniForm1.Caption := 'New Caption';
UniForm1.Show; // Show must be called
end;

必须避免在 datamodule 中使用窗体实例, 除非它用于创建和显示上面演示的窗体。

您必须避免从 datamodule 或其他窗体访问窗体的公共变量。 无论如何, 所有这些都违反了良好的 OOP 设计规则。

表单不能用于存储应用程序全局变量或数据组件 (如其他窗体和 datamodules 将使用的连接)。 每个表单必须被视为应用程序的一个原子元素, 它将为自己的内部使用保留自己的私有变量。 如果要为会话定义公共变量, 则必须为此目的 MainModule 或其他 datamodules。

例如, 在下面的代码中, 窗体中的标签用于显示字段数据。 如上文所述, 必须避免使用表格。

procedure TDataModule2.DataSource1DataChange(Sender: TObject; Field: TField);
begin
UniForm1.UniLabel1.Caption := Field.DisplayText; // incorrect usage, form function should not be directly used inside an event handler of a component which belongs to another form/module
end;

相反, 您可以通过简单地使用数据感知控件 (如TUniDBText ) 并将其连接到数据源来解决此情况。

解决上述问题的另一种方法是直接在窗体上放置一个TDataSource组件, 并使用其事件更新窗体的可视元素。

这里的经验法则是避免从不属于该表单的其他组件的事件处理程序中更新表单的可视元素。

以上规则的唯一例外是, 当您完全控制特定窗体的生存期时。 即, 您在自己的代码中显式创建和销毁它。


自由形式

这是自动生成的可用窗体代码 (已重命名):

unit _FreeForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm;

type
TFreeForm = class(TUniForm)
private
{ Private declarations }
public
{ Public declarations }
end;

implementation

uses
uniGUIApplication;

{$R *.dfm}

end.

这个自由形式和申请表之间的主要区别是函数的缺席, 相当于 VCL 全局变量。

但是, 重要的区别是, 通过请求自由形式, 用户计划做的事情是不可能的申请表:

·可以创建窗体的多个实例 (例如, 在屏幕上保留多个非模式窗体以显示数据集的不同记录)

·用户将对窗体生存期进行更严格的控制。

下一个示例显示相同的窗体, 但已修改以将文本捕获为模式窗体:

unit _FreeForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm, uniButton, uniGUIBaseClasses, uniEdit;

type
TFreeForm = class(TUniForm)
edtText: TUniEdit;
btnOk: TUniButton;
btnCancel: TUniButton;
private
function GetText: string;
public
property Text : string read GetText;
end;

function GetText : string;

implementation

uses
uniGUIApplication;

{$R *.dfm}

function GetText : string;
var
frm : TFreeForm;
begin
frm := TFreeForm.Create(uniGUIApplication.UniApplication);

if frm.ShowModal = mrOk then
Result := frm.Text
else
Result := '';
end;

{ TFreeForm }

function TFreeForm.GetText: string;
begin
Result := edtText.Text;
end;

end.

自由窗体的所有者不是全局应用程序, 而是处理当前会话的实例。

还请注意, 窗体 (frm) 会自动释放 (FreeOnClose 为 true, 并将值分配给 ModalResult)。

变量本身将在超出范围后释放 (当退出函数 GetText 时)。

当用户对像 GetText 这样的服务感兴趣而不是在表单上时, 自由形式很方便。 如果是这种情况, 该窗体将始终创建并作为服务实现的一部分发布, 并且不应暴露在程序的其余部分中。 程序期待的只是服务。


DataModules

uniGUI 支持向项目添加 DataModules。 它允许开发人员设计他们的应用程序, 如标准的 VCL 应用程序, 在几个 DataModules 中划分应用程序业务逻辑。 重要的是要记住的是, 虽然 uniGUI DataModules 是相同的标准 VCL DataModules 在本质上, 当他们使用 uniGUI 向导创建, 他们将自动管理每个会话 (其生存期将与会话相同本身)。 如果 DataModule 是使用标准 IDE 创建的, 则开发人员有责任在适当的时候创建和发布它。

Using uniGUI wizard to create a new DataModule

使用 uniGUI 向导创建新的 DataModule

uniGUI 中有两种类型的 DataModules。 应用程序 DataModule和免费 DataModule。


应用 DataModule

应用程序 datamodule 是由 uniGUI 运行时获得额外支持的 datamodule:

·在应用程序初始化期间, 它在 uniGUI 运行时注册

·它是在每个会话的开始或按需 (根据属性MainModule创建的.ApplicationDataModuleOptions.CreateOnDemand)

·如果用户未释放 (使用免费), 则在关闭会话时自动释放它。

需要额外 datamodules (即大于 MainModule) 的小型应用程序可能会使用默认值 (CreateOnDemand = False), 但大型应用程序将享受更好的内存管理, 从而创建 datamodules 按需 (并将其释放为尽快)。

这是应用程序 DataModule 的自动生成的代码:

unit _AppDM1;

interface

uses
SysUtils, Classes, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Param,
FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf,
Data.DB, FireDAC.Comp.DataSet, FireDAC.Comp.Client, FireDAC.Stan.StorageBin;

type
TAppDM1 = class(TDataModule)
tblList: TFDMemTable;
tblListID: TAutoIncField;
tblListText: TStringField;
dsList: TDataSource;
private
{ Private declarations }
public
{ Public declarations }
end;

function AppDM1: TAppDM1;

implementation

{$R *.dfm}

uses
UniGUIVars, uniGUIMainModule, MainModule;

此函数返回 TAppDM1 datamodule 的实例。

当 MainModule.ApplicationDataModuleOptions.CreateOnDemand 为 true 且 datamodule 实例尚未创建时,

此函数将创建一个实例并将其返回, 否则它将返回以前创建的实例。

// When MainModule.ApplicationDataModuleOptions.CreateOnDemand is false this function will return same instance of datamodule which is created when session is started.
function AppDM1: TAppDM1;
begin
Result := TAppDM1(UniMainModule.GetModuleInstance(TAppDM1));
end;

initialization
RegisterModuleClass(TAppDM1);

end.

请注意, 此代码与应用程序表单的相似程度。

如果参数 CreateOnDemand 更改为 true, 则让我们显示窗体将如何使用应用程序 datamodule。

unit _TestForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm,
_AppDM1, uniGUIBaseClasses, uniBasicGrid, uniDBGrid;

type
TTestForm = class(TUniForm)
UniDBGrid1: TUniDBGrid;
procedure UniFormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function TestForm: TTestForm;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function TestForm: TTestForm;
begin
Result := TTestForm(UniMainModule.GetFormInstance(TTestForm));
end;

procedure TTestForm.UniFormDestroy(Sender: TObject);
begin
AppDM1.Free; // this code is optional. it can be used to free resources when associated form is freed and datamodule is no longer needed.

如果重复使用 datamodule 多次, 或者同一会话中的其他几个窗体使用该方法, 则不应调用此方式
如果这里没有调用, datamodule 将在会话终止时释放.

end;

end.

datamodule 在销毁表单时被显式释放 (即, 一旦表单不再需要它)。 但是没有代码将 UniDBGrid 链接到 datamodule。 让我们来看看 DFM 文件:

object TestForm: TTestForm
Left = 0
Top = 0
ClientHeight = 246
ClientWidth = 477
Caption = 'Test (using App DM On Demand)'
OldCreateOrder = False
MonitoredKeys.Keys = <>
OnDestroy = UniFormDestroy
PixelsPerInch = 96
TextHeight = 13
object UniDBGrid1: TUniDBGrid
Left = 0
Top = 0
Width = 477
Height = 246
Hint = ''
DataSource = AppDM1.dsList
LoadMask.Message = 'Loading data...'
Align = alClient
Anchors = [akLeft, akTop, akRight, akBottom]
TabOrder = 0
end
end

链接在表单设计器中也可见:

数据源在设计时链接到 datamodule。 在运行时没有必要这样做。

运行应用程序证明它确实工作, 而没有内存泄漏。


免费 DataModule

一个自由的 datamodule 只是一个普通的老 datamodule。

unit _FreeDataModule;

interface

uses
SysUtils, Classes;

type
TFreeDataModule = class(TDataModule)
private
{ Private declarations }
public
{ Public declarations }
end;

implementation

{$R *.dfm}

end.

这种 datamodule 的使用方式与常用 VCL 应用的方法相同。 用户完全负责在需要时创建它, 并在不再需要时立即释放它。

使用这种 datamodule 的原因来自一些非可视组件需要一个主机/容器 (或者更容易用这种方式使用它们) 的事实。 一个很好的示例是报告工具, 因为它通常会被丢弃到 datamodule 中, 即使用户只使用它来导出报表。

使用 datamodules 的另一个原因是, 不应在表示层 (TForm、TUniForm、任何窗体) 中承载 "业务代码"。 使用免费 datamodule 允许开发人员按需处理资源, 使内存使用降到最低。

在FastReport和ReportBuilder演示中演示了自由 datamodule 的用法。\demos\desktop文件夹。


框架

uniGUI 对框架有完全的支持。 必须使用 uniGUI IDE 向导创建框架。

Using uniGUI wizard to create a new Frame

使用 uniGUI 向导创建新框架

An empty uniGUI Frame

空 uniGUI 框架


ServiceModule

只有当项目类型为Windows 服务时, 才会创建 ServiceModule。 它是德尔斐标准 TService 阶级的后裔。 它允许配置与 Windows 服务相关的参数, 包括服务名称、服务类型等。


组件

作为德尔菲本身, uniGUI 提供了一套类似的非视觉和视觉组件。 某些德尔菲组件是兼容的或 uniGUI 支持的, 使得不需要将它们作为 uniGUI 包的一部分。 例如, 对于 VCL 和 uniGUI 应用程序, 许多数据访问控件都是相同的。 其他组件, 如 TActionList, 由 uniGUI 支持。

uniGUI 可视控件是 Ext JS 控件 (或与之进行), 通常提供等效 VCL 控件的超集。 有时, uniGUI 控件的功能比原始 VCL 控件更接近强大的第三方控件 (如 DevExpress 或 TMS 控件)。

接下来的部分将帮助开发人员根据他们以前在德尔菲和 VCL 控件调色板中的经验选择正确的 uniGUI 控件。


标准

uniGUI 标准控件提供来自多个德尔菲调色板的等效控件: 系统、Win32、TeeChart、标准、附加和示例。

VCL | uniGUI | 笔记
TCheckBox | TUniCheckBox |
TComboBox | TUniComboBox |
TEdit | TUniEdit TUniNumberEdit TUniFormattedNumberEdit |
TLabel | TUniLabel |
TMemo | TUniMemo |
TPanel | TUniPanel (相当于 TPanel) TUniSimplePanel (更简单的 TUniPanel) TUniHiddenPanel (隐形容器) TUniContainerPanel (可见的通用容器) TUniFieldSet (带有多个自动布局配置的字段集) TUniFieldContainer (可见字段容器) | Ext JS 面板提供了更多的对齐选项比 TPanel。 字段集和 FieldContainers 可以通过相互包容来创建树
Win32 TTabControl | TUniTabControl |
Win32 TToolBar | TUniToolBar |
Win32 TTreeView | TUniTreeView |
Win32 TStatusBar | TUniStatusBar | `
系统. TTimer | TUniTimer | TTimer 不会在 uniGUI 工作
TRadioGroup | TUniRadioGroup |
TRadioButton | TUniRadioButton |
Win32 TProgressBar | TUniProgressBar |
TButton | TUniButton |
TStringGrid | TUniStringGrid |
样品. TSpinEdit | TUniSpinEdit |
Win32 TPageControl | TUniPageControl |
TMainMenu | TUniMainMenu |
TGroupBox | TUniGroupBox |
TListBox | TUniListBox |
Win32 TImageList | TUniImageList (已否决) TUniNativeImageList (最新等价于 TImageList) TUniImageListAdapter (用于将 uniGUI ImageList 调整为 TImageList) |
TActionList |  | 支持。 使用 TUniImageListAdapter 作为其 TImageList

笔记:

·使用新的 TUniNativeImageList, 而不是已否决的 TUniImageList。

·当控件需要 TImageList 时, 请使用 TUniImageListAdapter。

·Web 控件对标准 VCL 控件的最大差异/优势与 TPanel 相关。

·不要使用 TTimer, 而是等价的 TUniTimer。 如果执行服务器线程时需要计时器, 请使用 Win32 API。


额外

此调色板提供了一些在德尔菲 VCL 中不可用的控件。

VCL | uniGUI | 笔记
TBitBtn | TUniBitBtn |
Win32 TDateTimePicker | TUniDateTimePicker |
的TImage | TUniImage |
TSpeedButton | TUniSpeedButton |
Win32 TTrackBar | TUniTrackBar |
TSplitter | TUniSplitter |
TMonthCalendar | TUniCalendar |
 | TUniCalendarDialog |
 | TUniURLFrame |
 | TUniPDFFrame |
 | TUniFileUpload |
 | TUniScreenMask |
 | TUniHTMLFrame |
TPopupMenu | TUniPopupMenu |
Win32 TRichEdit | TUniHTMLMemo | 相同的目标, 不使用转换器时不兼容
TScrollBox | TUniScrollBox |
 | TUniCanvas |
 | TUniMenuButton |
TColorBox | TUniColorPalette |
TeeChart TButtonColor | TUniColorButton |
 | TUniPropertyGrid |
 | TUniGenericControl | 使用 JavaScript 实现的控件, 能够与客户端控件交互并使 Ajax 请求服务器

.

笔记:

·新的应用程序应该使用基于 HTML 的文档而不是旧的 RTF 格式。

·可以将任何外部 URL 作为 TUniURLFrame 插入, 但对嵌入框架的控制有限。

·TUniURLFrame 能够识别和渲染一些类似的扩展。pdf (它提供了默认的 pdf 查看器)。

·TUniPDFFrame 提供了一个功能丰富的 PDF 查看器。

·检查 TUniPropertyGrid, 它是惊人的强大。


数据控件

除了 VCL 数据控件之外, uniGUI 还提供了 TUniDBTreeGrid、TUniDBVerticalGrid 和 TUniDBVerticalTreeGrid。

VCL | uniGUI | 笔记
TDBEdit | TUniDBEdit |
 | TUniDBNumberEdit |
TDBGrid | TUniDBGrid |
TDBImage | TUniDBImage |
TDBMemo | TUniDBMemo |
TDBRichEdit | TUniDBHTMLMemo | 相同的目标, 不兼容
TDBNavigator | TUniDBNavigator |
TDBListBox | TUniDBListBox |
 | TUniDBFormattedNumberEdit |
TDBComboBox | TUniDBComboBox |
TDBCheckBox | TUniDBCheckBox |
TDBText | TUniDBText |
TDBLookupListBox | TUniDBLookupListBox |
TDBLookupComboBox | TUniDBLookupComboBox |
 | TUniDBDateTimePicker |
TDBRadioGroup | TUniDBRadioGroup |
 | TUniDBTreeGrid |
 | TUniDBVerticalGrid |
 | TUniDBVerticalTreeGrid |
TDBCtrlGrid |  |

笔记:

·除了标准 TUniDBGrid 之外, 还要查找其他网格 (很像 DevExpress 网格)。


额外

uniGUI 提供了SynEdit编辑器的 Web 版本, 以及基本的 TUniChart (与 TeeChart 相比)。

VCL | uniGUI | 笔记
 | TUniSyntaxEdit |
 | TUniSyntaxEditEx |
 | TUniCalendarPanel |
 | TUniThreadTimer |
TeeChart TChart | TUniChart | 相同的目标, 不兼容

笔记:

·TUniThreadTimer 创建一个在其自己的线程中运行的计时器。 这是不同于原始 TTimer 或它的 uniGUI 等价, TUniTimer。 不要尝试访问 TUniThreadTimer 的事件处理程序中的 uniGUI 可视组件。


UniApplication 对象

UniApplication 返回一个 TUniGUIApplication 实例, 它是所有当前会话资源 (MainForm、 MainModule和所有其他窗体和 DataModules) 的所有者。 它是全局可用的, 可以在 uniGUI 控件事件处理程序内访问。 UniApplication 有几个有用的属性, 可用于获取有关关联会话的信息。 例如, URL 参数、客户端屏幕宽度、客户端屏幕高度、设备类型、与客户端 PC 相关的信息和 cookie。

在代码中创建窗体时, 必须将其所有者设置为 UniApplication。

procedure TMainForm.UniButton2Click(Sender: TObject);
begin
with TUniForm2.Create(UniApplication) do
ShowModal();
end;

UniApplication 本身实际上是一个函数, 它返回当前会话的 TUniGUIApplication 的正确实例:

unit uniGUIApplication;

interface

function UniSession: TUniGUISession;
function UniApplication: TUniGUIApplication;
...

For this reason UniApplication should only be accessed inside an event handler which is fired from a uniGUI control.

procedure TMainmForm.UnimButton1Click(Sender: TObject);
begin
if upAndroid in UniApplication.UniPlatform then
ShowMessage('This is an Android device!');
end;


UniSession 对象

UniSession 返回当前会话的 TUniGUISession 类的实例。 它包含与会话相关的所有信息。 例如, IP 地址、用户代理、主机地址、平台相关数据等。UniSession 还包含可用于控制会话 (终止会话终止) 的重要方法:

procedure TMainForm.UniButton2Click(Sender: TObject);
begin
UniSession.Terminate; // Terminate current session
end;

procedure TMainForm.UniButton2Click(Sender: TObject);
begin

// Redirect current window to a new location
UniSession.UrlRedirect('http://www.newsite.com');
end;

与 UniApplication 一样, UniSession 仅在从 uniGUI 控件事件处理程序 (即属于会话的控件) 访问时才返回有效实例。 例如, TUniButton 实例始终属于会话。

procedure TMainForm.UniButton2Click(Sender: TObject);
var
IPAddress : string;
begin

我们在一个 TUniButton 的事件处理程序中, 所以 UniSession

// can be accessed here.
IPAddress := UniSession.RemoteIP;

end;

在下面的示例中, 访问 UniSession 将导致访问冲突, 因为 UniThreadTimer 不是 uniGUI 控件, 其事件不与任何会话关联。 UniThreadTimer 事件在单独的线程中异步运行。

procedure TMainForm.UniThreadTimer1Timer(Sender: TObject);
var
IPAddress : string;
begin

这将导致访问冲突错误, 因为

// no session associated with this event
IPAddress := UniSession.RemoteIP;

end;

UniSession 是一个 "全局" 对象, 可以在从不同的会话中访问时返回不同的值。 这是相同的, 发生在 UniApplication;UniSession 实际上是在uniGUIApplication中声明的全局函数, 它在事件处理程序内调用时返回正确的会话实例。

unit uniGUIApplication;

interface

function UniSession: TUniGUISession;
function UniApplication: TUniGUIApplication;
...


UniServerInstance 对象

UniServerInstance 是在 uniGUIServer 中定义的全局函数, 它返回ServerModule单例的全局实例。

unit uniGUIServer;

interface

function UniServerInstance: TUniGUIServerModule;
...


创建新的 uniGUI 应用程序

创建一个新的 uniGUI 项目很容易。 使用德尔菲 IDE 激活 uniGUI 应用程序向导就足够了。

选择文件-> 新建-> 其他

Select uniGUI for Delphi -> Application Wizard

为德尔菲-> 应用程序向导选择 uniGUI

Select a Project Type

选择项目类型


独立服务器项目

独立服务器项目是最简单的 uniGUI 项目类型。 它创建的应用程序运行类似于桌面应用程序, 即应用程序是由用户启动和终止的。 此类应用程序最好用于调试目的。 下面是一个典型的新创建的独立服务器项目的示例。 还请参阅部署选项。

program Project1;

uses
Forms,
ServerModule in 'ServerModule.pas' {UniServerModule: TUniGUIServerModule},
MainModule in 'MainModule.pas' {UniMainModule: TUniGUIMainModule},
Main in 'Main.pas' {MainForm: TUniForm};

{$R *.res}

begin
Application.Initialize;
TUniServerModule.Create(Application);
Application.Run;
end.


独立服务器/ISAPI 模块项目

此类型的项目是一个组合项目, 使用条件编译创建两种类型之一: 独立服务器或ISAPI 模块。 如果 ISAPI 模块部署将面向生产使用, 则此类型的项目非常好。 只需注释出该文件的第一行, 就可以将独立服务器转换为 ISAPI DLL。 还请参阅部署选项。

{$define UNIGUI_VCL} // Comment out this line to turn this project into an ISAPI module'

{$ifndef UNIGUI_VCL}
library
{$else}
program
{$endif}
Project1;

uses
uniGUIISAPI,
Forms,
ServerModule in 'ServerModule.pas' {UniServerModule: TUniGUIServerModule},
MainModule in 'MainModule.pas' {UniMainModule: TUniGUIMainModule},
Main in 'Main.pas' {MainForm: TUniForm};

{$R *.res}

{$ifndef UNIGUI_VCL}
exports
GetExtensionVersion,
HttpExtensionProc,
TerminateExtension;
{$endif}

begin
{$ifdef UNIGUI_VCL}
Application.Initialize;
TUniServerModule.Create(Application);
Application.Run;
{$endif}
end.

默认情况下, 上述项目将生成一个 EXE 文件, 它是独立服务器。 如果您注释掉项目的第一行, 它将变成 ISAPI 模块。 稍后, 可以通过从第一行删除注释将其转换回独立的 EXE 模式。 下面是它的工作原理:

将组合项目转换为 ISAPI DLL 的步骤。

1.打开朝鲜文件。

2.将朝鲜第一行文件更改为: {. $define UNIGUI_VCL}

3.如果您的德尔菲版是 XE2 或更新的, 您需要关闭项目并再次打开它。 (如果您的德尔菲版本比 XE2 早, 则不需要此步骤)

4.构建您的应用程序。

5.DLL 文件将在输出文件夹中创建。

将组合项目转换回独立模式的步骤。

1.打开朝鲜文件。

2.将朝鲜第一行文件更改为: {$define UNIGUI_VCL}

3.如果您的德尔菲版是 XE2 或更新的, 您需要关闭项目并再次打开它。 (如果您的德尔菲版本比 XE2 早, 则不需要此步骤)

4.构建您的应用程序。

5.您的项目将再次编译为 EXE 文件。


ISAPI 模块项目

如果计划启动 ISAPI 模块项目并将其选择为默认部署方法, 则可以选择此类型的项目。 该文件显示该项目在编译时将生成 DLL。 有关部署选项, 请参见ISAPI 模块部分。 ISAPI 模块可以部署到 Microsoft ® IIS 服务器或用于 Windows 的 Apache 服务器。 还请参阅部署选项。

library Project1;

uses
uniGUIISAPI,
ServerModule in 'ServerModule.pas' {UniServerModule: TUniGUIServerModule};

{$R *.res}

exports
GetExtensionVersion,
HttpExtensionProc,
TerminateExtension;
end.


Windows 服务项目

生产环境的另一种部署方法是Windows 服务。 它创建一个标准的德尔菲 Windows 服务项目, 并进行了一些修改, 以允许创建 uniGUI ServiceModule。 服务项目生成的标准 windows 服务可执行文件可以像任何常规 windows 服务一样安装。 windows 服务由 windows 自动启动, 只要运行 windows, 它始终可用。 这为 uniGUI 应用程序提供了高水平的可用性。 另外, 请参阅部署选项。

program Project1;

uses
SvcMgr,
ServerModule in 'ServerModule.pas' {UniServerModule: TUniGUIServerModule},
MainModule in 'MainModule.pas' {UniMainModule: TUniGUIMainModule},
Main in 'Main.pas' {MainForm: TUniForm},
ServiceModule in 'ServiceModule.pas' {UniServiceModule: TUniGUIService};

{$R *.res}

begin
if not Application.DelayInitialize or Application.Installing then
Application.Initialize;
Application.CreateForm(TUniServiceModule, UniServiceModule);
Application.Run;
end.


应用程序设计注意事项

有些方面使 uniGUI Web 应用程序与桌面应用程序不同。 桌面应用程序设计为在桌面上作为单个实例运行, 并且它可以使用所有可用的系统资源。 大多数情况下, 这将导致应用程序设计不佳, 因为开发人员不认为需要优化资源使用。 随着计算机技术的进步, 新的系统在每个新的模型上都有更多的资源。 几年前, 有 128mb RAM 的个人电脑被视为入门级, 而今天这个限制升级到至少 2 GB 的内存。 拥有这么多资源可能会导致开发人员在设计应用程序和浪费系统资源时采用低效的做法。 与单个用户会话在单个进程内运行的桌面应用程序不同, Web 应用程序承载多个在同一进程实例中运行的用户会话。 在这种情况下,资源管理成为一项至关重要的任务, 需要仔细分析。

另一个差异与共享资源有关。 在桌面应用程序中, 所有资源都专用于一个用户, 而在 uniGUI Web 应用程序中, 资源在几个用户会话之间共享。 任何资源管理不善都会影响 Web 应用程序服务器的运行状况, 这可能会导致服务器不稳定。 资源管理不善的一个例子是内存泄漏。 在桌面应用程序内存中, ks 可能是一个严重的问题, 如果它运行了很长一段时间。 由于桌面应用程序通常每天使用几个小时, 主要是工作时间, 因此这种内存泄漏不会导致严重问题。 但是, 在 Web 应用程序中, 内存泄漏会在短时间内严重影响服务器的稳定性。 如果桌面应用程序出现故障, 受影响的用户可以重新启动它并继续其工作。 如果 Web 应用程序失败, 所有连接的用户都会受到影响。

Web 应用程序服务器的目的是工作 24/7, 因此任何与资源管理相关的问题都可能随着时间的推移而累积起来, 从而导致服务器耗尽资源。 这样的情况会导致应用程序服务器不稳定, 最终会造成服务器崩溃。 讨论共享资源时, 值得一提的是内存损坏问题。 在桌面应用程序中, 内存损坏问题可以长期保持不被发现, 因为这种损坏可能不会产生危险的副作用。 在 Web 应用程序中, 内存在多个线程中以非常高的速率分配和释放, 并且任何内存损坏都会导致服务器不稳定和应用程序行为不稳定。

最后, uniGUI Web 应用程序具有大量多线程, 多个会话以不同的线程执行。 因此, uniGUI 中使用的组件必须是多线程识别的。 没有多线程识别的组件将导致严重问题, 从而导致 Web 服务器不稳定。


总体设计理念

每个 uniGUI 应用程序都是用MainForm、 MainModule和ServerModule创建的。 如前所述, MainModule 和 MainForm 每个会话实例化, 而 ServerModule 是一个单例。 除了 MainModule, 您还可以向应用程序添加额外的DataModules。 如果需要, 还可以添加LoginForm。

MainModule 是用您的应用程序自动创建的, 它执行许多重要任务。 MainModule 是将共享资源用于会话的最佳位置。 这些资源在会话的窗体和 DataModules (或由它们调用的单位) 之间共享。 数据库连接是共享资源的一个很好的示例。 我们可以将每个会话视为 web 应用程序的单独副本, 并且与常规应用程序一样, 您需要为每个应用程序实例单独的数据库连接。 在 uniGUI 中, 可以通过将连接组件放在 DataModule 上来实现, 以便在会话终止时启动和销毁新会话时创建连接。 这将保证每个会话将使用其数据库连接的专用副本。

共享资源的另一个示例是共享数据结构。 例如, 每个会话可能需要保留保存当前用户信息的共享记录。 应用程序中的所有其他窗体和 DataModules 都应能够在需要时访问此结构。 请考虑在MainModule单元中定义的以下数据结构:

type
TUserInfo = record
Name, Surname : string;
UserId : Integer;
end;

此结构需要从会话中的其他窗体访问, 因此我们必须将其定义为TUniMainModule中的公共属性:

TUniMainModule = class(TUniGUIMainModule)
private
{ Private declarations }
FUserInfo: TUserInfo;
public
{ Public declarations }
property UserInfo: TUserInfo read FUserInfo;
end;

当用户登录时, 上面的结构将使用真实的用户数据填充。 MainForm可以像这样访问用户信息:

procedure TMainForm.UniFormCreate(Sender: TObject);
begin
UniLabel1.Caption :=

UniMainModule.UserInfo.Name +

' ' +

UniMainModule.UserInfo.Surname;
end;

您可以看到, 通过访问修改后的UniMainModule对象的属性用户信息可以很容易地完成该操作。 由于每个会话都有其UniMainModule的私有副本, 因此每个窗体都将访问用户信息记录的正确实例。

在这一点上, 值得一提的是, 应该避免在 uniGUI 中使用共享的全局对象。

例如, 可以将 TUserInfo 定义为 MainModule 中的全局变量:

unit MainModule;

interface
...
var
UserInfo: TUserInfo;

并在 MainForm 中访问它:

procedure TMainForm.UniFormCreate(Sender: TObject);
begin
UniLabel1.Caption := UserInfo.Name + ' ' + UserInfo.Surname;
end;

以上代码实际上在初始测试中有效, 因为只要只有一个活动会话,用户信息就不会被共享, 而且应用程序似乎行为正常！ 但是, 一旦多个会话登录, 应用程序就会以一种奇怪的方式开始行为。

当一些会话在一次活动时, 这种设计缺陷可能仍未被发现, 但是随着负载的增加, 初始设计缺陷的副作用将变得更加明显, 并且可能不容易发现问题的根源。 因此, 从一开始就坚持正确的设计模式是很重要的。

同样, 必须避免将组件放在ServerModule上。 如前所述, ServerModule 是一个单一的, 它不会为每个会话创建, 所以例如, 将数据库连接组件放在 ServerModule 上可能会在没有会话的情况下工作, 但是有更多的会话, 您将会收到意想不到的错误。难以检测和调试。

Application ServerModule


Web 应用程序可伸缩性

当谈到 web 应用程序开发时, 可伸缩性是最重要的主题之一。 Web 应用程序旨在为许多用户提供服务。 而对于一些应用程序, 一些同时使用的用户可以是最大值, 对于其他应用程序100可以是最小值。 它是桌面应用程序和 web 应用程序之间的主要区别之一。 桌面应用程序一次只为一个用户提供服务, 但是 web 应用程序同时为多个用户提供服务。 随着用户数量的增长, 可伸缩性问题可能会开始显现。 许多此类问题都与错误的应用程序设计有关。 web 应用程序必须能够为多个并发会话提供服务, 并且它应该注意增加的资源使用量。

请考虑将桌面应用程序移植到 Web 应用程序。 在桌面应用程序中, 您有一个 TDataset 和一个 TDBGrid, 它从表中加载所有可用行。 现在假设每次新会话开始时, 从表中提取数以千计的行并添加到网格中。 请考虑每次表变为活动状态时, 两个数据结构都可能消耗多达几兆字节的内存。 如果它是桌面应用程序, 则不会产生任何问题, 因为所有系统资源都可能属于桌面应用程序。 但是, 在 Web 应用程序中, 每个新会话都将消耗此内存量。 在这种情况下, 如果您有100个活动会话, 并且每个会话消耗 10MB RAM, 则所需内存总数将为 100 x 10 = 1000 MB = 1 GB。

正如您所看到的那样, 如果您的会话没有为资源使用进行优化, 内存消耗可能会变得很大。 它显示了资源优化对于可伸缩性很重要的应用程序的重要性。 在某些情况下, 可伸缩性没有最高优先级。 例如, 如果您为 s 系统开发了多达10个用户的 web 应用程序, 则可伸缩性不是问题, 但是对于一个 web 应用程序, 其中100个用户是最低的, 资源管理应确保应用程序不会耗尽资源。

压力测试工具可以帮助找到可伸缩性问题。 运行压力测试可以精确显示在运行一定数量的会话时服务器将消耗的最大内存量。 它可用于在最坏情况下测试 web 应用程序, 并在这种罕见的情况下查看是否可以通过测试。 如果您的应用程序可以通过压力测试, 它将准备部署到生产环境中。

uniGUI 本身在许多方面进行了优化, 以处理资源问题。 为了给出一些例子, 它将位图存储在缓存文件中, 在用户关闭它们时创建窗体, 并在缓存文件中保留 ImageLists。同样, 开发人员应该考虑采用基于需求的创建资源来优化资源使用。 一个很好的例子可以是数据库表和相关的数据集。 在经典的德尔菲方法中, 开发人员可以将连接组件和所有相关的数据集放在 DataModule 上, 但在 uniGUI 中, 它将强制在每次新会话启动时创建所有数据集。 如果数据集在默认情况下处于活动状态, 则会造成更多问题。

在 uniGUI 中, 正确的方法是将连接组件放置在 MainModule 上, 并将数据集放在窗体或窗体所拥有的 DataModules 上。 在窗体上放置数据集时, 将在窗体关闭时显示和销毁窗体时创建它。 此方法将优化与数据集相关的内存使用。


根据需要创建资源

这是设计可伸缩 web 应用程序时必须遵循的方法。 这里的一般规则是在需要时 (而不是以前) 创建资源敏感对象, 并在不再需要它们时销毁它们。 一个示例是数据库表。 如上一节所述, 数据库表可以一次加载许多行, 这样可以增加更多行数的内存使用量。 这是真的, 特别是与内存数据集。 德尔菲的内置TClientDataset是内存数据集的一个例子。 为了确保在需要时创建并加载数据集, 最好将数据集放在存在 db 感知控件的窗体或帧上。 每次用户需要访问该表单时, 它都会随表和数据库感知控件一起创建和显示。 当用户关闭表单时, 数据集将被释放并将其内存发送回应用程序内存池。

该方法可应用于报告工具。 与其将报表组件放在 MainModule 上, 不如在生成报表之前创建它, 并在生成报表并将其保存到临时文件中后销毁。


数据库连接

与数据库的连接是在应用程序可伸缩性方面应考虑的另一个资源。 根据建议, 对于每个会话, 连接对象应放在MainModule上。 将在启动新会话时创建连接对象的新实例, 因此在典型情况下, 每个会话都将建立到数据库服务器的新连接。 如果数据库服务器只能为有限数量的同时连接提供服务, 这可能会带来可伸缩性问题。 这是许多数据库服务器的情况。 其中有些人的默认限制为百, 而另一些则可以处理数以千计的连接或更多。 同样, 数据库连接必须被视为有限的资源, 您的应用程序不应达到上限。

您可以将服务器配置为提供更高数量的同时连接, 但这不是解决此问题的正确方法。 有办法解决这个问题:

1。。 启用连接池。 连接池是一种重新使用具有相同属性的数据库连接的方法。 某些数据库库默认情况下实现池, 而另一些则不实现此功能。 必须检查特定的连接库才能查看池功能是否可用。 例如, 在默认情况下, TADOConnection组件与Microsoft SQL server结合启用池。 FireDAC 还支持池, 并且可以启用。 请参阅FireDAC节。

2。。 使用中间件库。 当并发会话的数量大于数据库服务器的上限时, 解决此问题的一种方法是使用中间件层。 这是中间件层的目的之一, 以最佳的方式管理数据库连接。 中间件层在需要时创建一个连接, 当它不再使用时释放它, 从而可以为许多会话同时提供几个连接。 一些中间件库实现了连接池, 从而提高了性能。 在德尔菲中常用的中间件库中, 我们可以计算DataSnap、 DataAbstract和kbmMW。


Web 应用程序稳定性

使用 Web 应用程序, 稳定性是其他主要关注的问题。 稳定性和可伸缩性可以看作是两个相互关联的概念。 如果一个 Web 应用程序一开始不稳定, 就不能进行伸缩。 因此, 实现可伸缩性的第一步是确保 Web 应用程序完全稳定。 可以说, 可伸缩性问题可能导致不稳定的应用程序。 同样, 不稳定的应用程序也很难伸缩。 因此, Web 应用程序设计器必须在所有开发阶段都牢记可伸缩性和稳定性。

我们已经在Web 应用程序可伸缩性章节中讨论了一些与稳定性相关的设计问题。 在本章中, 我们将尝试深入地讨论稳定性概念。


内存管理

德尔菲应用程序是 windows 本机应用程序, 即它们可以直接在 windows PC 上运行, 而不需要运行库 (如. NET Framework 或 Java)。 每一个德尔菲可执行文件都是一组在 cpu 上本机运行的机器代码指令, 不同于. NET 或 Java 应用程序, 需要翻译人员首先将特殊的 OP 代码转换为 cpu 机器代码。 本机德尔菲应用程序与托管代码应用程序 (如 Java 和. NET) 之间存在着一些差异, 但在这里, 我们将只关注内存管理。

像. NET 和 Java 这样的框架配备了具有内置垃圾回收器的高级内存管理员。 垃圾回收器的可用性使开发人员的生活更加轻松。 垃圾回收器自动管理对象生存期。 您只需创建对象, 而垃圾回收器将在不再需要时处理它们。 这将为开发人员创建一个更安全的环境, 因为它们不应照顾内存管理。 它们可以将内存管理完全保留在装有垃圾回收器的框架中。

另一方面, 德尔菲没有垃圾回收器, 开发人员应该正确地处理销毁对象。 的确, 德尔菲具有某些功能, 允许自动处理对象, 如与接口一起使用的自动引用计数, 但它远不是一个完整的垃圾回收器。 因此, 德尔福开发人员应始终照顾的对象寿命时, 他们创建他们手动, 即你必须摧毁你所创造的！ 幸运的是, 您不需要处理在设计时创建的对象, 如窗体、框架、数据模块、组件和控件。 您只需要处理在代码中动态创建的对象。

虽然适当的内存管理对 web 应用程序至关重要, 并且应该非常小心地处理, 但在设计桌面 VCL 应用程序时, 可能并不那么谨慎。 桌面 VCL 应用程序的生存期有限, 即它们是由用户启动和终止的。 典型的桌面客户端应用程序可能每天只工作几个小时, 通常为6-10 小时。 另一方面, web 应用程序服务器被设计为无限期地运行。 虽然有几种方法可以回收或周期性地重新启动 web 应用程序, 但其背后的主要思想是设计它们以连续方式运行。 桌面 VCL 应用程序设计器可能会对对象生存期管理采用错误的编码习惯, 但是由于桌面应用程序的生存期有限, 这些错误的习惯不会影响最终用户, 原因有几个。

如上所述, 其中一个原因是许多 VCL 应用程序的生命周期短了几个小时。 随着这个短暂的生命周期, 即使严重的内存泄漏仍然可以隐藏和处理。 现代客户端 pc 配备了数 gb 的内存, 与典型的 VCL 应用程序中可能发生的最严重的内存泄漏相比, 这是巨大的。 这就是为什么 VCL 开发人员可以采用错误的编码习惯而不知道它们的原因。 但是, 这些习惯应该避免用于 Web 应用程序。 在 Web 应用程序中, 许多会话将被动态创建和销毁, 甚至每个会话中的小内存泄漏都可能导致在一段时间后过度使用内存, 这最终会导致服务器不稳定和崩溃。

当遗留 VCL 应用程序移植到 uniGUI 时, 这是典型的风险。 在这种情况下, 必须格外小心, 以确保遗留的 VCL 代码没有内存泄漏。 幸运的是, 在德尔菲中, 通过向项目文件中添加一行, 可以很容易地跟踪内存泄漏。

begin
{$ifdef UNIGUI_VCL}

// add this line to report memory leaks
ReportMemoryLeaksOnShutdown := True;
Application.Initialize;
TUniServerModule.Create(Application);
Application.Run;
{$endif}
end.


正确的内存管理的重要性

为什么正确的内存管理对 web 应用程序如此重要？

答案很简单: 与任何其他服务器应用程序一样, uniGUI 服务器被设计为连续运行。 这意味着任何有害的内存操作都会对服务器的功能和稳定性产生负面影响。 当服务器负载较低时, 这些效果可能会保持隐藏和未被发现, 但当更多客户端请求其服务时, 这些影响将会可见。

现在让我们更详细地讨论那些所谓的有害内存操作:

1.内存泄漏

当手动分配的内存从未释放时, 会发生内存泄漏。 内存泄漏的来源可以是多种多样的。 一个非常常见的错误是动态创建对象而不添加适当的代码来处理它。 下面的代码显示内存泄漏的示例:

A := TSomeControl.Create(nil);
A.Property1 := 0;
A.Run;
A.Free;

乍一看, 上面的代码可能看起来是正确的, 因为它调用自由 ()方法来处理创建的控件。 但是, 如果Run ()方法失败并引发异常, 将会发生什么情况？ 在这种情况下, 将跳过自由方法, 并会发生内存泄漏。 这是相同代码的更正版本:

A := TSomeControl.Create(nil);
try
A.Property1 := 0;
A.Run;
finally
A.Free;
end;

在前面的代码段中, 无论try 中的代码如何, 都将始终调用自由 ()方法 .. 最后块。

下面的代码显示错误的解决方案 (它不会产生内存泄漏, 但会导致内存损坏)。

try

// consider that an exception occurs while creating the object
A := TSomeControl.Create(nil);
A.Property1 := 0;
A.Run;
finally

在这种情况下, 变量 A 将不被初始化, 调用

// to Free may lead to memory corruption
A.Free;
end;

如果第一个语句未能通过引发异常来创建对象 A, 则下一条语句将是释放 a 的请求。 但是 A 的值未定义, 因为创建对象的失败不会给变量赋零。 尝试释放指向随机位置的对象可能会导致内存损坏或触发访问冲突。

内存损坏

当内存操作覆盖已被对象或变量占用的另一个内存位置时, 会发生内存损坏。 内存损坏比内存泄漏更有害。 内存损坏可能会在一段时间内保持隐藏, 但最终您将开始观察应用程序中的访问冲突错误和 uniGUI 日志文件。 有许多内存损坏的来源。 其中大部分是错误的编码习惯的结果。 如前几节所述, 某些错误的编码习惯在桌面 VCL 应用程序中可能不会有害, 但在服务器应用程序中, 这些习惯的负面影响将变得明显。

可能是多线程服务器应用程序中内存损坏的主要来源之一是使用全局变量。 如果服务器应用程序需要全局变量, 则必须保护对它们的访问。 在标准的 VCL 应用程序中, 使用全局变量是常见的, 但在多线程应用程序中, 只有在严格必要时才应使用它们。 虽然使用序号变量 (如整数) 不会导致即时内存损坏, 但使用动态全局字符串会导致内存损坏。 当然, 内存损坏只是使用全局变量的一个副作用, 而全局变量的另一个重要问题是它们在会话之间共享。

请考虑以下情况:

您有一个带有登录表单的应用程序, 并将用户信息保存在记录变量中。

type
TUserRecord = record
Name, Surname: string;
end;

现在, 让我们将它声明为一个单元中的全局变量 (错误的方式):

var
CurrentUser: TUserRecord;

在新用户登录后, 让我们将用户名分配给此变量:

procedure TMainForm.UniBitBtn1Click(Sender: TObject);
begin
CurrentUser.Name := UniNameEdit.Text;
CurrentUser.Surname := UniSurnameEdit.Text;
end;

该方案存在两大问题:

·创建新会话并登录用户 #1 时, 将将其名称分配给 CurrentUser 变量。 如果创建了另一个会话, 并且登录了用户 #2, 此时 CurrentUser 将被当前会话的新用户信息覆盖。 显然, 第一个会话将丢失以前保存的用户信息, 因为它被第二个会话覆盖。

·此方案可能容易导致严重的内存损坏。 如果应用程序的通信量略高, 则会导致从多个线程中写入冲突, 从而导致字符串损坏。 在德尔菲中, 字符串对象是由内存管理器内部管理的动态内存位置。 当两个并发线程在同一字符串上写入时, 它可能容易导致指针损坏。

现在, 人们可能会问, 实现这种情况的正确方法是什么？ 答案很简单, 很简单。 在 uniGUI 中, 每个会话都带有它对该会话公开的模块和对象的集合, 但对于其他成员则是私有的。 窗体和模块是此类对象的示例。 每个会话都有自己的 MainForm、其他窗体、MainModule 和 DataModules (DataModules 用 uniGUI 向导创建) 的私有副本。

因此, 如果要声明一个 "全局" 变量, 请将其声明为上述对象之一的字段或属性。 通过这样做, 您的变量将是全局或公共的, 它的父会话, 但私有或不可见的其他会话。 它将避免我们刚才讨论的所有与内存相关的问题。

让我们根据这些规则重新实施我们的方案。 在这里, 我们将使用 MainModule 作为我们的 UserRecord 容器类。 必须将名为UserRecord的新属性添加到TUniMainModule类中.

type
PUserRecord = ^TUserRecord;
TUserRecord = record
Name, Surname : string;
end;

TUniMainModule = class(TUniGUIMainModule)
private
{ Private declarations }
FUserRecord: TUserRecord;
function GetUserRecord: PUserRecord;
public
{ Public declarations }
property UserRecord: PUserRecord read GetUserRecord;
end;

function UniMainModule: TUniMainModule;

implementation

{$R *.dfm}

uses
UniGUIVars, ServerModule, uniGUIApplication;

function UniMainModule: TUniMainModule;
begin
Result := TUniMainModule(UniApplication.UniMainModule)
end;

function TUniMainModule.GetUserRecord: PUserRecord;
begin
Result := @FUserRecord;
end;

这将确保每个会话都可以访问其自己的 UserRecord 的私有副本, 从而避免使用全局变量引发的任何可能的内存问题。

最后, 我们可以使用以下语法简单地访问其他窗体和模块中的UserRecord:

procedure TMainForm.UniBitBtn1Click(Sender: TObject);
begin
UniMainModule.UserRecord.Name := UniNameEdit.Text;
UniMainModule.UserRecord.Surname := UniSurnameEdit.Text;
end;


对象生存期管理

在德尔菲中, 存在多个层次的对象生存期管理。

德尔菲负责处理窗体上的组件和控件等设计时对象。 它还将自动释放任何自动创建的表单。

但是, 在代码中创建的任何动态对象都是开发人员的责任。 如果使用具有引用计数的接口, 则不需要显式释放变量。 但开发人员将负责释放任何其他变量。 如果开发人员无法处理这些变量, 它将创建内存泄漏, 这可能会耗尽服务器内存, 并在一段时间内使应用程序崩溃。

创建具有短生命周期的动态对象的典型语法是在尝试 中创建和处理它们。 最后块。

F := TFileStream.Create('NewFile.bin', fmCreate);
try
F.Write(Buffer, 1024)
finally
F.Free; // dispose object when it is done
end;

尝试 时将释放文件流对象. 最后执行块。 在创建具有短寿命的动态对象时, 必须遵循此原则。


ISAPI 模块的特殊注意事项

ISAPI 模块是由 Microsoft IIS web 服务器加载的专用 DLL。 ISAPI 模块是标准的 Windows dll, 必须由另一个进程承载。 在 IIS 中, 名为 w3wp.exe 的进程加载并服务 ISAPI dll。 每个 ISAPI DLL 都从其宿主进程继承特权和访问权限。 它只能访问由宿主进程访问的文件夹、文件和其他系统资源。

Windows 为每个 ISAPI 主机进程分配一个用户帐户, 这些过程既可以是名为 IUSR 的内置帐户, 也可由用户配置的任何其他用户帐户。 宿主进程也称为 ISAPI 池。 实际上, ISAPI 池是共享相同配置的主机进程的集合。 每个池都可以配置为运行一个或多个 ISAPI 应用程序。 有关 IIS 7.0 的内容, 请参阅此部分以查看如何创建和配置应用程序。

可以在多个 ISAPI 模块之间共享一个池。 还可以为每个 ISAPI 模块创建单独的应用程序, 并为每个应用程序分配单独的池。 同一池中的应用程序与其他池中的其他应用程序完全隔离, 但同一池中的应用程序没有完全隔离。 如果池中的某个应用程序变得不稳定, 则会影响共享该池的其他应用程序。 如果一个池崩溃, 则在应用程序池再次启动之前, 该池中的所有应用程序也将变为不可用。

uniGUI 应用程序是有状态的, 将复杂的状态信息保留在服务器内存中。 如果应用程序崩溃, 则活动会话将丢失状态信息。 当应用程序池再次联机时, 所有连接的用户都应登录到新会话。 当应用程序池因不稳定的 ISAPI 模块而崩溃时, 对于所有用户来说, 这可能是一个不愉快的体验。 因此, 请确保 uniGUI 应用程序在将其部署到生产环境之前足够稳定。


线程堆栈大小

每个 Windows 线程都接收与其主机进程的默认堆栈大小相同的私有堆栈。 在独立或 Windows 服务模式下, 堆栈大小由德尔菲决定。 默认情况下, 德尔菲应用程序的堆栈大小为 1 MB。 可以将其更改为Project-> 选项-> 链接-> 最小堆栈大小/最大堆栈大小参数。

还可以通过编译器指令调整它们:

{$M minstacksize,maxstacksize}
{$MINSTACKSIZE number}
{$MAXSTACKSIZE number}

这些设置仅适用于可执行的二进制文件。 ISAPI dll 使用主机进程的内部线程的默认堆栈大小值。 对于 uniGUI 应用程序来说, 这可能是一个问题, 它设计为使用与 DLL 宿主进程的堆栈大小不同的堆栈大小。 在 IIS 7.0 和更高版本中, 32 位应用程序的默认堆栈大小为 256Kb, 64 位应用程序为512Kb。 这些值低于 1 MB 的德尔菲预设。 因此, 您可能会遇到一个在独立模式下正常运行的 web 应用程序在以 ISAPI 格式部署时可能会产生意外的堆栈溢出异常。

德尔菲应用程序的默认 1 MB 堆栈大小实际上是典型应用程序的一个大值。 德尔菲应用程序不应依赖堆栈大小来正常工作。 堆栈是内存中的临时存储, 用于函数和过程中的临时变量。 在设计应用程序时, 最好将局部变量的使用率降至最低。 例如, 在下面的代码段中, 方法需要本地数组才能使用:

procedure TMyClass.DoCalculate;
var
MyCalcArray : array [1..10240] of Integer;
I : Integer;
begin
for I := 1 to 10240 do
MyCalcArray[I] := I * 10;
end;

前面的代码使用本地数组。 此数组在堆栈中创建, 并占用有限的堆栈内存空间。 应避免这种地方数据结构;开发人员应使用在应用程序的主内存池 (堆) 中创建的动态数据结构:

var
MyCalcArray : array of Integer;
I : Integer;
begin
SetLength(MyCalcArray, 10240);
for I := 1 to Length(MyCalcArray) do
MyCalcArray[I] := I * 10;
end;

如果应用程序使用堆栈溢出消息崩溃, 则表示对方法的递归调用, 即 bug, 或者应用程序堆栈使用率大于默认堆栈设置。 在这种情况下, 您必须尝试通过更改代码和基础逻辑或增加应用程序的默认堆栈大小来减少应用程序堆栈的使用。

值得知道的是, 堆栈溢出是一个严重的条件, 没有恢复选项。 在某些情况下, 堆栈溢出条件将在 uniGUI 日志文件中用屏幕上的异常消息记录错误, 但在 isapi 应用程序中, 堆栈溢出错误可能会在没有屏幕异常消息或错误日志的情况下默默地终止 isapi 池。uniGUI 日志文件。 但是, 您将能够在 Windows事件查看器中看到一条消息, 显示应用程序池因意外错误而终止并重新启动。

更改 ISAPI 应用程序的默认堆栈大小

在任何情况下, 您仍可能希望更改由 IIS 工作进程强加的 DLL 应用程序的堆栈大小。 幸运的是, uniGUI 提供了实现此目的的简单方法。

在 ServerModule 展开ISAPIOptions并设置AsyncMode为真。 现在将ThreadStackSize设置为所需值。 在下面的示例中, 堆栈大小设置为 2 MB。

请注意, 这些设置仅适用于 ISAPI DLL 模块。 对于独立和服务应用程序, 堆栈大小应使用常规方法进行调整, 如前所述。


第三方组件库

很少有应用程序可能不依赖第三方组件库。 第三方库有助于创建具有扩展功能的功能丰富的应用程序。 在某些情况下, 使用第三方库是必需的。 例如, 数据库引擎可能需要一组特定的组件来连接和进行查询。 报告是第三方工具的另一个领域。 当涉及到 uniGUI 应用程序时, 第三方工具兼容性成为一个重要问题。 如前几节所述, web 应用程序的稳定性取决于应用程序的所有元素。 错误的、不发达的或不兼容的第三方库可能导致服务器不稳定。

在为 uniGUI 应用程序选择第三方库时, 需要考虑一些要点:

兼容性:正如本文档中的许多地方所描述的, uniGUI 服务器是一个严重的多线程环境。 这意味着与 uniGUI 一起使用的任何工具都必须是线程安全的, 即组件的几个实例必须能够并行地在多个线程中运行。 如果组件依赖于全局变量, 则在 web 应用程序中很可能会失败。 同样, 如果组件依赖于嵌入式 VCL 控件, 则它可能会以有害的方式影响服务器的稳定性。 VCL 控件不是线程安全的, 因为它在 VCL 文档中显式说明。 不幸的是, 某些报告工具设计为依赖 VCL 控件生成报表输出。

某些第三方工具依赖于窗口句柄 (HWND) 和消息循环来运行。 这是另一点, 可能导致严重的问题, 在 uniGUI。 uniGUI 没有主线程, 也没有实现消息循环。 虽然独立服务器从德尔菲的 VCL 借用了一个主线程, 但一个主线程在 ISAPI DLL 中不可用。 因此, 依赖于发送消息的任何第三方组件很可能在 ISAPI 模块中失败。 您可能会发现某些应用程序在独立模式下工作正常, 在部署为 ISAPI 模块时失败或工作方式不同。 它的发生是因为在独立模式下, uniGUI 服务器实现了一个最小化到托盘的 VCL 形式。 因此, 调用应用程序. Run () 是为了使服务器保持运行, 直到用户终止它为止。 该窗体将创建一个消息循环, 允许某些不兼容的组件工作到某个级别。 但是, 在 ISAPI 模块中, 没有创建 VCL 表单, 也没有对应用程序的调用. 运行 (), 以便调用 SendMessage () 的任何尝试都无法返回结果, 因为没有消息循环处理消息。

为了给出一个具体的例子, TRichEdit 是一个 VCL 控件, 它是带有样式文本的备注。 一些报告工具使用它来生成各种样式的文本块。 与其他 VCL 控件一样, 它内部使用 SendMessage () 与它的句柄 (HWND) 进行通信。 如上所述, 该请求将失败, RichEdit 将无法呈现。 您可能会发现, 当应用程序以独立模式部署时, 包含 RichEdit 的报表将正确呈现, 但当同一应用程序作为 Windows 服务或 ISAPI 模块运行时, 不会呈现任何内容。

作为最后一个示例, 第三方组件不应使用全局变量。 它们必须遵循适用于 uniGUI 应用程序的相同规则。 组件的每个实例都必须有自己的私有变量集来使用, 并且不应依赖全局变量或数据模块, 而不必保护对它们的访问。

某些第三方组件需要正确配置以在线程环境中运行。 您必须检查其手册的特定部分的说明。

稳定性:任何具有稳定性问题的第三方组件都将影响 uniGUI 应用程序的稳定性。 组件库必须没有 bug, 并且测试内存泄漏。供应商必须确保它可以在多线程环境中使用, 而不会有任何问题。

可伸缩性:在 uniGUI 中使用的第三方组件必须是可伸缩的。 除非受到系统资源的限制, 否则可能共存的组件的同时实例数不应有上限。 如果有这样的限制, 开发人员必须了解它们并相应地设计应用程序。


报告工具

对于报告工具, 有一些特别的指导原则, 以确保它们以兼容的模式与 uniGUI 运行。 某些报告工具具有多线程应用程序的特殊设置。 在许多情况下, 您需要在报表引擎运行时禁用可视对话框和其他与 VCL 相关的窗体。 由于在 uniGUI 中无法显示 VCL 窗体, 因此需要将报表导出到文件。 通常使用的格式是 PDF, 可以嵌入到浏览器窗口或iframe中。

在 uniGUI 中, 建议创建报表的方法是将其转换为 PDF 文件, 并将其显示在TUniUrlFrame控件中。 对于每个报告工具, 都有不同的路径来实现这一点。 在本主题下, 我们将很快报道一些报告工具。 您还可以加载和运行相关的演示, 以查看它是如何实际完成的。

重要:如前几节所述, 报告工具中有一些常见问题可能会影响 uniGUI 应用程序的稳定性和可伸缩性。 仅为桌面应用程序创建了报告工具的初始版本。 随着时间的推移, 他们进化并开始支持服务器应用程序;但是, 它们仍可能包含与多线程服务器应用程序不兼容的组件或代码部分。 某些报告工具使用 VCL 控件生成报告, 在将其导出到 PDF 文件之前将其呈现给画布。

请注意, 一些报告工具使用 TRichEdit控件来呈现他们的报告, 这些问题都是我们之前描述的。 此类报告工具不能在 uniGUI 应用程序内使用, 但可以由某些外部 web 服务使用。 "打印服务器" web 服务可能会生成用于导出 PDF 报告的外部进程, 但此解决方案需要一些中央数据库来存储报告所需的信息 (在这种情况下, uniGUI 应用程序可以请求一些特定报告并接收 PDF 文件作为结果)。

要完全测试报表工具与应用程序的兼容性, 应运行压力测试并全面分析应用程序行为。 重要的是要确保您的特定报告工具和特定的报表设计可以在服务器负载时扩展。

这里的另一个重要问题是确保在同一事件中创建报告组件和生成报告。 某些报表组件内部使用需要创建窗口句柄的 VCL 控件。 必须在同一线程中创建和释放这些句柄, 因此需要在单个事件调用中创建和销毁报表组件。 为了实现这一点, 最好的方法是将报告组件放在一个免费的 DataModule上。 当需要生成报告时, 将创建自由数据模块。 创建报表后, 应释放数据模块。

下面的代码是从我们的 FastReport 演示:

procedure TUniForm1.UniFormBeforeShow(Sender: TObject);
var
dm : TfrDM;
RepUrl : string;
begin
dm := TfrDM.Create(nil);
try
RepUrl := dm.GenReportPDF(InvNum);
finally
dm.Free;
end;

UniURLFrame1.URL := RepUrl; // display generated PDF inside a frame
end;

在上面的代码数据模块是根据需要创建的, 生成报告链接, 然后销毁数据模块。

下面是用于生成报告的实际可用数据模块:

A Free DataModule for report generation

报告生成的免费 DataModule


FastReport

FastReport是德尔斐广泛使用的报告工具。 要在 uniGUI 应用程序中使用 FastReport, 有一定的规则可循。 FastReport 引擎必须进行调整, 以便能够处理 uniGUI 多线程环境。

首先, 必须调整报表引擎参数。 在下一个示例报告中是TfrxReport对象。

Report.EngineOptions.SilentMode := True;
Report.EngineOptions.EnableThreadSafe := True;
Report.EngineOptions.DestroyForms := False;
Report.EngineOptions.UseGlobalDataSetList := False;

下一步是调整报表设置:

Report.PrintOptions.ShowDialog := False;
Report.ShowProgress := False;
Report.PreviewOptions.AllowEdit := False;

为了导出文件, 您需要使用导出组件。 对于 PDF 文件, 您需要TfrxPDFExport组件。 以下是设置:

Exp.Background := True;
Exp.ShowProgress := False;
Exp.ShowDialog := False;

/ Assign a unique name to exported file name
Exp.FileName := UniServerModule.NewCacheFileUrl(False, 'pdf', '', '', AUrl);
Exp.DefaultPath := '';

完成所有设置后, 最后一步是生成报告:

Report.PrepareReport;
Report.Export(Exp);

最后, 我们需要在 uniGUI 中显示报告。 有两个选项: 使用 TUniURLFrame 或 TUniPDFFrame。 该示例 PDFViewer 使用它们。

UniURLFrame1. URL: = AUrl;

PDF Viewer using TUniURLFrame

使用 TUniURLFrame 的 PDF 查看器

UniPDFFrame1. PdfURL: = Url;

PDF Viewer using TUniPDFFrame

使用 TUniPDFFrame 的 PDF 查看器

另一种方法是通过使用UniSession SendFile方法下载报告给用户。

UniSession.SendFile(Exp.FileName);


关于 FastReport 和螺纹安全的重要说明

FastReport 是一个流行的报告工具, 为德尔菲和 C 建设者。 在上一节中, 我们描述了在 uniGUI 应用程序中使用 FastReport 组件的一般准则。 最近的一些发现表明, 必须特别注意确保 FastReport 与 uniGUI 及其多线程环境在兼容模式下运行。

避免使用 FastReport 全局数据集列表

全局数据集是 FastReport 在 VCL 应用程序中使用的集合。 此集合应在 uniGUI 中避免, 否则其使用可能会导致严重问题, 如堆栈溢出和即时服务器崩溃。 在上一节中, 我们提到必须应用以下设置以避免全局数据集的使用:

Report.EngineOptions.UseGlobalDataSetList := False; // Do not keep a global list of a datasets

开发人员必须确保在创建 TfrxReport 组件后正确应用此设置。 如果您的 TfrxReport 组件位于窗体或 DataModule 上, uniGUI 将提供一个方便的方法来执行此项。 转到 MainModule, 单击OnNewComponent事件并添加以下代码:

procedure TUniMainModule.UniGUIMainModuleNewComponent(AComponent: TComponent);
begin
if AComponent is TfrxReport then
begin
(AComponent as TfrxReport).EngineOptions.UseGlobalDataSetList := False;
end;
end;

前面的代码将确保TfrxReport组件的每个新实例都具有其UseGlobalDataSetList属性的正确设置.

如果在代码中创建TfrxReport组件, 请手动修改该设置:

//始终创建具有所有者的 TfrxReport 组件

f := TfrxReport.Create(Self);
try
f.EngineOptions.UseGlobalDataSetList := False;
f.EngineOptions.SilentMode := True;
f.EngineOptions.EnableThreadSafe := True;
f.EngineOptions.DestroyForms := False;
f.LoadFromFile(UniServerModule.FilesFolderPath + RepName + '.fr3');

// generate report here

finally
f.Free;
end;

在上面的示例中, TfrxReport组件是使用所有者自身创建的。 这是非常重要的, 以确保你已经通过了一个合适的所有者。 您应该避免创建TfrxReport与零所有者。

使用动态组件生成报表

要保存系统资源, 生成报告的最佳方法是使用动态创建的组件。 请查看以下演示, 以查看执行此操作的最佳方法:

..\uniGUI\Demos\PublicDemos\Desktop\FastReport - Dynamic

..\uniGUI\Demos\PublicDemos\Desktop\FastReport - MultiReport

在这些演示中, 我们使用与 TfrxReport 和其他必需的报告组件的免费 DataModule:

当我们想要生成一个新的报告时, 我们只需创建自由 DataModule, 生成报告, 然后销毁 DataModule:

procedure TUniForm1.UniFormBeforeShow(Sender: TObject);
var
dm : TfrDM;
begin
dm := TfrDM.Create(nil);
try
UniURLFrame1.URL := dm.GenReportPDF(RepName, Id);
finally
dm.Free; // free reporting components as soon as report is generated
end;
end;

上述代码将确保在生成报表后立即释放专用于报告工具的系统资源。 在用户数量高且需要精确的资源管理的系统中, 这可能很重要。

当报表组件使用 Windows 句柄时, 也需要这样做。 在这种情况下, 必须在同一线程中创建和销毁所有报表组件。 使用上面演示的动态数据模块, 可以确保在同一线程的上下文中完成报表组件的创建、报告生成和销毁。


ReportBuilder

ReportBuilder是德尔福的另一个广泛使用的报告工具。 我们还生成报告的 PDF 输出, 并将其定向到TUniUrlFrame。

lPDFDevice := TppPDFDevice.Create(nil);
try
lPDFDevice.PDFSettings := ppReport1.PDFSettings;
lPDFDevice.FileName :=

UniServerModule.NewCacheFileUrl(False, 'pdf', '', '', AUrl);
lPDFDevice.Publisher := ppReport1.Publisher;
// generate the report
ppReport1.PrintToDevices;
UniURLFrame1.URL := AUrl;
finally
lPDFDevice.Free;
end;

为了避免与基于 VCL 的报表组件和 uniGUI 多线程引擎发生冲突, 您必须采用动态创建报表。

为此, 您需要从 uniGUI 向导创建一个免费 DataModule, 并将所有组件和报表生成代码放在此模块中:

免费 DataModule

报告的生成将简单地通过创建数据模块来完成, 如下所示:

procedure TUniForm1.UniFormBeforeShow(Sender: TObject);
var
dm : TrepDataModule;
begin
dm := TrepDataModule.Create(nil);
try
UniURLFrame1.URL := dm.GenRep(InvNum)
finally
dm.Free;
end;
end;

完整的演示可以在..\demos\Report Builder folder.


使用报表服务器

在某些情况下, 稳定性和可伸缩性问题具有最高的优先级, 因此使用单独的报表服务器是明智的。 在许多情况下, 您的报告工具已经提供了一个运行在自己的服务中的服务器解决方案。 您的 uniGUI 应用程序可以通过向报表服务器发送适当的请求来生成报告。 生成的报表可以按正常方式显示在 TUniUrlFrame 或 TUniPDFFrame 中。


数据库组件

使用共享数据库的3层多用户应用程序需要处理并发。 它同时发生在桌面和 web 应用程序上。 在许多方面, 这也发生在客户端/服务器桌面应用程序从多个线程访问数据库。

有些数据库接受单个连接, 而另一些则具有不同的限制。 如前所述, 连接池是一种有用的功能, 通常可用于高端数据库引擎 (如 Microsoft SQL server 和 Oracle), 使用正确的数据访问技术 (如 ADO 和 FireDAC)。

在本节中, 我们将介绍如何为选定的数据库引擎配置多个数据库组件。


FireDAC

以前称为 AnyDAC, FireDAC 是一个广泛使用的一套数据库组件的德尔菲。 使用 FireDAC 时有一些线程安全方面的注意事项。 uniGUI 是一个完全多线程的应用程序, 因此每个会话中使用的组件必须与其他会话隔离。 如 "数据库连接" 部分所述, 每个会话都应该有自己的连接组件副本。 同样的原则也适用于 FireDAC。 在这个恩巴克德罗文档中, 有关于在多线程应用程序中使用 FireDAC 的准则。

在 uniGUI 中, 每个 TFDConnection 组件必须放在 MainModule 或从 uniGUI 向导创建的 DataModule中。 还可以创建和销毁公共 datamodule 作为任何 TUniForm 的构造函数和析构器的一部分 (因为它们也属于其会话的私有)。 这将确保每个会话都有自己的私有 TFDConnection 组件。

此外, 我们需要在 ServerModule 上放置一个 TFDManager 组件, 并在 ServerModule 的 OnCreate 事件中将其活动属性设置为 True。

在 ServerModule 上放置的其他组件是TFDGUIxWaitCursor和TFDPhysXXXDriverLink。 TFDPhysXXXDriverLink是数据库引擎的驱动程序链接组件。 对于 MySQL, 它应该是TFDPhysMySQLDriverLink的。 用于TFDGUIxWaitCursor设置提供程序以控制台和ScreenCursor到gcrNone。

在 OnCreate 事件 FDManager1 中,活动应设置为True。

procedure TUniServerModule.UniGUIServerModuleCreate(Sender: TObject);
begin
.
.
.
.
FDManager1.Active := True;
end;

以类似的方式, 在 OnDestroy 事件FDManager1.。

procedure TUniServerModule.UniGUIServerModuleDestroy(Sender: TObject);
begin
.
.
.
FDManager1.Close;
end;

池连接

FireDAC 中的高级功能之一是对池连接的支持。 它允许在会话之间共享相同的数据库连接。

有关详细信息, 请参阅:

http://docwiki 恩巴克德罗/RADStudio/XE8/en/Multithreading_ (FireDAC)

http://docwiki 恩巴克德罗/RADStudio/XE8/en/Defining_Connection_ (FireDAC)

还可以查看我们的示例项目:

DBLookupComboBox 自定义远程查询


使用运行时包

德尔菲的一个有用特性是它支持 "运行时包"。通过使用运行时包, 德尔菲应用程序可以传播几个模块。 这些模块是 BPL 运行时程序包使用的德尔菲 IDE 本身。 这些包是根据需要动态加载到应用程序内存空间中的特殊 dll。 主应用程序的大小将更小, 因为任何外部 BPL 库仅在需要时才会出现内存。 此功能还允许创建模块化应用程序。 您可以将应用程序模块和窗体划分为较小的库, 在需要时可以在代码中加载它们。

uniGUI 支持使用运行时包, 这可以通过在项目选项中启用运行时包的链接来轻松完成。 设置此选项后, 必须确保所有uniGUI 运行时包都在运行时包列表中。

下面是 uniGUI 应用程序中使用的所有运行时包的列表。 XX 代表与您的德尔斐版本对应的数字。 例如, uniGUI25 为德尔菲10.2 东京。

uniToolsXX
uIndyXX
uniGUIXXCore
uniGUIXX
uniGUIXXChart
uniGUIXXmCore
uniGUIXXm

重要的是要记住, 您必须确保所有上述软件包将与您的应用程序动态链接。 如果在运行时包列表中只包含这些库的子集, 则可能会导致某些 uniGUI 库将被静态链接, 而另一些则将动态加载。 这必须避免。 您需要静态或动态地链接所有 uniGUI 库。

在此设置之后, 您将注意到项目可执行文件的大小要小得多。 对于在发布模式下的空 uniGUI 项目, 可执行文件的大小将大约为43KB。 当项目在没有运行时包的情况下正常编译时, 将其与几个兆字节进行比较。


运行时包安培;C 生成器

Runtime Packages & C++ Builder

与德尔菲不同, C 生成器的默认生成模式是将应用程序与运行时包链接起来。 创建新项目时, 它将与运行时包动态链接。 您可以从 IDE 项目选项中更改此设置。 如果要使用运行时包生成项目, 则必须确保所有 uniGUI 运行时包都与应用程序动态链接。

下面是 uniGUI 应用程序中使用的所有运行时包的列表。 XX 代表与您的德尔斐版本对应的数字。 例如, uniGUI25 为德尔福/C 建设者10.2 东京。

uniToolsXX
uIndyXX
uniGUIXXCore
uniGUIXX
uniGUIXXChart
uniGUIXXmCore
uniGUIXXm

重要的是要记住, 您必须确保所有上述软件包将与您的应用程序动态链接。 如果在运行时包列表中只包含上述库的一个子集, 则会导致某些 uniGUI 库将被静态链接, 而另一些则将动态加载。 这必须避免。 您需要静态或动态地链接所有 uniGUI 库。


特殊考虑事项

本节列出了在编写 web 应用程序时需要考虑的一些事项。


FormatSettings

FormatSettings 是德尔斐的全球记录, 用于保持区域设置相关变量, 如 DecimalSeparator、CurrencyFormat、CurrencyString 等。这个变量引入了德尔菲。 在德尔福, DecimalSeparator, CurrencyFormat 等被宣布为单独的全局变量在 SysUtils. pas。

FormatSettings 的问题在于它是一个全局变量, 它不适应 uniGUI 的多线程环境。 要解决此问题, 开发人员必须确保仅在会话启动之前修改 FormatSettings。 在会话中修改 FormatSettings 是不安全的。 为了克服这个问题, uniGUI 实现了两个函数, 它以安全的方式访问 FormatSettings。

function FmtSettings: TFormatSettings;
function PFmtSettings: PFormatSettings;

函数 FmtSettings 返回一个只读 TFormatSettings 记录, 可用于各种 RTL 函数, 如:

FloatToStr(FValue、 FmtSettings);

函数 PFmtSettings 返回指向 TFormatSettings 记录的指针, 它允许进行如下修改:

PFmtSettings.CurrencyString:= '£';

当在会话上下文中调用时, 上述函数将返回属于该会话的 TFormatSettings 的私有副本。 TFormatSettings 的这个私有实例将在内部用于所有使用 TFormatSettings 参数的 uniGUI 函数。 但是, 并非所有内部 DB 函数都使用此专用副本。 某些 DB 函数 (如 TField AsString) 使用全局 FormatSettings, 因此, 如果要对每个会话使用不同的 FormatSettings, 则必须考虑到这一点。 使用德尔菲全球 FormatSettings 的第三方数据库工具也是如此。

另一方面, 如果在 ServerModule 的 OnCreate 事件中调用它们, 则会设置应用程序中所有会话使用的全局 FormatSettings。

下面的代码将更改应用程序中所有会话的全局 FormatSettings:

procedure TUniServerModule.UniGUIServerModuleCreate(Sender: TObject);
begin
// Change application global format settings
PFmtSettings.CurrencyFormat := 0;
PFmtSettings.CurrencyString := '�';
PFmtSettings.DateSeparator := '/';
PFmtSettings.ShortDateFormat := 'dd/mm/yyyy';
end;

当在会话上下文中调用上述函数时, 返回的记录不是在 SysUtils 中定义的 FormatSettings 记录的全局实例。 相反, 它将是在 TUniGUIApplication 类中保存的 TFormatSettings 记录的私有实例。 这意味着, 在会话中, 您应该显式地将 FmtSetting 用作 RTL 函数的参数, 使用它:

外语:=StrToFloat(' 10.0 ', FmtSettings);

下面的代码只会更改当前会话的专用 FormatSettings:

procedure TUniMainModule.UniGUIMainModuleCreate(Sender: TObject);
begin
// Change private format settings for current session
PFmtSettings.DateSeparator := '/';
PFmtSettings.CurrencyFormat := 0;
PFmtSettings.CurrencyString := '$';
PFmtSettings.ShortDateFormat := 'dd/mm/yyyy';
end;


动态创建的控件

uniGUI 完全支持在运行时创建控件。 此高级功能使开发人员能够动态创建 UI, 这多亏了 Ext JS 框架动态创建、插入和删除控件的能力。 在 uniGUI 和 VCL 中创建动态控件之间没有区别。 您可以通过将控件分配给所有者和父级来简单地创建它。 但是, 在 uniGUI 中, 应始终使用所有者创建所有控件。 此外, 在许多情况下, 应始终将父级分配给控件。


在运行时创建控件

在 VCL 中, 与 uniGUI 相比, 动态创建控件的规则更加轻松。 例如, 在 uniGUI 中, 无法在没有所有者的情况下创建窗体和控件。 在 uniGUI 中, 窗体必须在创建时显示, 而在 VCL 中则不存在这样的限制。

这里的另一个重要说明是关于动态创建的控件的生存期管理。 当一个控件动态创建时, 正确的做法是让他们被所有者或父母摧毁。 建议不要在代码中动态地销毁控件。 当所有者本身被释放时, 每个拥有所有者的控件将被其所有者 (或父级) 销毁, 因此在正常情况下, 不需要编写附加代码来销毁动态创建的控件。 最佳做法是允许用户操作决定何时必须释放控件。 例如, 如果您的窗体包含多个动态创建的控件, 则当用户关闭所有者窗体时, 这些控件将被释放。

让我们总结一下在动态创建控件时必须遵循的一些规则:

1.uniGUI 窗体、控件和组件必须与所有者一起创建。

2.对于表单, 所有者应始终是UniApplication对象。

3.对于控件和组件, 所有者必须是TUniForm或TUniFrame子代。

4.表格必须在创建后显示。 无法立即创建表单, 以后再显示。

5.如果可能, 必须为动态创建的对象分配一个唯一名称。

6.在创建控件后, 必须为其分配父级。

7.指派的父母必须是TUniContainer、 TUniForm或TUniFrame的后裔。

8.正确的做法是不销毁代码中的控件。 当控件的所有者 (或父) 被销毁时, 让框架销毁它们。

在下面的代码中, 窗体是动态创建的。 正如您所看到的, 所有者是UniApplication对象, 并且窗体在创建后就会显示出来。

procedure TMainForm.UniButton1Click(Sender: TObject);
begin
TUniForm1.Create(UniApplication).Show;
end;

在显示该语法之前, 可以将其扩展为修改表单属性。

procedure TMainForm.UniBitBtn1Click(Sender: TObject);
begin
with TUniForm1.Create(UniApplication) do
begin
Color := clGray;
BorderStyle := bsNone;
ShowModal;
end;
end;

在这两个示例中, 我们不保留创建窗体的实例, 因为在许多情况下, 它是不必要的。 应将窗体视为使用和丢弃的独立对象。 当然, 您可以保留对表单实例的引用, 但必须注意, uniGUI 窗体在用户关闭时会动态销毁, 这样, 在窗体关闭后, 任何持有该窗体实例的变量都将处于无效状态。

要动态创建控件, 可以使用窗体的OnCreate或OnBeforeShow事件。 实际上, 可以使用任何其他事件, 但如果要创建许多动态控件, 则在窗体变为可见之前执行此操作是明智的。

procedure TUniForm1.UniFormCreate(Sender: TObject);
begin
with TUniButton.Create(Self) do
begin
Caption := 'Button1';
Top := 100;
Left := 10;
Parent := Self;
OnClick := UniButton1Click;
end;

with TUniLabel.Create(Self) do
begin
Caption := 'Button1';
Top := 140;
Left := 10;
Parent := Self;
end;
end;


在运行时销毁控件

在 uniGUI 中, 通过简单调用它的自由 ()方法, 可以在运行时销毁控件。 此操作将从其父项中移除该控件, 将销毁它及其所有关联的资源。 虽然可以通过调用自由 ()来销毁 uniGUI 控件, 但不建议这样做, 除非有充分的理由这样做。 在必须释放控件时, 允许 uniGUI 管理总是更好。 通常, 控件在其所有者窗体关闭时被销毁。 如果所有者是 TUniFrame, 则当所有者窗体关闭时, 其子控件将被销毁。 在某些情况下, 控件或 TUniFrame 可能驻留在页控件的选项卡页 (TUniTabSheet) 中。 同样, 如果选项卡页是 closeable 的, 当用户关闭它时, 它将被释放。 因此, 它的所有子控件也将被释放。

可以在运行时动态创建控件, 但在销毁它们时必须使任务 uniGUI。 因此, 作为一般规则, 所有控件必须由主要关闭窗体、关闭选项卡页、关闭会话等的用户操作释放。

为什么我们不喜欢通过调用销毁 ()或自由 ()方法来销毁组件？ 这个问题的答案是网络的异步性质。 请考虑以下情况:

屏幕上有数据填充的 UniDBGrid, 您希望通过调用它的自由 ()方法来销毁它。 让我们把这个方法在一个按钮 OnClick 方法。

procedure TMainForm.UniButton1Click(Sender: TObject);
begin
UniDBGrid1.Free;
end;

上述代码将销毁 DBGrid, 并将其从所有者窗体中按预期方式删除。 这里的问题是, 用户按下 UniButton1 的时间和从表单中释放网格的时间之间可能会有延迟。 服务器和客户端之间的通信可能需要一段时间, 而此延迟可以在 LAN 上察觉, 但在 internet 上, 根据连接质量的不同, 可能需要更长的距离。 在这个很小的时间间隔内, 用户仍然可以访问 DBGrid, 并可继续在其中导航。 可能, 它将生成将返回到服务器的事件。 但是, 此时服务器上不存在网格, 无法处理这些事件。服务器将引发异常。

通过启用 UniButton1 的屏幕掩码和阻止用户交互, 直到 DBGrid 被释放并完全从 UI 中删除时, 才能避免此问题。 此修补程序将解决此问题, 但会增加应用程序设计的复杂性, 并要求每次出现此类情况时都启用屏幕掩码。 开发人员可以轻松地忘记启用屏幕掩码, 因为他/她在本地网络上看不到问题, 因为此问题只能在 internet 上的低速连接中出现。

简而言之, 虽然在运行时销毁 uniGUI 可视化控件很有可能, 但不建议使用上面解释的原因。 最佳做法是允许框架使用常见用户操作 (如关闭窗体、关闭制表位、结束会话等) 来释放控件。


在运行时创建或销毁多个控件

如果应用程序需要动态创建 (或销毁) 大量控件, 则在开始创建它们之前调用SuspenLayouts ()方法是很好的。

每个SuspendLayouts()调用必须与ResumeLayouts()方法配对, 最好在尝试中进行匹配。终于阻止了。

SuspendLayouts;
try
for I := 1 to 20 do
begin
B := TUniDBGrid.Create(Self); // create 20 grids dynamically
B.DataSource := DataSource1;
B.Parent := UniContainerPanel1;
.
.
.

end;
finally
ResumeLayouts;
end;

当您将新控件插入到已创建和呈现的窗体或框架中时, 调用SuspendLayouts()特别有用。


最佳实践

在查看标准 VCL 桌面应用程序和 web 应用程序 (SPA) 之间的一些差异之后, 我们准备就如何使用 uniGUI (以及应避免什么) 提供一些具体的指导。


处理并发

即使新的应用程序是来自当前 VCL 应用程序 (已经是多用户客户端/服务器) 的迁移的结果, uniGUI 应用程序也将为同一进程中的多个用户提供服务。

多用户客户端/服务器 VCL 应用程序通常在多台计算机上运行客户端应用程序的多个实例, 其中每一个都连接到共享数据库服务器。 几乎所有数据库服务器都通过在需要时实现行/表锁定来支持多个连接。

这种 VCL 应用程序是典型的2层应用程序。 类似的 uniGUI 应用程序是一个3层应用程序, 因为用户界面 (表示层) 在客户端浏览器中运行, 大多数以前的应用程序代码在服务器上运行 (现在服务于多个用户/会话), 并且共享相同的数据库服务器。

uniGUI 应用程序的新非可视代码服务于多个用户, 这一事实需要处理对任何共享资源的并发访问。 这种并发访问将以多线程方式出现, 其中每个请求都来自不同的线程。

ServerModule

对全局和可能只读/只信息使用ServerModule单例。

大多数 web 应用程序都需要访问数据库服务器。 有时它会使用像 TFDConnection 这样的数据访问控制, 但也可以使用一些中间件。 在任何情况下, 有关如何连接到数据库服务器的信息都应可用于所有用户会话。 如果使用 TFDConnection 或类似的方法, 程序将需要生成正确的连接字符串或实例化相应的组件。

如前所述, 如果使用 FireDAC, 则应添加 TFDManager、所选数据库的驱动程序等。如果使用 TADOConnection, 就足以拥有连接字符串。

创建 ServerModule 后, 它应该只向用户会话公开只读属性。

警告: 由于 ServerModule 是单一的, 在服务器启动时创建, 引入读/写变量将阻止使用任何负载平衡技术。 uniGUI 计划释放其负载平衡服务器, 它不假定用户和服务器之间, 只是用户和会话之间的亲和力;换言之, 如果需要重新启动会话, 它应该能够在任何可用的服务器中运行。 任何具有在 ServerModule 上共享的权限的读/写变量都应移动到共享数据库 (它应自行处理并发访问)。

尽管前面有警告, 但在这里公开读/写属性有一些非常重要的原因, 但必须确保每次只有一个会话可以修改它。 这类保护的明显解决方案是使用临界区。

单位ServerModule;

界面

使用
班级, SysUtils, uniGUIServer, uniGUIMainModule, uniGUIApplication,

uIdCustomHTTPServer, uniGUITypes, SyncObjs;

type
TUniServerModule = class(TUniGUIServerModule)
procedure UniGUIServerModuleCreate(Sender: TObject);
procedure UniGUIServerModuleDestroy(Sender: TObject);
private
FConnectionString : string;
FSharedVariable : string;
FLockSharedVariable : TCriticalSection;

function GetSharedVariable : string;
procedure SetSharedVariable(const Value : string);
protected
procedure FirstInit; override;
public

property ConnectionString : string read FConnectionString;
property SharedVariable : string read GetSharedVariable

write SetSharedVariable;
end;

function UniServerModule: TUniServerModule;

implementation

{$R *.dfm}

uses
UniGUIVars;

function UniServerModule: TUniServerModule;
begin
Result:=TUniServerModule(UniGUIServerInstance);
end;

procedure TUniServerModule.FirstInit;
begin
InitServerModule(Self);
end;

procedure TUniServerModule.UniGUIServerModuleCreate(Sender: TObject);
begin
FConnectionString := 'Some Connection String';
FSharedVariable := 'Shared';
FLockSharedVariable := TCriticalSection.Create;
end;

procedure TUniServerModule.UniGUIServerModuleDestroy(Sender: TObject);
begin
FLockSharedVariable.Free;
end;

function TUniServerModule.GetSharedVariable : string;
begin
FLockSharedVariable.Acquire;
try
Result := FSharedVariable;
finally
FLockSharedVariable.Release;
end;
end;

procedure TUniServerModule.SetSharedVariable(const Value : string);
begin
FLockSharedVariable.Acquire;
try
FSharedVariable := Value;
finally
FLockSharedVariable.Release;
end;
end;

initialization
RegisterServerModuleClass(TUniServerModule);
end.

在这种情况下, 在创建 ServerModule 时分配了连接字符串, 并且它是只读/唯一属性。 但是, SharedVariable 是一个读/写属性, 我们必须确保只有一个用户可以写入它。 我们正在使用关键节强制执行对变量的单一访问。 更重要的是, 我们将此事实隐藏在 SetSharedVariable 方法中, 以便在访问属性时不需要知道、获取或释放 TCriticalSection 对象。 当然, 通过允许修改该属性, 我们也需要控制读数。 任何读/写属性都会减慢应用程序的速度 (这取决于它访问变量的频率)。

需要注意的是, 虽然可以在上面定义的 ServerModule 中使用可写共享变量, 但建议不要将其作为应用程序范围内的全局变量。 将来每个 uniGUI 应用程序都可以分为多个较小的过程来实现负载平衡和其他提前特性。 在这种情况下, ServerModule 中的任何共享全局变量都将只对其所有者进程可见, 而不是其他进程将在进程池中服务器相同的应用程序。 为此, 建议将此类全局变量保留在数据库表中, 而不是保存在内存中。

MainModule

Use the MainModule for user session variables.

使用 RAD 工作室为德尔菲开发为单用户 VCL 应用程序生成正确的代码, 但其中的一些代码将在多用户或多线程环境中创建问题。 让我们举几个例子, 说明它为什么会失败以及如何修复它。

在要求创建一个新的 VCL 表单后, 我们得到以下代码:

unit Unit1;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,

System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs;

type
TForm1 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

end.

请注意全局变量Form1。 你可能会被诱惑使用它从其他单位这样:

unit Unit2;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,

System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,

Vcl.StdCtrls;

type
TForm2 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form2: TForm2;

implementation

uses
Unit1;

{$R *.dfm}

procedure TForm2.Button1Click(Sender: TObject);
begin
Form1 := TForm1.Create(Self);
try
Form1.ShowModal;
finally
Form1.Free;
end;
end;

end.

任何用户会话都可以访问 Form1, 并且几个用户可能正在使用它。 想象一下, 如果两个用户创建了相同的变量 (在释放幸存的窗体时导致一个窗体的内存泄漏和无效引用), 会发生什么情况。

简单的修复将避免这种情况:

procedure TForm2.Button1Click(Sender: TObject);
begin
with TForm1.Create(Self) do
try
ShowModal;
finally
Free;
end;
end;

正如您所看到的, 在 uniGUI 中, 我们甚至不需要在变量中保留窗体的实例。 换句话说, 我们不需要也不应该有全局变量。 删除所有自动生成的全局变量并创建您自己的私有实例。 这是 VCL 应用程序的好习惯, 它需要修复窗体和数据模块。

unit Unit3;

interface

uses
System.SysUtils, System.Classes;

type
TDataModule1 = class(TDataModule)
private
{ Private declarations }
public
{ Public declarations }
end;

这是一个全局变量, 不能在 uniGUI 中使用。
var
DataModule1:TDataModule1;

implementation

{$R *.dfm}

end.

uniGUI 不仅仅是一组用于渲染表示层的组件;它还是支持 web 应用程序开发的最佳做法的框架。 在支持的最佳做法中, 缺少自动生成的全局变量和附加到用户会话的窗体和数据模块的自动内存管理。

uniGUI 创建了基于以前注册类型返回会话实例的函数, 而不是德尔菲为窗体和数据模块创建的全局变量。 让我们来看看 MainModule 作为一个典型的例子。

unit MainModule;

interface

uses
uniGUIMainModule, SysUtils, Classes;

type
TUniMainModule = class(TUniGUIMainModule)
private
{ Private declarations }
public
{ Public declarations }
end;

function UniMainModule: TUniMainModule;

implementation

{$R *.dfm}

uses
UniGUIVars, ServerModule, uniGUIApplication;

function UniMainModule: TUniMainModule;
begin

// returns the correct instance of MainModule for current session
Result := TUniMainModule(UniApplication.UniMainModule)
end;

initialization
RegisterMainModuleClass(TUniMainModule);
end.

请注意初始化期间对RegisterMainModuleClass的调用。 当需要访问会话的 MainModule 时, 使用函数UniMainModule就足够了 。 另一个会话函数UniApplication将返回与当前会话对应的已注册 MainModule 的实例。

同样的解决方案也适用于会话窗体和模块。 如果我们使用 uniGUI 向导创建一个新表单, 并选择 "应用程序表单", 我们将得到以下内容:

unit Unit1;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm;

type
TUniForm1 = class(TUniForm)
private
{ Private declarations }
public
{ Public declarations }
end;

function UniForm1: TUniForm1;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function UniForm1: TUniForm1;
begin

// returns the correct instance of UniForm1 for current session
Result := TUniForm1(UniMainModule.GetFormInstance(TUniForm1));
end;

end.

但 uniGUI 也管理表单的生存期。 让我们看看如何使用该窗体:

unit Main;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIRegClasses, uniGUIForm, uniGUIBaseClasses, uniButton;

type
TMainForm = class(TUniForm)
btnShow: TUniButton;
btnShowModal: TUniButton;
procedure btnShowClick(Sender: TObject);
procedure btnShowModalClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function MainForm: TMainForm;

implementation

{$R *.dfm}

uses
uniGUIVars, MainModule, uniGUIApplication,
uniGUIDialogs,
Unit1;

function MainForm: TMainForm;
begin

// returns the correct instance of MainForm for current session
Result := TMainForm(UniMainModule.GetFormInstance(TMainForm));
end;

procedure TMainForm.btnShowClick(Sender: TObject);
begin
UniForm1.Show;
end;

procedure TMainForm.btnShowModalClick(Sender: TObject);
begin
UniForm1.ShowModal; // Synchronous mode
ShowMessage('Result = ' + IntToStr(UniForm1.ModalResult));
end;

initialization
RegisterAppFormClass(TMainForm);

end.

在前面的 VCL 代码中, 我们需要创建表单的临时实例, 使用 ShowModal 打开它, 然后释放它 (我们使用了 try... 最后, 以避免内存泄漏)。

此代码只需要调用: UniForm1. ShowModal.为什么？

UniForm1 创建一个临时表单, 在达到其生存期结束时自动释放, 但在该时刻可用。

显示非模态表单怎么样？ uniGUI 窗体在关闭时自动释放。

如果只需要几个数据模块, 则可以使用 MainModule 共享连接和其他会话变量, 并将这些数据模块创建为 "应用程序数据模块"。 他们将被附加到会议, 并由 uniGUI 管理。 代码与上一个窗体非常相似。

unit Unit2;

interface

uses
SysUtils, Classes;

type
TDataModule2 = class(TDataModule)
private
{ Private declarations }
public
{ Public declarations }
end;

function DataModule2: TDataModule2;

implementation

{$R *.dfm}

uses
UniGUIVars, uniGUIMainModule, MainModule;

function DataModule2: TDataModule2;
begin

// returns the correct instance of DataModule2 for current session
Result := TDataModule2(UniMainModule.GetModuleInstance(TDataModule2));
end;

initialization
RegisterModuleClass(TDataModule2);

end.

如果您有许多窗体或数据模块占用了许多资源, 则任何窗体都可以通过在 FormCreate 上创建它并在 FormDestroy 上释放它来手动处理其自己的数据模块。 不过, 通过将数据模块创建为 uniGUI 数据模块, 将不会创建全局变量, 也不需要在 FormCreate 中创建它, 如果开发人员忘记了在 FormDestroy 中释放数据模块, 则在关闭用户会话。

当自动为每个新用户创建 MainModule 时, 附加到会话的所有变量都应通过它访问。 不要使用全局变量。

自由形式和 DataModules

在 uniGUI 中, 您还可以创建自由形式和datamodules。 他们被称为免费的, 因为他们的一生是由开发者管理的, 而不是框架。

登录表单

一旦 MainModule 存在, uniGUI 运行时将检查登录表单是否已注册。 您可以使用 uniGUI 向导创建登录表单, 并选择 "登录。 与其他应用程序表单的唯一区别是它从 TUniLoginForm 继承, 并且在启动新会话时会自动显示。

unit LoginFormUnit1;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIRegClasses, uniGUIForm;

type
TUniLoginForm1 = class(TUniLoginForm)
private
{ Private declarations }
public
{ Public declarations }
end;

function UniLoginForm1: TUniLoginForm1;

implementation

{$R *.dfm}

uses
uniGUIVars, MainModule, uniGUIApplication;

function UniLoginForm1: TUniLoginForm1;
begin
Result := TUniLoginForm1(UniMainModule.GetFormInstance(TUniLoginForm1));
end;

initialization
RegisterAppFormClass(TUniLoginForm1);

end.

在实现此表单的功能时, 必须记住, 它只能使用 ServerModule、DataModule 和任何其他临时表单, 而不是使用 MainForm。 事实上, 任何形式都不应该访问 MainForm, 而是 MainModule。

使用全局窗体和 DataModule 引用

在 uniGUI应用程序表单中,应用程序 DataModules和MainModule都使用全局函数引用, 该功能返回当前会话的该对象的实例。

对于窗体, 此函数替换在 VCL 应用程序中创建的标准变量。 它允许为访问表单保留相同的语法。

UniForm1.显示;

在上面的代码中, 调用UniForm1函数将创建一个TUniForm1的实例, 或者返回现有实例 (使其成为单一例)。 它可用于轻松创建和显示表单。 这里的一个重要细节是, 这些函数不应用作这些窗体或数据模块的全局引用。 不建议将窗体的全局函数从其他窗体或数据模块中调用, 以用于显示该窗体以外的任何目的。

uniGUI 框架管理所有以前的 "应用程序" 对象 (窗体和 datamodules) 的生存期。 每个 uniGUI 会话/MainModule 保留托管窗体和 datamodules 的列表。 像 UniMainModule. GetFormInstance (TUniForm1) 的电话要求提供该特定表格 (TUniForm1) 的单例。 如果该实例不存在, 将创建它。 如果该实例已创建, 则返回它。

除了单例工厂之外, uniGUI 还尝试优化内存使用。 关闭会话时, 应用程序 datamodules 将自动释放 (因为无法知道它们是否正在使用), 但窗体在不使用时立即释放。 例如, 从对 ShowModal 的调用退出并接收结果后, 该表单可以安全地释放。

使用用于访问窗体而不是全局变量的函数提供了两个优点:

·每个会话都可以访问其自己的窗体, 并且只有一个 (类似于每个会话的单个实例)

·申请表由 uniGUI 框架管理, 成为按需资源


N 层 web 应用程序

N 层应用程序是分层应用程序, 每个层都应该只知道并访问它下面的层。 这种方法提供了许多优点, 即使是像使用嵌入式数据库服务器这样的 VCL 应用程序这样的单片应用程序。

然而, 当层被物理地分开时, 它甚至更好。 典型的2层应用程序是连接到共享数据库服务器的客户端/服务器 VCL 应用程序。 在这种情况下, 两个层都是清晰识别和分离的。

创建等效的 uniGUI web 应用程序将自动创建第三层, 在浏览器中实际运行, 从 uniGUI 应用程序动态创建并传输到客户端浏览器。

开发人员必须知道表示层和业务层之间的物理分离 (不管它对面向对象的设计有多好)。 在新应用程序中, 某些代码在客户端上运行, 而其他代码则在服务器上运行。 客户端事件、服务器端事件和来自客户端的动态 AJAX 请求都必须由服务器应答。

一个非常重要的区别是典型 web 应用程序的异步字符。 服务器应用程序动态呈现客户端网页, 并等待来自所有会话的客户端事件;它不应该在消息循环中等待特定会话的 VCL 应用程序。

更重要的是了解如何与大量数据进行交互 (最终, 这些应用程序中的大多数需要在后端使用共享数据库服务器)。


同步和异步操作

默认情况下, uniGUI 应用程序是在异步模式下工作的标准 web 应用程序。 这意味着服务器在等待来自客户端的用户响应时不需要阻止会话线程。

异步模式

如何在这种模式下使用ShowModal？

让我们用一个非常简单的例子。 我们将创建一个表单, 用于输入具有接受或取消请求的按钮的人名。 另一个窗体将使用上一个窗体更新其自己的内部字段 FFullName。

unit Unit3;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm, uniButton, uniEdit, uniGUIBaseClasses, uniLabel;

type
TUniForm3 = class(TUniForm)
UniLabel1: TUniLabel;
edtFullName: TUniEdit;
btnOk: TUniButton;
btnCancel: TUniButton;
private
function GetFullName: string;
{ Private declarations }
public
{ Public declarations }

property FullName : string read GetFullName;
end;

function UniForm3: TUniForm3;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function UniForm3: TUniForm3;
begin
Result := TUniForm3(UniMainModule.GetFormInstance(TUniForm3));
end;

{ TUniForm3 }

function TUniForm3.GetFullName: string;
begin
if edtFullName.Text = '' then
Result := 'John Doe'
else
Result := edtFullName.Text;
end;

end.

如果全名为空, 此表单将返回输入的全名或 "无名氏"。 其结果将取决于按下按钮 OK 或取消 (或关闭窗体)。 我们如何使用它？

unit Unit1;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm, uniGUIBaseClasses, uniButton;

type
TUniForm1 = class(TUniForm)
btnGetFullName: TUniButton;
procedure btnGetFullNameClick(Sender: TObject);
private
{ Private declarations }

FFullName : string;
public
{ Public declarations }
end;

function UniForm1: TUniForm1;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication,
Unit3;

function UniForm1: TUniForm1;
begin
Result := TUniForm1(UniMainModule.GetFormInstance(TUniForm1));
end;

procedure TUniForm1.btnGetFullNameClick(Sender: TObject);
begin
UniForm3.ShowModal
(
procedure (Sender: TComponent; AResult: Integer)
begin

if TModalResult(AResult) = mrOk then
FFullName := (Sender as TUniForm3).FullName;
end
);
end;

end.

这里发生了什么？

·我们先按下按钮 GetFullName。

·一旦我们调用 UniForm3, 就会创建并返回表单 TUniForm3 的实例。

·对 ShowModal 的调用将弹出窗体, 它的行为类似于客户端模式形式 (这意味着在该窗体之外不允许进一步交互)。

·但是, 服务器端, 程序将继续, 在这种情况下意味着它将退出 OnClick 事件处理程序。

·此时, 服务器完成了上一个请求 (即, 由用户触发客户端的 OnClick 事件)。

·当用户最终单击任何按钮 (Ok 或取消) 或关闭窗体时, 另一个请求将转到服务器: 处理该窗体的 ShowModal 结果。

·服务器将标识用户会话, 并将执行仍保留对 UniForm3 的引用的匿名方法 (匿名方法的标准行为)。

·执行匿名方法后, 表单 UniForm3 将可供处理 (其生存期由 uniGUI 框架管理)。

同步模式

在异步模式下创建 web 应用程序总是首选, 但在迁移旧的 VCL 应用程序时, 开发人员可能会发现无法实现的方案, 或者会使任务变得太难。 如有必要, uniGUI 可以强制作为典型的 VCL 应用程序工作。

如果开发人员需要重现 VCL 应用程序中可用的同步模式, 则属性MainModule EnableSynchronousOperations必须设置为 true。

ShowModal

如何在这种模式下使用 ShowModal？

procedure TUniForm1.btnGetFullNameClick(Sender: TObject);
begin
if UniForm3.ShowModal = mrOk then
FFullName := UniForm3.FullName;
end;

这与我们通常用于 VCL 应用程序的代码相同。 它还会在调用 ShowModal 直到返回结果后停止执行。 此类代码要求在服务器中生成一个线程, 等待客户端请求/应答。 正如您所期望的那样, 这种应用程序消耗的资源比标准 web 应用程序要多。 幸运的是, uniGUI 使用内部线程池最小化线程使用量, 但对于每个等待模式窗口, 线程将被消耗。 关闭模式窗口后, 等待线程将返回到线程池, 以供将来使用。

除了 ShowModal, uniGUI 还允许在其他方案中执行同步操作。 让我们来看看一些演示。

阻塞情态动词

有一个演示, 说明阻塞情态动词 "BlockingModals"。

BlockingModals \

下一个代码显示了每个函数现在是一个阻塞调用, 就像它在 VCL 中一样。

procedure TMainForm.btnShowModalClick(Sender: TObject);
var
M : TModalResult;
begin
M := UniForm1.ShowModal;

case M of
mrOK : ShowMessage('OK');
mrCancel : ShowMessage('Cancel');
mrNone : ShowMessage('None');
end;

ShowMessage('Done, ' + UniForm1.UniEdit1.Text);
end;

procedure TMainForm.btnMessageDlgClick(Sender: TObject);
var
Res : Integer;
begin
Res := MessageDlg('Test', mtConfirmation, mbYesNoCancel);

case Res of
mrYes : ShowMessage('Yes');
mrNo : ShowMessage('No');
mrCancel : ShowMessage('Cancel');
end;
end;

procedure TMainForm.btnPromptClick(Sender: TObject);
var
sResult : string;
begin
if Prompt('Please type something?', 'Value',
mtInformation, mbYesNoCancel, sResult, True) = mrYes then
ShowMessage('Result: ' + sResult);
end;

procedure TMainForm.btnNestedCallsClick(Sender: TObject);
var
sResult : string;
begin
if UniForm1.ShowModal = mrOK then
if Prompt('Please type something?', '',
mtInformation, mbYesNoCancel, sResult, True) = mrYes then
if MessageDlg('Continue?', mtConfirmation, mbYesNoCancel) = mrYes then
ShowMessage('Result: ' + sResult + ' ' + UniForm1.UniEdit1.Text );
end;

procedure TMainForm.btnNestedForms1Click(Sender: TObject);
begin
if UniForm1.ShowModal = mrOK then
if UniForm2.ShowModal = mrOK then
if UniForm3.ShowModal = mrOK then
if UniForm4.ShowModal = mrOK then
begin
ShowMessage('All shown!');
end;
end;

procedure TMainForm.btnNestedForms2Click(Sender: TObject);
begin
if UniForm5.ShowModal() = mrOK then
ShowMessage('Completed');
end;

procedure TMainForm.btnFileUploadClick(Sender: TObject);
begin
if UniFileUpload1.Execute then
begin
ShowMessage('File upload completed.' +
^M^M'Filename: ' + UniFileUpload1.FileName +
^M^M'Temporary file is located under:' +
UniFileUpload1.CacheFile);
end;
end;

进度栏

演示 "" SyncClientUpdate-1 "显示了如何在执行冗长的任务时更新进度栏。

一旦用户单击按钮开始, 这就是实现所需行为的代码类型:

begin
FCancelled := False;

UniProgressBar1.Min := 1;
UniProgressBar1.Max := MAX_FILES;
btnStart.Enabled := False;
btnCancel.Enabled := True;

// Reset Progressbar and Label
UpdateClient(0);

// Initial refresh before entering the loop.

// (Progressbar and Label will be refreshed)
UniSession.Synchronize;

N := 0;
try
for I := 1 to MAX_FILES do
begin

// Refresh the client at "X_INTERVAL" intervals
if UniSession.Synchronize(X_INTERVAL) then
UpdateClient(N);

检查是否取消了操作。

// (Either when Cancel button is pressed or Form is closed)
if FCancelled then Break;

// perform some tasks here
CreateDummyFile(I);

Inc(N); // +1 number of created files
end;

UpdateClient(N);
finally
btnStart.Enabled := True;
btnCancel.Enabled := False;
end;
end;

呼叫 UniSession 同步强制立即更新。 传递参数允许在受控期间 (每隔几毫秒) 进行同步。

使用 ShowMask 同步处理

演示 "SyncClientUpdate-3" "执行具有状态更新的任务序列, 并使用 ShowMask 来避免用户与其他可视化控件进行交互。

"开始" 按钮的代码为:

begin
UniLabel1.Caption := '';
UniLabel2.Caption := '';
UniLabel3.Caption := '';
UniCheckBox1.Enabled := False;

UniImage1.Hide;

UniProgressBar1.Position := 0;
UniButton1.Enabled := False;

ShowMask('Level 1 in progress ..');
UniSession.Synchronize;

Sleep(1000);

UniProgressBar1.Position := 1;
UniLabel1.Caption := 'Level 1 Completed';
UniLabel1.Font.Style := [fsBold];
HideMask;

ShowMask('Level 2 in progress ....');
UniSession.Synchronize;

Sleep(1000);

UniProgressBar1.Position := 2;
UniLabel2.Caption := 'Level 2 Completed';
UniLabel2.Font.Style := [fsBold];
HideMask;

ShowMask('Level 3 in progress ......');
UniSession.Synchronize;

Sleep(1000);

UniProgressBar1.Position := 3;
UniLabel3.Caption := 'Level 3 Completed';
UniLabel3.Font.Style := [fsBold];
HideMask;

ShowMask('Last level in progress ........');
UniSession.Synchronize;

Sleep(1000);

UniProgressBar1.Position := 4;
UniImage1.Show;
UniButton1.Enabled := True;
UniCheckBox1.Enabled := True;
HideMask;
end;

同步等待事件或超时

演示 "" SyncClientUpdate-5 "显示同步 (等待: 布尔值) 和同步 (延迟: 整数) 之间的差异。

按下按钮 "" 开始不等待 "将 UniPanel1 移动到右下角5像素, 每200毫秒 (显示移动)。

按下按钮 "" 开始 "" "将完成相同的任务, 但每个步骤只有在检测到某些客户端事件时才会发生 (例如, 按下虚拟按钮" "按!" 或调整窗体的大小。

该代码为:

procedure TMainForm.btnStartNoWaitClick(Sender: TObject);
begin
btnStart.Enabled := False;
btnStartNoWait.Enabled := False;

while UniPanel1.Top < Self.Height - 150 do
begin
if UniSession.Synchronize(200) then // wait 200 ms
begin
UniPanel1.Top := UniPanel1.Top + 5;
UniPanel1.Left := UniPanel1.Left + 5;
end
else
Sleep(10);
end;

UniPanel1.Top := 5;
UniPanel1.Left := 5;
btnStart.Enabled := True;
btnStartNoWait.Enabled := True;
end;

procedure TMainForm.btnStartClick(Sender: TObject);
begin
FStopped := False;

btnStart.Enabled := False;
btnPress.Enabled := True;
btnStop.Enabled := True;

while not FStopped do
begin
UniSession.Synchronize(True); // wait until next event

UniPanel1.Top := UniPanel1.Top + 5;
UniPanel1.Left := UniPanel1.Left + 5;
end;

UniPanel1.Top := 5;
UniPanel1.Left := 5;
btnStart.Enabled := True;
btnPress.Enabled := False;
btnStop.Enabled := False;
end;

procedure TMainForm.btnPressClick(Sender: TObject);
begin
// Dummy Event generator
end;


以数据库为中心的应用程序

大多数 web 应用程序都需要一些持久性存储, 通常意味着共享数据库。 在这种应用中, 它可能会识别多个层:

1.驻留数据库的数据库服务器。

2.将应用程序连接到数据库的数据访问技术

3.web 应用程序

4.在许多浏览器上运行的一个或多个客户端。

虽然客户端可以从 Internet 或本地 Intranet 进行连接, 但 web 应用程序服务器需要与数据库服务器进行更佳/更紧密的连接, 以便在应答客户端请求时达到合理的性能。

直接从 web 应用程序服务器访问数据库 (这意味着项目2将成为 it 的一部分) 会增加服务器上资源的使用, 从而危及其稳定性和可伸缩性。

考虑到客户端和服务器之间可用的带宽有限, 重要的是限制发送到客户端的信息量 (或从它返回)。

由于一个 web 服务器将应答来自多个客户端的请求, 因此限制客户端功能所需的信息量也很重要。

拥有一个共享数据库服务器还意味着应该为请求访问该共享资源的多个连接优化数据库。

让我们来谈谈处理此类应用程序的一些最佳做法。

数据库

使用共享数据库作为持久存储的企业级应用程序通常遵循一些最佳做法:

·它不是为所有目的使用相同的表, 而是将它们与任务分开。 甚至有可能由不同的服务器承载不同的数据库。 其中的一个数据库负责联机事务处理 (OLTP), 而其他数据则为复杂报表 (OLAP) 提供了信息。

·OLTP 数据库应为第三范式 (3NF), 使其更易于保持其完整性。 应针对快速数据输入、更新和简单查询 (索引应为预期的使用进行优化) 进行优化。 复杂更新应使用事务 (以便一次可以回滚事务)。

·如果事务量减慢了数据库的速度, 则值得评估将事务 (此时可以被视为只读) 的选项转移到用于历史目的的辅助数据库或 OLAP 数据库的数据源。

·应为具有快速响应时间的复杂查询优化 OLAP 数据库。 因此, 它通常聚合数据, 并添加维度。 最典型的体系结构是星型架构中的一个大事实表。

·对数据库中存储的数据进行的任何繁重处理都应在数据库服务器上进行 (如果我们想通过从应用服务器读取并将它们重新发布到表中来修改一个表格中的数以百万计的记录, 可能会发生什么情况)。 即使一些现代开发人员认为数据库只是一个对象存储, 企业级数据库服务器也可以将一些业务逻辑作为存储过程来承载, 这会对性能产生戏剧性的影响。

·除非严格必要, 否则应避免基于客户端请求触发同步操作。 例如, 如果需要向远程服务器或 web 服务发布/记录对数据库的任何更新, 最好在本地表中保留通知并处理后台作业中的通知。

以前的最佳做法都没有专门与 uniGUI 相关, 它们适用于任何以数据为中心的应用程序。

数据库访问

拥有强大的数据库服务器和与之快速的连接并不意味着我们应该请求我们不需要的信息。 数据库表可以容纳数以百万计的行, 但我们不应该要求所有的, 因为这样做没有实际的目的。

假设我们使用的是数据模块、数据库连接 (FireDAC TFDConnection 或类似) 以及查询来访问数据库。 如果我们从前一张巨大的桌子上索取所有的记录, 会发生什么？ 显然, 经过一段时间 (可能会很长一段时间取决于服务器上可用的内存量), 我们将收到一个异常 "内存不足"。 我们真的需要要求所有的记录吗？ 不, 我们只是应用同样的技术, 我们使用的小应用程序。

访问数据库时最重要的最佳做法是要求足够的信息, 正是我们需要的, 而不是更多。

了解访问数据库所使用的特定技术也很重要:

·有些技术很聪明, 可以接受打开这样一个查询的请求, 但是使用巧妙的分页算法来传递数据。 开发人员将知道有多少记录可用, 但分页过程只在需要时才提供。

·其他技术需要手动配置以避免大量的数据负载, 并且必须获取所有记录才可以报告 RecordCount。

·大多数中间件产品都处理这些问题 (如RemObjects DataAbstract、 kbmMW和DataSnap)。

解决此类问题的最简单方法是确保始终筛选用户请求的任何信息。 如果记录量大于某个预定义的限制, 则请求可能被拒绝, 用户应提供一个较窄的筛选器。

另一个选项是接受原始请求, 但要注意分页(如果数据访问技术本身不支持)。

首先, 作为一个原则, 用户能够使用的信息量总是很小, 从不发送多余的信息 (质量和数量)。

数据控件 (服务器和客户端)

在 N 层应用程序的分层方法中, 每个层都应该关注如何通过将请求转换为对上一层的请求来回答来自下一层的要求。

任何 uniGUI 数据控件都通过数据集对上一层进行请求。

客户端 Ext JS 组件 (由 uniGUI 服务器动态呈现) 显示接收到的数据, 并接受用户输入, 使 Ajax 请求返回到服务器。

显然, 这一流量应该保持在最低限度。

让我们看几个示例, 说明 uniGUI 如何实现以前的最佳做法:

·TUniEdit属性 CheckChangeDelay 允许在一段时间后触发 OnChange 事件 (在基于更改的值筛选数据集时很有用)。

·TUniDBLookupComboBox -允许启用自定义 RemoteQuery, 可以根据用户键入的信息进行优化 (还有一个 RemoteDelay)。

·TUniDBGrid -它提供了列筛选、分页、客户端导航等功能。


用户界面

uniGUI 方便地与 RAD 工作室VCL 设计器集成, 以利用当前开发人员的知识, 但设计者无法呈现在 web 浏览器中运行的实际应用程序的确切用户体验。

正如前面提到过的, VCL 组件与 uniGUI 不兼容, 但 uniGUI 组件保留若干 VCL 属性, 在将 VCL 应用程序迁移到 uniGUI 应用程序时提供了更好的兼容性。 但是, uniGUI 组件呈现为 web 专门设计和优化的 ExtJS 组件。 也就是说, 即使是标准的 uniGUI 组件 (如 TUniPanel) 利用 web 功能, 而其他组件在 VCL 中却没有等价的。 uniGUI 组件成为 Sencha 在客户端浏览器中运行的 Ext JS 组件, Sencha 正在不断改进其组件并创建新组件。

创建 uniGUI 用户界面的最佳做法是为 web 配置组件, 并使用为 web 优化的最佳组件。

让我们通过比较 VCL 对齐方式和客户端对齐方式来演示如何应用此最佳做法, 并学习如何使用 TUniFieldSet 和 TUniFieldContainer。

VCL 对齐与 web 对齐

VCL 对齐方式发生在服务器端, 而 web 对齐则立即执行客户端。 如果开发人员使用带有 TSplitter 的 TPanel 控件来调整面板宽度, 则拖动 TSplitter 将创建一个请求序列, 该服务器将向客户端发送更新以呈现更改。 它成本高昂, 使用户界面响应能力降低。

Vcl

某些 VCL 控件属性为:

·高度

·宽度

·返回页首

·离开

·对齐-alNone, alTop, alBottom, alLeft, 好, alClient

windows 和 VCL 是面向对象的、基于组件的 windows 可视对象的封装, 它们都基于屏幕物理坐标(这是由于使用更高的 DPI 显示高分辨率显示器造成当前故障的原因)。

万维网必须处理非常不同的客户端设备, 它基于相对位置。

uniGUI 保留相同的 VCL 属性, 但它也允许重写 VCL 行为并利用更强大和灵活的 web 属性。

Web

容器中的对齐方式

某些 Web 属性为:

·AlignmentControl-uniAlignmentClient, uniAlignmentServer

·版式-绝对, 手风琴, 锚, 自动, 边框, 适合, 窗体, hbox, vbox, 表, 列

·LayoutAttribs

o对齐-上、中、下、拉伸、stretchmax

o列

o包装-开始, 中心, 结束

o填充

·LayoutConfig

o锚

oBodyPadding

oColSpan

oColumnWidth

oDockWhenAligned

oFlex

o高度

oIgnorePosition

o边缘

o填充

o地区-北部, 南部, 东部, 西部, 中心 (相当于 alTop, alBottom, 好, alLeft, alClient)

oRowSpan

o拆分-启用自动客户端拆分器

o宽度

在文件夹演示中, 有几个项目演示如何使用以前的属性:

·Clientside 对齐-停靠和对齐

·Clientside 对齐-版式手风琴

·Clientside 对齐-布局锚点

·Clientside 对齐-布局边框

·Clientside 对齐-布局列

·Clientside 对齐-布局适合

·Clientside 对齐-版式窗体

·Clientside 对齐-布局 HBox

·Clientside 对齐-布局百分比

·Clientside 对齐-布局 VBox

·Clientside 对齐-布局表格

·Clientside 对齐-布局表格跨度

·Clientside 对齐-功能演示

板

UniGUI 面板也可折叠。 相应的属性有:

·倒塌

·CollapseDirection-cdBottom, cdDefault, cdLeft, cdRight, cdTop

·湿 陷 性

演示可折叠面板显示了几个可折叠面板。

Collapsible Panels with server-side alignment

具有服务器端对齐的可折叠面板

此演示从一些折叠的面板开始, 所有四可以使用相应的按钮折叠或展开。 您将注意到, 每个折叠/展开请求都需要访问服务器才能呈现新布局。 请尝试通过将窗体 AlignmentControl 更改为 uniAlignmentClient 并使用前面描述的 web 属性配置控件来更改该行为。 确认新应用程序执行布局更改的速度要快得多 (不再需要向服务器请求新的呈现)。

字段集

TUniFieldSet

uniGUI 控件 TUniFieldSet 包含字段 (uniGUI 控件/编辑器), 应在一列中进行排列。 此容器将根据公共属性呈现每个字段:

·FieldLabel

·FieldLabelAlign-alLeft, 好的, alTop

·FieldLabelFont

·FieldLabelSeparator-默认值 ":"

·FieldLabelWidth

在 TUniFieldSet 中可以包含的 uniGUI 控件中有 TUniFieldContainer, 它允许创建类似于以下内容的分层形式:

TUniFieldSet and TUniFieldContainer(s) - UniFieldContainer example

TUniFieldSet 和 TUniFieldContainer (s)-UniFieldContainer 示例

TUniFieldContainer

容器根据所请求的 web 布局排列字段。 在前面的示例中, FieldContainer3 正在使用3列的版式表格。

FieldContainer3

TUniFieldContainer 按创建顺序呈现字段。 如果该顺序是或变为不正确, 则应使用属性 CreateOrder 指示所需的订单。 默认值为零, 这意味着它们应该是最后一个字段。 所需的顺序以一个开头, 字段将保存在。该命令的 DFM 文件。

独立于业务逻辑的用户界面

德尔福是一个刺激发展速度的 RAD 环境。 只需在窗体上除去几个组件、更改某些属性、添加一些事件处理程序以及实现作为应用程序的原始目标的实际代码, 就可以轻松地创建一个小应用程序。 几分钟后, 你就可以有一个工作的应用程序。 这是真的;RAD 工作室让它成为可能。 但在大多数情况下, 这种应用对更大的项目来说并不是一个好的基础。 将用户界面与业务逻辑合并, 添加可视化控件和数据访问组件, 在同一单元中拥有所有类型的事件处理程序是失败的好方法。

有许多用于断开表示层和业务逻辑的解决方案。 每个人都知道MVC (模型视图-控制器)、 MVP (模型视图-演示者)、 MVVM (模型视图-ViewModel) 以及类似的设计模式。 虽然这些模式中有些是通用的, 并且可以在几种语言、开发环境和支持框架中实现, 但其中一些类型对于特定的方案更好。 例如, MVVM 是专门为利用WPF (Windows 表示基础)、 XAML和事件驱动编程而设计的。

尽管德尔斐的环境, 它的一些功能允许实现以前的目标, 不一定遵循严格的模式。

使用接口, 而不是窗体

让我们首先演示如何应用更重要的最佳实践之一: 针对接口的代码, 而不是具体的类。

我们的程序可以作为桌面应用程序或 Web 应用程序运行, 但我们的业务逻辑将需要用户交互。 如果我们需要询问用户一些信息, 我们需要使用一些视觉伪影, 但这并不意味着我们的业务逻辑需要知道有关它的不必要的细节。 甚至更好的是, 如果不具体实现工件 (窗体、消息对话框, 无论是什么), 都可以编写业务逻辑。

用一个简单的例子来解释这个原则会更好。

unit _MyForm;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
TMyForm = class(TForm)
edtSomeText: TEdit;
btnOk: TButton;
btnCancel: TButton;
lblSomeText: TLabel;
private
function GetSomeText: string;
procedure SetSomeText(const Value: string);
public

property SomeText : string read GetSomeText write SetSomeText;
end;

function GetOrModifySomeText(var aText : string) : boolean;

implementation

{$R *.dfm}

function GetOrModifySomeText(var aText : string) : boolean;
var
MyForm: TMyForm;
begin
MyForm := TMyForm.Create(Application);
try
MyForm.SomeText := aText;

Result := MyForm.ShowModal = mrOk;

if Result then
aText := MyForm.SomeText;
finally
MyForm.Free;
end;
end;

{ TMyForm }

function TMyForm.GetSomeText: string;
begin
Result := edtSomeText.Text;
end;

procedure TMyForm.SetSomeText(const Value: string);
begin
edtSomeText.Text := Value;
end;

end.

业务逻辑只需要访问函数GetOrModifySomeText , 如果修改了作为参数传递的文本, 则返回 true。 它不需要知道如何做到这一点。 如果使用该函数的唯一方法是对实现业务逻辑的单元进行显式引用, 我们将添加对 VCL 窗体的依赖关系, 甚至更糟的是它的特定实现。

同样的形式, 这一次实现作为一个 uniGUI 自由形式, 需要一个类似的代码。

unit _MyForm;

interface

uses
System.Classes,
Vcl.Controls,
Vcl.Forms,
uniGUIBaseClasses,
uniGUIClasses,
uniLabel,
uniGUITypes,
uniGUIAbstractClasses,
uniGUIForm,
uniGUIApplication,
uniEdit,
uniButton;

type
TMyForm = class(TUniForm)
edtSomeText: TUniEdit;
btnOk: TUniButton;
btnCancel: TUniButton;
lblSomeText: TUniLabel;
private
function GetSomeText: string;
procedure SetSomeText(const Value: string);
public

property SomeText : string read GetSomeText write SetSomeText;
end;

function GetOrModifySomeText(var aText : string) : boolean;

implementation

{$R *.dfm}

function GetOrModifySomeText(var aText : string) : boolean;
var
MyForm: TMyForm;
begin
MyForm := TMyForm.Create(UniGUIApplication.UniApplication);
MyForm.SomeText := aText;

Result := (MyForm.ShowModal = mrOk); // Synchronized mode

if Result then
aText := MyForm.SomeText;
end;

{ TMyForm }

function TMyForm.GetSomeText: string;
begin
Result := edtSomeText.Text;
end;

procedure TMyForm.SetSomeText(const Value: string);
begin
edtSomeText.Text := Value;
end;

end.

这里唯一的区别是我们不需要显式释放 uniGUI 表单。

现在, 如何能够编写业务逻辑而不需要具体的窗体实现, 甚至更好地使用相同的代码来针对 VCL 和 uniGUI？

让我们做一些重构。

Extract an interface exposing the form behavior

type

IMyForm = interface
['{80CDB092-DDA0-49A0-9FFF-F8D69E18777C}']

function GetSomeText: string;
procedure SetSomeText(const Value: string);
function _ShowModal : integer;

property SomeText : string read GetSomeText write SetSomeText;
end;

修改两个表单以实现接口

unit _MyForm;

interface

uses
System.Classes,
Vcl.Controls,
Vcl.Forms,
uniGUIBaseClasses,
uniGUIClasses,
uniLabel,
uniGUITypes,
uniGUIAbstractClasses,
uniGUIForm,
uniGUIApplication,
uniEdit,
uniButton,
_MyFormIntf;

type

TMyForm = class(TUniForm, IMyForm)
edtSomeText: TUniEdit;
btnOk: TUniButton;
btnCancel: TUniButton;
lblSomeText: TUniLabel;
private
function GetSomeText: string;
procedure SetSomeText(const Value: string);
function _ShowModal : integer;
public

end;

implementation

{$R *.dfm}

{ TMyForm }

function TMyForm.GetSomeText: string;
begin
Result := edtSomeText.Text;
end;

procedure TMyForm.SetSomeText(const Value: string);
begin
edtSomeText.Text := Value;
end;

function TMyForm._ShowModal : integer;

开始

Result := ShowModal;

end;

end.

不引用表单, 但接口

function GetOrModifySomeText(MyForm: IMyForm; var aText : string) : boolean;
begin
MyForm.SomeText := aText;

Result := MyForm._ShowModal = mrOk;

if Result then
aText := MyForm.SomeText;
end;

创建表单后, 可以将其作为实现的接口传递。 请注意, 由于 VCL 和 uniGUI 的 ShowModal 不同, 我们将 _ShowModal 添加到接口中。

不访问窗体, 除非创建和释放实例

德尔菲形式是 "视图" 或表示层的可能实现之一。 它应该始终实现一个或多个接口来阐明其目的。 对表单的任何访问都应通过使用它所实现的接口之一来完成, 在修改内部资源时隐藏任何实现细节并避免将来出现错误。

通过接口访问表单可避免在窗体更改时使用窗体破坏代码。 它还打开了创建实现同一接口的不同窗体/视图的可能性。 例如:

·应用程序可以共享大部分的业务逻辑, 同时针对德尔福 VCL 桌面和德尔福 uniGUI (也类似桌面)。

·使用 uniGUI 构建的 web 应用程序可以提供两个不同的用户界面, 一个用于桌面用户 (有鼠标和键盘), 另一种用于触摸设备 (电话和平板电脑)。

从不在表单中引入业务逻辑代码, 利用德尔菲功能

表示业务逻辑并嵌入表单的每一行代码都会创建不必要的依赖项, 并妨碍创建新视图。 典型的 web 应用程序通过业务逻辑层提供对数据库服务器的访问。 等效的德尔菲对象是窗体、数据模块、可选的业务类和数据库连接。

让我们分析一下这些德尔菲对象与 MVC 模式的组件的关系。

Model View Controller

模型视图控制器

德尔菲实现 MVC 是这样的:

·风景是 TForm, TUniForm, TUnimForm。

o它是在设计时创建的, 在运行时呈现。

o某些属性可以通过其接口进行更新 (仍然是一个良好的行为)

o大多数更新将通过数据库组件自动传播 (在创建过程中连接)

o任何用户操作都可能触发与数据库相关的更新 (完善), 而另一些则可能触发操作(也很好)。 其他可接受的良好行为可能是执行所需/预期操作, 如创建自己的数据模块并释放它。

·控制器通常是表单使用的数据模块, 大多数时候它包含所有模型 (至少是数据库组件) 的一部分。

o在一个小的应用中, 控制器和模型将在 MainModule 中实现。

o除了 MainModule 之外, 更大的应用程序还可以使用数据模块来实现某些形式。

模型-视图-演示者模式显示了类似的内容。

Model View Presenter

模型视图演示者

在此模式中, 演示者是在 VCL 或 uniGUI 中实现的机制, 用于处理模型 (数据模块和业务类) 和视图 (窗体) 之间的 "通信"。 值得一提的是, 这种观点是被动的 (或尽可能被动), 这意味着它不包括任何业务逻辑。

在下一节中, 我们将演示如何构建面向两个 uniGUI 平台 (桌面和触摸) 的应用程序, 但列出应用程序将使用的德尔菲功能非常重要。

数据库控件及其事件

由于目标是将业务逻辑代码保留在视图之外, 因此我们决不会在窗体中使用数据访问组件。 相反, 所有这些组件都将由数据模块承载。 在 VCL 应用程序中, 该数据模块将自动创建并分配给全局变量。 uniGUI 的方法是不同的, 因为 MainModule 可以通过函数 uniMainModule 访问。 在这两种情况下, 任何窗体都可以在设计时 (指向相应的数据源和数据集) 链接其数据访问控件。

遵循在需要时消耗资源的原则, 总是按需进行, 需要数据库访问的每个表单都应声明/请求数据模块激活/取消激活其资源 (即, 打开和关闭数据集)。 这些行动应在 OnCreate 和 OnDestroy 中进行。

在较大的应用程序中, 可能需要创建和销毁表单使用的任何其他数据模块。

在任何情况下, 通过在数据模块中承载数据访问控件, 任何事件都将在同一位置实现 (或者它可以请求从数据模块中定义的某些业务类)。

通过遵循这些实践, 没有将任何业务逻辑添加到视图中。 正如我们在本章后面将提到的, 甚至可以注入这些最小依赖项。

从视图请求操作而不编写代码 (独立于平台)

如何使用菜单(MainMenu 或 PopupMenu), 按下请求全局操作的按钮或针对某个选定数据集记录的操作？

在以数据为中心的应用程序中, 大多数请求的操作都会影响活动记录或某些记录的选择。 其他操作是全局的, 可以在用户界面的多个位置请求。

德尔菲长期支持操作和操作列表。 工具栏中的每个按钮都可以链接到一个操作。 任何菜单中的菜单项也可以链接到操作。 其他按钮共享该功能。 该操作列表由同一数据模块承载, 每次用户触发操作时, 操作的 Execute 方法都可以查询上下文 (例如, 数据集的选定记录) 并实现所请求的业务逻辑。

当然, 某些操作可能需要通过与用户的用户界面进行交互。 在使用接口时, 开发人员可以在不知道它是如何实现的情况下访问视图, 也不了解它的目标平台, 在这种情况下, 我们将需要实例化当前平台的视图。

让我们看一下前面问题的两个可能的解决方案。

创建接口作为平台依赖服务的门面

当 VCL 应用程序或 uniGUI 会话启动时, 无论当前平台是什么, 都可以保存并在以后使用以实例化正确的视图。 当然, VCL 应用程序将始终是 VCL 的, 但 uniGUI 应用程序可以针对两个平台: 桌面和触摸。 如果用户会话从触摸设备开始, 任何进一步的交互都将依赖于触摸窗体和消息。

对于小型项目, 最简单的解决方案是为业务逻辑所需的所有 "服务" (窗体和消息) 定义一个接口, 为这两个目标实现它们, 并根据用户会话实例化正确的。 之后, 每次需要表单时, 都将使用通过接口提供的具体实现。

使用依赖项注入注册同一窗体接口的多个实现

大型项目可以有数以百计甚至上千种形式, 使以前的解决方案乏味、耗时且容易出错。 正确的解决方案是将该任务留给像Spring4D这样的依赖注入框架。

这是超出了本文档的范围, 但我们将展示如何修改我们的演示, 以做到这一点。


小型混合应用

安装 uniGUI 专业人员后, 超过100个演示项目显示了 uniGUI 的不同功能, 用于创建在桌面浏览器上运行的 web 应用程序。 随着 uniGUI 的完成, 一个新的目标平台是可用的, 具有触摸功能的设备。

几乎任何应用程序都可以为启用触摸功能的设备 (通常是智能手机和平板电脑) 提供其功能的子集, 而标准 web 界面则是功能齐全的。 例如, 企业应用程序需要多种形式来处理其所有子系统, 但管理人员可能对从任何设备 (PC、笔记本、智能手机、tablet) 访问的更简单的仪表板感兴趣。

换言之, 混合应用程序是标准的, 本节将显示一个模式, 说明如何按照前面提到的最佳做法创建它们。


特性和限制

此演示应用程序演示了几种最佳做法, 但它很简单, 可以读取和测试。 因此, 有些功能涉及限制。

什么是应用程序关于

这是一个非常有限的销售点应用。

·用户是操作员。

·具有管理权限的用户可以创建其他用户。

·所有用户都可以输入订单。

·订单标识客户。

·订单知道什么时候创建的, 当客户为他们支付。

·每个订单可以有几个项目。

·物料是指某些产品数量。

·产品有价格。

·销售报表将导出并显示具有所有注册订单的 PDF 文件。

有几个明显的限制:

·同一用户可以从多个设备登录

·订单未记录用户

·支付的订单不是只读的

·报告不提供任何筛选

正如在查看代码后将会清楚的那样, 以前的大多数限制都很容易克服。

它是一个基于数据库的应用程序

即使是小型 web 应用程序也会使用一些持久性。 最常见的持久性是一个关系数据库, 由多个会话共享。 根据应用程序将接受多少个并发会话, 选择适当的数据库 (响应时间、并发) 是非常重要的。

·演示应用程序使用SQLite, 一个更适合公文包使用的数据库。

·使用 SQLite 的唯一原因是 FireDAC 在本地支持它, 而无需任何外部驱动程序。

·而不是 SQLite, Microsoft SQL server Express (或 LocalDB) 可能是更好的选择。

如果应用程序将来应该能够支持或使用不同的数据库引擎, 而不是 "hardwiring" "当前选项", 则最好使用ORM (对象关系映射) 工具。 例如, 以下设计是由Devart 实体开发人员从 SQLite 数据库进行反向工程的。

销售数据库

数据访问技术 FireDAC

由于 FireDAC 是德尔菲首选的数据访问技术, 它为 SQLite 提供本地支持, 此演示使用 FireDAC 数据访问控制。

如何访问应用程序

·作为任何 web 应用程序, 您都需要一个现代化的浏览器。

·标准的最佳做法是使用登录窗体来处理用户权限。

·如果使用登录表单, 最好使用 ssl (https://), 但此演示不使用 ssl (仅http://)

·应用程序有两个入口点:

o http://<host>:8077为完全桌面般的体验

o http://<host>:8077/m, 用于移动/触摸功能体验

访问控制

应用程序登录表单使用验证用户的最低标准: 用户名和密码。

·与往常一样, 密码用星号屏蔽。

·密码不应转到数据库。 最佳做法建议存储哈希值。

·此应用程序在数据库中存储未加密的密码(显示如何生成 uniGUI 应用程序并不重要)。

·登录成功后, 应用程序将知道当前用户是否是管理员 (管理员可以管理用户列表)。

它超出了本文档的范围, 但RBAC (基于角色的访问控制) 描述了如何管理特权和角色 (即, 每个用户都可以扮演多个角色, 每一个都提供特定权限)。

Desktop Login Form

桌面登录表单

Touch Login Form

触摸式登录表单

主要形式

主要的申请表通常为应用程序的 "启动垫"。 它是用户执行应用程序模块的地方。

·像这个演示这样的小应用程序将有几个模块。 它使用平铺样式的 UI (标准菜单加工具栏或只是几个大按钮)。

·具有多个模块的中型或大型应用程序应使用不同的范例。 一个好的开始可以是在左面板上使用导航树, 以及在右侧承载模块的页面控件。

Desktop Main Form

桌面主窗体

Touch Main Form

触摸主窗体

编辑用户窗体

应用程序的用户将操作该应用程序。 管理员可以创建和编辑其他用户, 而其他用户只能访问订单和报表。 除了已提及的限制外, 还有其他限制, 有待改进。 工具栏上的按钮允许切换所选用户的 "管理员" 状态。 在可见快照中, 按下按钮将弹出一条消息 ""您至少需要一个管理帐户!"", 但如果使用该复选框发生相同的更改, 则会被接受。 在表用户的 OnBeforePost 事件中也应发生相同的验证。

存储和显示密码也是错误的。 在这个演示的未来实施中, 我们将应用一些关于应用程序安全性的最佳实践。

Desktop Users Editor Form

桌面用户编辑器窗体

Touch Users Editor Form

触摸用户编辑器表单

编辑订单窗体

这个简单的销售点应用程序将用它们的项目记录订单, 它将标识客户。 数据库已经包含了一些数据, 但是为了能够管理所有必需的信息, 可以编辑客户和产品。 每个新订单记录它开始的时间 (创建)。 当订单完成并支付后, 用户按下按钮 (带付费的掩码), 并记录时间 (付费)。

请注意, 已支付的订单应为只读/只, 但在此实现中不强制执行。

Desktop Orders Editor Form

桌面订单编辑器窗体

Touch Orders Editor Form

触摸式订单编辑器窗体

报表销售

这是关于如何在 web 应用程序中使用快速报表的演示。 它演示如何修改快速报表设置, 以避免不必要的可视化弹出窗口, 在线程安全模式下执行它, 并根据需要消耗资源。

Desktop Sales Report

桌面销售报告

Touch Sales Report

触控销售报告


混合应用程序的编程

由于此演示应用程序已包括在演示 \ 触摸 \ PointOfSales 中, 本节仅涵盖每个单元或窗体的最相关的编程详细信息。


服务器模块

Server Module

它包含一个组件 TFDManager, 它在服务器模块创建期间变为活动状态。 这是读取全局连接字符串以访问共享数据库服务器并将其分配给只读属性的正确位置。

服务器模块

此代码的限制:

·连接字符串是硬编码的, 它应该来自一些外部源 (如 INI 或 XML 文件)

·除了文件系统安全性或对外部源的任何其他访问控制外, 任何关键信息都不应处于打开状态。 使用加密来增强保护。

unit ServerModule;

interface

uses
Classes, SysUtils, uniGUIServer, uniGUIMainModule, uniGUIApplication,

uIdCustomHTTPServer, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error,

FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def, FireDAC.Phys,

FireDAC.Comp.Client;

type
TUniServerModule = class(TUniGUIServerModule)
FDManager1: TFDManager;

procedure UniGUIServerModuleCreate(Sender: TObject);
private
FConnString : string;

protected

procedure FirstInit; override;
public

property ConnString : string read FConnString;
end;

function UniServerModule: TUniServerModule;

implementation

{$R *.dfm}

uses
UniGUIVars;

function UniServerModule: TUniServerModule;
begin
Result:=TUniServerModule(UniGUIServerInstance);
end;

procedure TUniServerModule.FirstInit;
begin
InitServerModule(Self);
end;

procedure TUniServerModule.UniGUIServerModuleCreate(Sender: TObject);
begin
FDManager1.Active := True;

FConnString :=

'DriverID=SQLite;Database=' +

FilesFolderPath +

'SQLite_Sales.db';
end;

initialization
RegisterServerModuleClass(TUniServerModule);
end.


主模块

Main Module

主数据模块包含多个与会话关联的对象:

1.与数据库的连接 (TFDConnection)

2.访问数据库的表或查询 (TFDTable、TFDQuery)

3.用于从数据绑定控件访问数据库对象的数据源。

4.行动清单 (TActionList)。

5.带有适配器 (TUniImageListAdapter) 的图像列表 (TUniNativeImageList), 用于使用操作列表中的图像。

使用以前的组件及其内部代码, MainModule 封装了此简单应用程序的大多数业务逻辑。

创建/销毁

每个会话都有自己的数据库连接。 连接字符串已从服务器模块中获得。

procedure TUniMainModule.UniGUIMainModuleCreate(Sender: TObject);
begin
Conn.ConnectionString := UniServerModule.ConnString;
Conn.Open;
end;

procedure TUniMainModule.UniGUIMainModuleDestroy(Sender: TObject);
begin
Conn.Close;
end;

登录过程

根据用户访问应用程序的方式, 应用程序将实例化桌面或触摸式登录表单。 输入用户名和密码后, 按 OK 将触发登录验证过程。

{ Login validation and session platform selection }

function TUniMainModule.Login

(

aUsername,

aPassword: string;

aDesktop : boolean

): boolean;
var
UserFound : boolean;
IsAdmin : Variant;
begin
Result := false;

IsAdmin :=
Conn.ExecSQLScalar
(
Format
(
'SELECT Admin FROM Users WHERE Username = ''%s'' AND Password = ''%s''',
[aUserName, aPassword]
)
);

UserFound := not VarIsClear(IsAdmin);
if UserFound then
begin
FUsername := aUsername;
FPassword := aPassword;

FIsAdmin := not VarIsNull(IsAdmin) and IsAdmin;

actEditUsers.Visible := FIsAdmin;

if aDesktop then
SetAsDesktop
else
SetAsMobile;

Result := true;
end;
end;

登录封装了两个业务规则: 如何识别有效用户 (组合用户名/密码), 以及什么样的用户可以看到操作 "编辑用户"。

平台选择

在尝试登录时, 每个登录窗体 (dLoginForm 和 mLoginForm) 都向 MainModule 所选平台报告。

{ Platform selection }

procedure TUniMainModule.SetAsDesktop;
begin
FServices := TdServices.Create;
end;

procedure TUniMainModule.SetAsMobile;
begin
FServices := TmServices.Create;
end;

选择平台后, 会话将知道如何与该平台上的用户进行交互。 此应用程序中使用的解决方案是在外观/接口后面对常见操作的不同实现进行分组, 并为所选平台实例化正确的方法。

The interface is IService:

unit Services;

interface

type

IServices = interface
['{CB442F06-D33F-488B-B643-22887D8F499B}']

procedure EditUsers;
procedure EditOrders;
procedure ReportSales(const aPDFUrl : string);
end;

implementation

end.

有两个实现 (分别用于桌面和触摸)。

Desktop

unit dServices;

interface

uses
Classes,
Services;

type

TdServices = class(TInterfacedObject, IServices)

procedure EditUsers;
procedure EditOrders;
procedure ReportSales(const aPDFUrl : string);
end;

implementation

uses
Vcl.Controls,
uniGUIForm,
_dUsersForm,
_dOrdersForm,
_dSalesReportForm;

{ TdServices }

procedure TdServices.EditUsers;
begin
dUsersForm.ShowModal();
end;

procedure TdServices.EditOrders;
begin
dOrdersForm.ShowModal();
end;

procedure TdServices.ReportSales(const aPDFUrl : string);
var
frm : TdSalesReportForm;
begin
frm := dSalesReportForm;
frm.PDFUrl := aPDFUrl;
frm.ShowModal();
end;

end.

Touch

unit mServices;

interface

uses
Classes,
Services;

type

TmServices = class(TInterfacedObject, IServices)

procedure EditUsers;
procedure EditOrders;
procedure ReportSales(const aPDFUrl : string);
end;

implementation

uses
Vcl.Controls,
uniGUImForm,
_mUsersForm,
_mOrdersForm,
_mSalesReportForm;

{ TdServices }

procedure TmServices.EditUsers;
begin
mUsersForm.ShowModal();
end;

procedure TmServices.EditOrders;
begin
mOrdersForm.ShowModal();
end;

procedure TmServices.ReportSales(const aPDFUrl : string);
var
frm : TmSalesReportForm;
begin
frm := mSalesReportForm;
frm.PDFUrl := aPDFUrl;
frm.ShowModal();
end;

end.

现在很明显, 两个实现实际上是相同的, 因为唯一的区别是实现接口中每个方法的类。 这是该解决方案中的一个重要限制, 因为存在重复代码, 这是一个糟糕的编程实践。 可能的解决方案之一是使用依赖项注入为给定平台定位正确的类, 并避免代码重复。

使用动作实现发射板

将用户界面与业务逻辑分离的最佳做法之一是使用操作。 主数据模块的操作列表具有以下操作:

1.编辑用户 (仅对管理员可用)

2.编辑订单

3.销售报告

无论目标平台使用什么样的视觉控制, 如果它支持操作, 它将能够触发选定的操作。 例如, 桌面主窗体在 TUniMainMenu 和 TUniToolBar 中公开这些操作, 而触摸主窗体会将它们显示在大按钮中。

用户界面未显式请求 MainModule 中的任何内容, 并且 MainModule 不知道用户界面的任何方式。

另一方面, 业务逻辑需要执行请求的操作, 这是创建接口服务的原因。

{ Launching pad business logic actions }

procedure TUniMainModule.actEditOrdersExecute(Sender: TObject);
begin
Services.EditOrders;
end;

procedure TUniMainModule.actEditUsersExecute(Sender: TObject);
begin
Services.EditUsers;
end;

procedure TUniMainModule.actReportSalesExecute(Sender: TObject);
var
PDFUrl : string;
begin
PDFUrl := ExportSalesReportToPDF;
if PDFUrl <> '' then
Services.ReportSales(PDFUrl);
end;

function TUniMainModule.ExportSalesReportToPDF : string;
var
dm : TfrDataModule;
begin
dm := TfrDataModule.Create(nil);
try
Result := dm.ExportReport;
finally
dm.Free;
end;
end;

procedure TUniMainModule.UniGUIMainModuleNewComponent(AComponent: TComponent);
begin
if AComponent is TfrxReport then
(AComponent as TfrxReport).EngineOptions.UseGlobalDataSetList := False;
end;

遵循最佳做法, 只有在询问报表时, 才会创建相应的数据模块, 并用于将报表导出到 PDF 格式。

将业务逻辑从用户界面移动到数据模块

用户的显式请求已经映射到了数据模块中的操作和处理。 其他用户请求到达数据模块作为与数据相关的事件, 应根据应用程序的业务逻辑进行处理。 但有一个隐含的业务逻辑, 需要一个明确的请求, 开放和关闭数据集使用的具体形式。

在这个应用程序中, 我们不会使用任何注册系统 (这可能与资源的具体形式或接口相关), 但只是要求数据模块支持的显式调用。

资源处理

{ Resource handling for forms }

procedure TUniMainModule.OnCreateOrdersEditor(Sender: TObject);
begin
tblCustomers.Open;
tblProducts.Open;
tblOrders.Open;
tblItems.Open;
end;

procedure TUniMainModule.OnDestroyOrdersEditor(Sender: TObject);
begin
tblItems.Close;
tblOrders.Close;
tblProducts.Close;
tblCustomers.Close;
end;

procedure TUniMainModule.OnCreateUsersEditor(Sender: TObject);
begin
tblUsers.Open;
end;

procedure TUniMainModule.OnDestroyUsersEditor(Sender: TObject);
begin
tblUsers.Close;
end;

每个需要数据模块资源的表单都将调用 UniMainModule. OnCreate < 形式 > (自) 和 OnDestroy < 表单 > (自), 以便数据模块设置并撕下所需的资源。 在前面的代码中, 数据模块打开并关闭每个窗体所需的数据集。

此特定代码只是服务器端业务逻辑, 可以通过使用依赖项注入来激活它。 它可以通过将资源注册到各自的 OnCreate 和 OnDestroy 方法来完成。

在更为复杂的情况下, 可以 "将" 组件 "注入" 具体形式。 例如, 可以在数据模块中定义应用程序 MainMenu, 某些泛型方法可以根据其类型为发件人窗体创建正确的等效项。

此类编程的目标是在用户界面和业务逻辑之间实现更大的脱节。 在查看每个窗体的实现时, 结果将会更晚。

与数据相关的事件处理

大多数业务逻辑属于此类别。 验证 OnBeforePost 中的更改、刷新信息或触发 OnAfterPost 中的其他操作、计算 OnCalcFields 中的虚拟字段等是典型的。

{ Data access event handlers }

procedure TUniMainModule.tblItemsAfterPost(DataSet: TDataSet);
begin
// After posting a change in items, force a recalculation of the order

tblOrders.Edit;
tblOrdersCalcFields(tblOrders);
tblOrders.Post;
end;

procedure TUniMainModule.tblItemsCalcFields(DataSet: TDataSet);
begin
if Conn.Connected and not tblItemsQuantity.IsNull and not tblItemsPrice.IsNull then
tblItemsTotal.Value := tblItemsQuantity.Value * tblItemsPrice.Value;
end;

procedure TUniMainModule.tblOrdersCalcFields(DataSet: TDataSet);
var
t : Variant;
begin
t :=
Conn.ExecSQLScalar
(
Format
(
'SELECT' +
' sum(i.Quantity * p.Price) ' +
'FROM' +
' Orders o' +
' INNER JOIN Items i ON o.ID = i.OrderID' +
' INNER JOIN Products p ON i.ProductID = p.ID ' +
'WHERE o.ID = %d',
[tblOrdersID.Value]
)
);

if VarIsNull(t) then
t := 0;

tblOrdersTotal.Value := t;
end;

执行操作

某些操作不是与数据相关的控件的标准, 它们在按钮、菜单项等上公开。

{ Business logic actions }

procedure TUniMainModule.actMarkAsPaidExecute(Sender: TObject);
begin
if tblOrders.State = dsBrowse then
tblOrders.Edit;

tblOrdersPaid.Value := Now;
tblOrders.Post;
end;

procedure TUniMainModule.actToggleAdminExecute(Sender: TObject);
var
AdminQty : integer;
begin
if tblUsers.State <> dsBrowse then
ShowMessage('Commit any pending udpate before requesting admin status changes!')
else
begin
AdminQty := Conn.ExecSQLScalar('SELECT count(*) FROM Users WHERE Admin = 1');
if tblUsersAdmin.Value and (AdminQty = 1) then
ShowMessage('You need at least one admin account!')
else
begin
tblUsers.Edit;
tblUsersAdmin.Value := tblUsersAdmin.IsNull or not tblUsersAdmin.Value;
tblUsers.Post;
end;
end;
end;

最后一项行动引起了关注。 如果业务逻辑要求至少有一个用户必须是管理员, 为什么只有在用户按下 "切换管理状态" 按钮时才会发生此验证？ 如果通过单击用户界面中的 "管理" 复选框来执行相同的操作, 将会发生什么情况？

解决前面的问题有两个选项: 使列管理员读/只, 或在两个地方执行此验证 (这里和 tblUsers. OnBeforePost)。


登录表单

Login Form

登录表单的代码几乎与目标平台相同, 但用户界面完全不同。 正如所料, 数据模块不知道差异。

Desktop Login Form

unit _dLoginForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIRegClasses, uniGUIForm, uniGUIBaseClasses, uniFieldSet,
uniEdit, uniPanel, uniButton;

type
TdLoginForm = class(TUniLoginForm)
UniFieldSet1: TUniFieldSet;
edtUsername: TUniEdit;
edtPassword: TUniEdit;
UniFieldContainer1: TUniFieldContainer;
btnOk: TUniButton;
btnCancel: TUniButton;
UniContainerPanel1: TUniContainerPanel;
procedure btnOkClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function dLoginForm: TdLoginForm;

implementation

{$R *.dfm}

uses
uniGUIVars, MainModule, uniGUIApplication;

function dLoginForm: TdLoginForm;
begin
Result := TdLoginForm(UniMainModule.GetFormInstance(TdLoginForm));
end;

procedure TdLoginForm.btnOkClick(Sender: TObject);
begin
if uniMainModule.Login(edtUsername.Text, edtPassword.Text, true) then
ModalResult := mrOk
else
ShowMessage('Invalid credentials!');
end;

initialization
RegisterAppFormClass(TdLoginForm);

end.

此表单使用字段集实现一致的可视化设计。

UniFieldContainer1 包含按钮 OK 和取消使用客户端对齐表与两个列。

UniFieldSet1 将使用客户端对齐形式呈现字段的用户名和密码。

触摸式登录表单

unit _mLoginForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUImClasses, uniGUIRegClasses, uniGUIForm, uniGUImForm,

uniGUImJSForm, uniGUIBaseClasses, unimFieldSet, uniEdit, unimEdit,

uniButton, unimButton;

type
TmLoginForm = class(TUnimLoginForm)
UnimFieldSet1: TUnimFieldSet;
UnimContainerPanel1: TUnimContainerPanel;
edtUsername: TUnimEdit;
edtPassword: TUnimEdit;
btnOK: TUnimButton;
btnCancel: TUnimButton;
procedure btnOKClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function mLoginForm: TmLoginForm;

implementation

{$R *.dfm}

uses
uniGUIVars, MainModule, uniGUIApplication;

function mLoginForm: TmLoginForm;
begin
Result := TmLoginForm(UniMainModule.GetFormInstance(TmLoginForm));
end;

procedure TmLoginForm.btnOKClick(Sender: TObject);
begin
if uniMainModule.Login(edtUsername.Text, edtPassword.Text, false) then
ModalResult := mrOk
else
ShowMessage('Invalid credentials!');
end;

initialization
RegisterAppFormClass(TmLoginForm);

end.

很明显, 这两种形式几乎是相同的。 这些组件也相似, 但它们中的每一项都针对不同的平台 (例如, TUnimContainerPanel 而不是 TUniContainerPanel)。


主要形式 (发射垫)

Main Form (launching pad)

 顶 以前 下

正如所料, 传统的 "桌面" 窗体公开应用程序的主要服务的可视化设计与最新的启用了触摸功能的用户界面不同。 第一个设计假设使用高精度设备, 鼠标, 而不是一个胖手指, 这是通常的 "设备" 的第二个。 在旧校用户界面中, 通过 "主菜单" 访问服务也很常见。

两个用户界面都可以访问相同的服务, 良好的实现应该保持用户界面代码的差异, 共享相同的业务逻辑。

桌面主窗体

unit Main;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIRegClasses, uniGUIForm, uniGUIBaseClasses, uniLabel,
uniButton, uniToolBar, uniBitBtn, Vcl.Menus, uniMainMenu;

type
TMainForm = class(TUniForm)
UniToolBar1: TUniToolBar;
btnEditUsers: TUniToolButton;
btnEditOrders: TUniToolButton;
btnReportSales: TUniToolButton;
UniMainMenu1: TUniMainMenu;
Main1: TUniMenuItem;
Exit1: TUniMenuItem;
Administration1: TUniMenuItem;
EditUsers1: TUniMenuItem;
Operations1: TUniMenuItem;
EditOrders1: TUniMenuItem;
Reports1: TUniMenuItem;
ReportSales1: TUniMenuItem;
Logoff1: TUniMenuItem;
procedure Exit1Click(Sender: TObject);
procedure Logoff1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function MainForm: TMainForm;

implementation

{$R *.dfm}

uses
uniGUIVars, MainModule, uniGUIApplication;

function MainForm: TMainForm;
begin
Result := TMainForm(UniMainModule.GetFormInstance(TMainForm));
end;

procedure TMainForm.Exit1Click(Sender: TObject);
begin
ModalResult := mrCancel;
end;

procedure TMainForm.Logoff1Click(Sender: TObject);
begin
ModalResult := mrOk;
end;

initialization
RegisterAppFormClass(TMainForm);

end.

此表单中只有两行代码是冗余的, 显示了注销和关闭会话之间的差异。 除了这两条可选代码行之外, 此窗体和 MainModule 之间的唯一链接是菜单项和工具栏按钮之间的链接。

视觉设计暴露在其结构中:

触摸主窗体

unit Mainm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIRegClasses, uniGUIForm, uniGUImForm, uniGUIBaseClasses,
uniLabel, unimLabel, uniButton, unimButton, uniToolBar, unimToolbar,
uniBitBtn, unimBitBtn, uniGUImJSForm, unimPanel, unimMenu;

type
TMainmForm = class(TUnimForm)
btnEditUsers: TUnimBitBtn;
btnEditOrders: TUnimBitBtn;
UnimContainerPanel1: TUnimContainerPanel;
btnReportSales: TUnimBitBtn;
private
{ Private declarations }
public
{ Public declarations }
end;

function MainmForm: TMainmForm;

implementation

{$R *.dfm}

uses
uniGUIVars, MainModule, uniGUIApplication;

function MainmForm: TMainmForm;
begin
Result := TMainmForm(UniMainModule.GetFormInstance(TMainmForm));
end;

initialization
RegisterAppFormClass(TMainmForm);

end.

此代码与上一代码的唯一区别是主菜单没有。

结构也类似。


用户编辑器窗体

此窗体使用网格和导航器添加、删除和编辑销售点的用户/操作员。 工具栏中有一个用于更改管理状态的按钮。 除了可视化控件之外, 在两个平台中的实现都是相同的。

桌面用户编辑器窗体

unit _dUsersForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm, uniBasicGrid, uniDBGrid, uniDBNavigator,
uniGUIBaseClasses, uniPanel, uniToolBar;

type
TdUsersForm = class(TUniForm)
dbnCustomers: TUniDBNavigator;
dbgCustomers: TUniDBGrid;
barTop: TUniToolBar;
btnToggleAdmin: TUniToolButton;
cntUsers: TUniToolButton;
procedure UniFormCreate(Sender: TObject);
procedure UniFormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function dUsersForm: TdUsersForm;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function dUsersForm: TdUsersForm;
begin
Result := TdUsersForm(UniMainModule.GetFormInstance(TdUsersForm));
end;

procedure TdUsersForm.UniFormCreate(Sender: TObject);
begin
uniMainModule.OnCreateUsersEditor(Sender);
end;

procedure TdUsersForm.UniFormDestroy(Sender: TObject);
begin
uniMainModule.OnDestroyUsersEditor(Sender);
end;

end.

它以前被提及作为最佳的做法。 此表单要求 MainModule 在开始时设置其环境 (OnCreate), 并在 OnDestroy 中进行同样的撕裂。 无论做什么, 都不是表示层的责任。

它的视觉设计是:

触摸用户编辑器表单

unit _mUsersForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUImClasses, uniGUIForm, uniGUImForm, uniGUImJSForm,
uniBasicGrid, uniDBGrid, unimDBListGrid, unimDBGrid, uniGUIBaseClasses,
uniDBNavigator, unimDBNavigator, uniToolBar, unimToolbar;

type
TmUsersForm = class(TUnimForm)
dbgUsers: TUnimDBGrid;
barUsers: TUnimToolBar;
btnToggleAdmin: TUnimToolButton;
UnimContainerPanel1: TUnimContainerPanel;
UnimToolButton1: TUnimToolButton;
dbnUsers: TUnimDBNavigator;
procedure UnimFormCreate(Sender: TObject);
procedure UnimFormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function mUsersForm: TmUsersForm;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function mUsersForm: TmUsersForm;
begin
Result := TmUsersForm(UniMainModule.GetFormInstance(TmUsersForm));
end;

procedure TmUsersForm.UnimFormCreate(Sender: TObject);
begin
uniMainModule.OnCreateUsersEditor(Sender);
end;

procedure TmUsersForm.UnimFormDestroy(Sender: TObject);
begin
UniMainModule.OnDestroyUsersEditor(Sender);
end;

end.

它是相同的代码 (通过 TUnim < > 控件更改 TUni < > 控件)。 它也是相同的结构。


订单编辑器窗体

名称是正确的, 但其含义涵盖了更多的功能。 没有能力管理客户和产品, 就不可能编辑订单。 编辑器使用网格来管理客户和产品, 以及用于编辑其项目的订单的主/详细网格对。

桌面订单编辑器窗体

unit _dOrdersForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm, uniGUIBaseClasses, uniPageControl, uniPanel,
uniBasicGrid, uniDBGrid, uniDBNavigator, uniSplitter, uniToolBar;

type
TdOrdersForm = class(TUniForm)
pc: TUniPageControl;
tsOrdersItems: TUniTabSheet;
pnlItems: TUniPanel;
dbnItems: TUniDBNavigator;
dbgItems: TUniDBGrid;
pnlOrders: TUniPanel;
dbgOrders: TUniDBGrid;
tsCustomers: TUniTabSheet;
dbnCustomers: TUniDBNavigator;
dbgCustomers: TUniDBGrid;
barCustomers: TUniToolBar;
barOrders: TUniToolBar;
btnPaid: TUniToolButton;
barItems: TUniToolBar;
tsProducts: TUniTabSheet;
barProducts: TUniToolBar;
dbnProducts: TUniDBNavigator;
dbgProducts: TUniDBGrid;
cntOrders: TUniToolButton;
dbnOrders: TUniDBNavigator;
cntItems: TUniToolButton;
btnItems: TUniToolButton;
btnCustomers: TUniToolButton;
cntCustomers: TUniToolButton;
btnProducts: TUniToolButton;
cntProducts: TUniToolButton;
procedure UniFormCreate(Sender: TObject);
procedure UniFormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function dOrdersForm: TdOrdersForm;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function dOrdersForm: TdOrdersForm;
begin
Result := TdOrdersForm(UniMainModule.GetFormInstance(TdOrdersForm));
end;

procedure TdOrdersForm.UniFormCreate(Sender: TObject);
begin
uniMainModule.OnCreateOrdersEditor(Sender);
end;

procedure TdOrdersForm.UniFormDestroy(Sender: TObject);
begin
uniMainModule.OnDestroyOrdersEditor(Sender);
end;

end.

此窗体使用 TUniPageControl 在不同的制表表中承载订单/物料、客户和产品。 它们中的每一项都与普通 VCL 应用程序中的工作几乎相同。 按照与 "用户编辑器" 表单相同的模式, 对 MainModule 只有两个请求, 其他所有内容都由事件处理程序和操作处理。

窗体的整个结构是:

触摸式订单编辑器窗体

unit _mOrdersForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUImClasses, uniGUIForm, uniGUImForm, uniGUImJSForm,
uniGUIBaseClasses, uniDBNavigator, unimDBNavigator, uniBasicGrid, uniDBGrid,
unimDBListGrid, unimDBGrid, uniToolBar, unimToolbar, unimCarousel,
MainModule;

type
TmOrdersForm = class(TUnimForm)
dbgOrders: TUnimDBGrid;
barOrders: TUnimToolBar;
btnMarkAsPaid: TUnimToolButton;
UnimCarousel1: TUnimCarousel;
cpOrders: TUnimCarouselPage;
pnlOrders: TUnimContainerPanel;
pnlItems: TUnimContainerPanel;
barItems: TUnimToolBar;
dbgItems: TUnimDBGrid;
cpCustomers: TUnimCarouselPage;
barCustomers: TUnimToolBar;
dbgCustomers: TUnimDBGrid;
cpProducts: TUnimCarouselPage;
barProducts: TUnimToolBar;
dbgProducts: TUnimDBGrid;
UnimToolButton1: TUnimToolButton;
dbnOrders: TUnimDBNavigator;
UnimToolButton2: TUnimToolButton;
UnimToolButton3: TUnimToolButton;
dbnItems: TUnimDBNavigator;
UnimToolButton4: TUnimToolButton;
UnimToolButton5: TUnimToolButton;
dbnProducts: TUnimDBNavigator;
UnimToolButton6: TUnimToolButton;
UnimToolButton7: TUnimToolButton;
dbnCustomers: TUnimDBNavigator;
procedure UnimFormCreate(Sender: TObject);
procedure UnimFormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

function mOrdersForm: TmOrdersForm;

implementation

{$R *.dfm}

uses
uniGUIApplication;

function mOrdersForm: TmOrdersForm;
begin
Result := TmOrdersForm(UniMainModule.GetFormInstance(TmOrdersForm));
end;

procedure TmOrdersForm.UnimFormCreate(Sender: TObject);
begin
uniMainModule.OnCreateOrdersEditor(Sender);
end;

procedure TmOrdersForm.UnimFormDestroy(Sender: TObject);
begin
uniMainModule.OnDestroyOrdersEditor(Sender);
end;

end.

与以前一样, 代码类似, 但组件不同。 在这种情况下, TUniPageControl 被一个 TUnimCarousel 所取代。 与网格的外观相似, 基于胖手指的功能需要对用户交互进行大的更改。 双击订单行后, 下列自动窗体弹出窗口:

请注意, 背景中有一个掩码覆盖网格 (告诉用户在关闭弹出式编辑器之前无法访问它们)。

单击该字段将会在屏幕底部显示典型的纠察框, 而不是为选择客户展示标准的查找组合键:

整个结构是:

当然, 这只是一个演示, 因为在数以百计的选择客户将成为一个问题, 使用这种方法。


销售报告表单

MainModule 已经包含了生成报告所需的大部分功能。 实际上, 使用数据模块 reportDM, 它在服务器上创建一个 PDF 报告。 "销售报表" 窗体仅根据目标平台显示 PDF 报告。

桌面销售报表窗体

unit _dSalesReportForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm, uniGUIBaseClasses, uniURLFrame;

type
TdSalesReportForm = class(TUniForm)
UniPDFFrame1: TUniPDFFrame;
private
procedure SetPDFUrl(const Value : string);
public

property PDFUrl : string write SetPDFUrl;
end;

function dSalesReportForm: TdSalesReportForm;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function dSalesReportForm: TdSalesReportForm;
begin
Result := TdSalesReportForm(UniMainModule.GetFormInstance(TdSalesReportForm));
end;

{ TdSalesReportForm }

procedure TdSalesReportForm.SetPDFUrl(const Value : string);
begin
UniPDFFrame1.PdfURL := Value;
end;

end.

完整的 PDF 查看器 (也可以下载、打印和执行其他操作) 已经是 uniGUI 调色板的一部分 (可用于两个平台)。 此窗体仅公开一个属性, 用于在显示 PDF 报告的文件名之前接收它。

触摸式销售报表

unit _mSalesReportForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUImClasses, uniGUIForm, uniGUImForm, uniGUImJSForm,
uniGUIBaseClasses, uniURLFrame, unimURLFrame;

type
TmSalesReportForm = class(TUnimForm)
UnimPDFFrame1: TUnimPDFFrame;
private
procedure SetPDFUrl(const Value : string);
public

property PDFUrl : string write SetPDFUrl;
end;

function mSalesReportForm: TmSalesReportForm;

implementation

{$R *.dfm}

uses
MainModule, uniGUIApplication;

function mSalesReportForm: TmSalesReportForm;
begin
Result := TmSalesReportForm(UniMainModule.GetFormInstance(TmSalesReportForm));
end;

{ TmSalesReportForm }

procedure TmSalesReportForm.SetPDFUrl(const Value : string);
begin
UnimPDFFrame1.PDFUrl := Value;
end;

end.


客户端脚本编写

uniGUI 的高级功能之一是为客户端事件编写 JavaScript 处理程序的能力, 即它允许为 uniGUI 控件的 Ext js 事件添加 js 代码。 Ext JS 是一个完整的 JavaScript 框架与丰富的组件集。 Ext JS 有它自己的对象层次结构。 与德尔菲对象一样, 每个 Ext JS 组件都是从父对象派生的。 虽然 javascript 不是直接面向对象的语言 (如对象帕斯卡), 但 javascript 中有几种方法允许开发人员使用面向对象的语言特征 (如继承) 创建对象层次结构。 例如, 在 JavaScript 中, 成员可见性与对象帕斯卡不同。 所谓的私有属性和方法在子类中仍然可见, 并且可以从对象实例访问。

在对 Ext JS 进行简短介绍之后, 让我们看看 uniGUI 中客户端脚本的要求是什么。 第一个要求是对 JavaScript 语言的公平了解。 JavaScript 是 c 类语言变体之一, 它的语法与 c 和 Java 非常相似。 另一个先决条件是熟悉 Sencha Ext. JS 库 API。 您仍然可以编写客户端代码, 很少了解 JavaScript 和 Ext JS API。 让我们展示一个简单的例子:

把 TUniButton 放在你的 MainForm 上。

Place a TUniButton on MainForm

把 TUniButton 放在 MainForm 上

在对象检查器中, 选择作为 ClientEvents 属性的子属性的 ExtEvents 属性。 双击 ExtEvents 将打开一个新窗口。

In Object Inspector double click on ExtEvents

在对象检查器中双击 ExtEvents

新窗口是 uniGUI JavaScript 事件编辑器。 在左侧, 您将看到所有可用的 Ext JS 事件, 与 TUniButton。 在左上角, 您将看到 Ext.button.Button 是在 TUniButton uniGUI 控件中使用的 Ext JS 组件。 如果双击这些事件, 将在编辑器中打开一个新的事件处理程序。 您可以开始键入自定义 JavaScript 代码, 以执行在客户端上所需的任何任务。

要访问 UniButton1, 必须使用特殊的语法。 任何 uniGUI 对象都可以使用所有者的名称及其名称 (< 所有者名称 > 来访问. < 对象名 >)。 名称必须在德尔菲 (但区分大小写) 中定义。 在我们的例子中, 我们有对象 UniButton1 拥有的 MainForm;访问它客户端的正确语法是 MainForm. UniButton1。

一旦您访问了对象, 就可以使用所有可用于 Ext.button.Button 组件的 Ext JS 方法。 因此, 虽然 UniButton1 是服务器端 TUniButton 类的实例, MainForm. UniButton1 是客户端 Ext.button.Button Ext JS 组件的一个实例。

setText() method of Ext.button.Button class

setText () Ext.button.Button 类的方法

上面的图片显示了Ext.button.Button类的 JavaScript setText ()方法。 在运行时可以调用此方法来更改按钮的标题。

In JavaScript editor choose 'click' event and double click.

在 JavaScript 编辑器中选择 "单击" 事件并双击。

如上所示, setText ()方法可用于更改按钮的标题。 再说一遍, 让我们强调 JavaScript 是区分大小写的, 并且控件名称必须与在德尔菲中声明的确切情况一起键入。

Run the application and click on the button

运行应用程序并单击按钮


Web 部署

目前, 有三个选项可用于将您的 uniGUI 项目部署到 Web 。

独立服务器

ISAPI 模块

Windows 服务


Sencha 许可证注意事项

FMSoft 公司。 有限公司是Sencha 公司的官方合作伙伴。 并被授予分配Sencha Ext JS和Sencha 触摸的OEM副本的权利。 UniGUI分发OEM Sencha Ext JS的部分副本。 您的uniGUI许可证授予您使用和部署Sencha Ext JS的权利。 但是, 您的OEM副本Sencha Ext JS只能与uniGUI结合使用。 您不得使用或分发OEM Sencha Ext JS除使用它开发和部署uniGUI项目以外的任何其他目的。 不能将此库视为独立库, 也不能用于基于它创建独立的软件产品。 Sencha Ext/Sencha 触摸作为uniGUI封装的一个集成部分分发。 Sencha Ext/Sencha 触摸不作为一个单独的产品分发, 你不被授予一个单独的许可证。


uniGUI 运行时包

uniGUI 运行时包是将 uniGUI 运行时文件分发到承载 uniGUI web 应用程序的服务器的简单方法。 运行时包可作为从 FMSoft 的客户门户下载。 此软件包不适用于 uniGUI 试用版。 安装运行时包后, web 应用程序将能够找到 Ext JS 和 Sencha 触摸所需的文件来运行您的应用程序。 它还包括运行时所需的其他 JavaScript 库文件。

必须确保已安装的运行时包与您用于编译 web 应用程序的 uniGUI 包的版本匹配。 例如, 如果已使用FMSoft_uniGUI_Complete_Pro_0.99.80.1267生成应用程序, 则还必须确保服务器上安装了FMSoft_uniGUI_Complete_runtime_0.99.80.1267。 您可以在服务器上安装运行时包的多个版本, 以便使用不同版本的 uniGUI 编译的 web 应用程序可以在同一服务器上进行协作。

使用运行时包进行部署需要所有路径设置及其默认值。 ExtRoot、 TouchRoot、 UniMobileRoot和UniRoot必须设置为默认设置。 见下:


调整路径

如果您不打算使用 uniGUI 运行时包部署应用程序, 因为您希望自己部署运行时文件, 则必须相应地调整路径设置。 对于uniGUI应用程序, 有一些基本路径必须经过调整才能在服务器上运行。

首先, 您必须确保 Web 应用程序知道Ext JS文件的位置。 为此, 在您的应用程序中ServerModule必须为ExtRoot分配正确的路径。 ExtRoot的默认值为 "[ext] ", 这意味着ext JS文件位于安装uniGUI 运行时软件包的位置: < InstallFolder > \ FMSoft \ 框架 \ uniGUI \ ext-x.y.z. 生成

如果在目标 PC 上未安装uniGUI 运行时包, 则必须为ExtRoot分配完整或相对路径。 如果分配相对路径, 则它将相对于ServerRoot , 您可以使用 ""。\..\..\ myfolder "" 部分路径表示法。

最简单的方法是将ExtRoot设置为"". \ [ext] \ ", 然后将"ext-x.y.x. 生成"文件夹复制到 web 应用程序的根文件夹 (应用程序可执行文件或 DLL 模块所在的位置)。 但是, 出于安全考虑, 最好将 "x.y.x. 生成" 文件夹放在另一个文件夹中, 并将所有 "x.y.x. 生成" 文件部署为只读。 在IIS下, 必须确保应用程序具有足够的凭据, 以便对 "x.y.x. 生成" 文件夹及其文件进行只读访问。

一些例子。

ExtJS 文件在:

ExtRoot = "[ext] " < 运行时包 InstallFolder > \ FMSoft \ 框架 \ uniGUI/分机-x.y.z. 生成 \ (* 推荐)

ExtRoot = "". \ [ext] \ " < 服务器根 > \ ext-x.y.z. 生成 \

ExtRoot = "" C: \ ExtJS \ [ext] \ "" C:\ExtJS\ext-x.y.z.build \

ExtRoot = "". ExtJS \ [ext] \ " < 服务器根目录 > \ ExtJS \ ext-x.y.z. 生成 \

ExtRoot = "". ExtJS \ ext \ " < 服务器根目录 > \ ExtJS \ ext \

在所有情况下, ext-x.y.x. 生成被翻译成一个字符串, 它代表了您的 ext JS 版本的正确文件夹。 例如, 如果您的 uniGUI 版本是基于Ext JS版本4.2. 5.1736则它将被转换为文件夹名称: . \ < 路径 > \ Ext-4.2. 5.1763 \

同样的原理也适用于uniRoot、 uniMobileRoot和TouchRoot的性质。

更多的例子。

uniGUI JS 文件在:

UniRoot = "[单] " < 运行时包 InstallFolder > \ FMSoft \ 框架 \ uniGUI/单向-x.y.z. 生成 \ (* 推荐)

UniMobileRoot = "[unim] " < 运行时包 InstallFolder > \ FMSoft \ 框架 \ uniGUI/unim-x.y.z. 生成 \ (* 推荐)

UniRoot = "". \ [单] \ " < 服务器根 > x.y.z. 生成 \

UniMobileRoot = "". \ [unim] \ " < 服务器根目录 > \ unim-x.y.z. 生成 \

注:在上述情况下, 如果"路径 \ x.y.z. 生成 \" "文件夹不存在, 则将使用" 路径 \ 单 \ \"文件夹。

字符串x.y.z. 生成对应于uniGUI版本。 如果您的 uniGUI 版本为0.99.80.1260,则翻译的文件夹名称将uni-0.99.80.1260.

如果将自定义 uniGUI 主题部署到服务器, 还可以为主题文件设置自定义文件夹。 可以为此使用UniPackagesRoot属性。

UniPackagesRoot的默认值为 "[unipack] ", 这意味着自定义主题放置在安装程序使用的默认位置。 可以通过为UniPackagesRoot属性指定不同的值来更改此位置。 请确保默认安装中的 "主题"文件夹已完全复制到目标文件夹:

主题在:

UniPackagesRoot = "C: \\ ext \\ [unipack] " ""C:\\ext\\unipackages hemes \\"" 文件夹

UniPackagesRoot = ""。\themefiles \\ "" "< 服务器根 > hemefiles hemes \\" "文件夹

UniPackagesRoot = "". \\ [unipack] \\ "" "< 服务器根目录 > \\ unipackages hemes \\" "文件夹

在ServerModule中还有两个路径参数: ServerRoot和CacheFolder。

ServerRoot:

ServerRoot定义所有相对路径的根路径。 空白值指向 "应用程序启动" 文件夹。

CacheFolder

uniGUI服务器需要一个文件夹来创建临时文件。 通常, 它是在模块存在的同一文件夹下创建的名为缓存的文件夹。 通过为CacheFolder参数指定不同的路径, 可以更改此默认位置。 在IIS下, 必须确保应用程序具有足够的凭据才能完全访问CacheFolder。

如果要在运行时更改这些属性, 请在UniServerModule的事件处理程序OnBeforeInit上执行此项。

程序 TUniServerModule.UniGUIServerModuleBeforeInit(发件人: TObject);
开始
ExtRoot: =' C: \\ 部署 \\ [ext] ';
UniRoot: =' C: \\ 部署 \\ [一] ';
结束;


独立服务器

独立服务器模式与 VCL 应用程序非常相似, 具有一些差异, 这使得它成为 Web 部署的更好选择。 在此模式下, 应用程序主窗体在桌面上不再可见, 而是将图标放在 Windows 任务栏中。

uniGUI Standalone Server

uniGUI 独立服务器

双击此图标将打开应用程序控制面板。 在下面您将看到控制面板的初始版本。

通过右键单击图标并选择 "关机", 可以关闭正在运行的服务器, 也可以打开 "服务器控制面板", 然后从顶部菜单中选择 "管理-> 关闭"。 。

uniGUI 中的高级功能之一是控制面板可从 web 访问。 您可以使用此 URL 访问控制面板:

http://mysite:port/server

默认图标可以通过更改德尔菲应用程序图标或为ServerModule-> 图标指定一个新图标来替换。

独立服务器是调试应用程序或在服务器可用性不十分重要时需要 web 服务器的好选择。 若要自动启动服务器, 必须在 Windows启动文件夹中为服务器可执行文件放置快捷方式。 在此模式下, 服务器将无法启动, 直到 Windows 用户登录。 对于严重的部署, 您必须选择ISAPI 模块或Windows 服务部署选项。


Windows 服务

Windows 服务像使用delphi创建的任何常规服务应用程序一样创建和部署。

要在命令行中安装服务类型, 请键入以下命令 *:

MyServiceApp -install

您可以从 Windows 服务管理器启动该服务, 或者键入以下命令:

net start ServiceName

创建新项目时, 服务名称的默认值为: UniServiceModule

你可以这样变更：ServiceModule.pas->UniServiceModule->Name

要卸载该服务:

MyServiceApp -uninstall

*请注意, 命令行提示符必须以管理权限启动, 才能运行以前的命令.


ISAPI 模块

将 Internnet 应用程序部署为 ISAPI 模块可能是最好的部署方法。 您可以在不需要将 TCP 端口专用于每个应用程序的情况下运行多个模块。

可以使用支持 ISAPI 扩展的任何 Web 服务器。 使用 iis 5.1、iis 6.0、iis 7.0 和 Apache 2.2 对 uniGUI 生成的 ISAPI 模块进行测试。

每个 Web 服务器的安装说明不同。 有关说明, 请参阅以下各节:

IIS 5。1

IIS 6。0

IIS 7。0

Apache 2。2


IIS 5.1

IIS 5

为了设置 IIS 5 uniGUI web 应用程序, 必须开始创建新的虚拟目录。

注意: 可以向 "默认网站" 或任何其他现有 "" 网站 "添加新的虚拟目录。

为新虚拟目录选择别名。

现在, 将文件夹分配给新创建的别名。

下一步是将必要的权限分配给虚拟目录。 需要执行权限。

创建新的虚拟目录后, 就可以部署 uniGUI 服务器了。将 ISAPI 模块和其他必需文件复制到虚拟目录中。 您必须确定ISS内置用户IUSR_ < ComputerName >和IWAM_ < ComputerName >有足够的权限访问您的虚拟目录和在 web 应用程序期间可能访问的其他文件夹执行。

例如, 如果 ISAPI 应用程序在 "文件夹C:\WebApps " 下, 则必须授予对该文件夹的完全访问权限。

可以通过浏览到此 URL 来加载 web 应用程序: http://本地主机/< virtualdirectory >/< modulename >. dll

还请参阅调整路径以了解更多信息。


IIS 6.0

IIS 6

在 IIS 6 中, 第一步是创建新的应用程序池。

给新池一个合适的名字。

在 "应用程序池属性" 的 "回收" 选项卡上, 取消选中 "回收工作进程" 选项。

在 "性能" 选项卡中, 取消选中 "空闲后关闭工作进程"..选项。

下一步是创建虚拟目录。

注意: 可以向 "默认网站" 或任何其他现有 "" 网站 "添加新的虚拟目录。

为新的虚拟目录分配别名。

选择 ISAPI 模块所在的文件夹。

确保对虚拟目录授予执行权限。

现在打开新创建的虚拟目录的属性, 并将默认池更改为在第一步中创建的池。

IIS 6 还有一个步骤。 您的 ISAPI 扩展必须添加到允许的扩展列表中。

为扩展名分配一个名称, 添加扩展的模块 DLL 文件, 并将设置的扩展状态检查为允许的选项。

创建新的虚拟目录后, 您就可以部署 uniGUI 服务器了。将 ISAPI 模块和其他必需文件复制到虚拟目录中。 您必须确定ISS内置用户IUSR_ < ComputerName >和IWAM_ < ComputerName >有足够的权限访问您的虚拟目录和在 web 应用程序期间可能访问的其他文件夹执行。

例如, 如果 ISAPI 应用程序在 "文件夹 C:\WebApps" 下,则必须对其进行完全访问。

还请参阅调整路径以了解更多信息。


IIS 7.0

IIS 7 和更高版本

类似于 iis 6, 在 iis 7 中, 第一步是创建一个新的应用程序池。

为池分配名称并选择 "无托管代码" 选项。

创建池后, 打开 "高级设置" 对话框并进行以下修改:

·将启用的32位应用程序设置为True。 (此选项在 Windows 的64位版本中可用)

·将 "禁用重叠循环" 设置为True。

·将规则时间间隔设置为0

现在添加一个新的应用程序。

注意: 可以向 "默认网站" 或任何其他现有 "" 网站 "添加新应用程序。

给它一个正确的名称, 并调整物理路径。 它是部署 DLL ISAPI 模块的路径。

选择 "管理应用程序" 并打开 "高级设置" 对话框。

在 "高级设置" 属性编辑器中, 将应用程序池设置为在第一步中创建的。

下一步是调整刚刚创建的应用程序的处理程序映射。

在处理程序映射屏幕中右击 ISAPI dll, 然后选择 "编辑功能权限"。

请检查执行选项, 然后按 "确定"。

如果在处理程序列表中未看到 isapi dll, 则必须确保 isapi 扩展安装在 Windows 功能中。 访问 Windows 功能以检查它是否已安装。

下一步是检查身份验证设置:

确保启用了匿名身份验证, 并设置了默认用户IUSR 。

最后一步是设置 web 应用程序物理文件夹的安全性。 如果IUSR在顶部列表中不可用, 则必须手动将其添加到列表中。 IUSR是一个内置的 Windows 用户帐户, 默认情况下是可用的。 您还需要对IIS_IUSRS用户组应用相同的访问权限。

还请参阅调整路径以了解更多信息。


ISAPI 和 CGI 限制

对于大于7.0 的 iis 版本, 需要额外设置, 以确保您的 ISAPI 模块可以由 IIS 服务器加载。 要调整此设置, 请打开 IIS 管理器, 并为根服务器选择 ISAPI 和 CGI 限制。 (请参见下图)

ISAPI and CGI Restrictions

ISAPI 和 CGI 限制

双击ISAPI 和 CGI 限制图标并打开模块设置。 这里有两种选择。 您可以单独添加每个 ISAPI 模块, 或者允许 IIS 在默认情况下运行所有未指定的 isapi 模块。

如果要在默认情况下启用所有 ISAPI 模块, 请选择 "编辑功能设置"..并检查允许未指定的 ISAPI 模块。

如果要一个接一个地添加模块, 请选择 "添加" 按钮并添加 ISAPI 模块, 如下图所示:


对应用程序池使用其他帐户

在前面的部分中, 我们始终使用应用程序池的默认 IUSR 帐户。 有些情况下, 您可能希望对池使用其他帐户。 在这种情况下, 您可以更改关联的系统帐户。 有时, 默认 IIS IUSR 帐户可能对 uniGUI ISAPI 应用程序所需的系统资源没有足够的访问权限。 无法访问这些资源可能会导致 ISAPI dll 停止工作。 为了避免这种情况, 您可能需要切换到其他帐户。

作为一般性建议, 如果您遇到 ISAPI 应用程序无法正常工作或在一段时间后挂起, 则可能需要切换到 Windows 内置本地系统帐户, 以查看它是否解决了问题。

要使用其他帐户, 请按照下列步骤操作:

打开 IIS 管理器并选择您的应用程序池。 在 "过程模型" 部分中选择 "标识":

在 "应用程序池标识" 窗体中, 选择首选帐户。 例如, 让我们选择网络服务系统帐户。

也可以选择本地帐户。

下一步是修改 web 应用程序的身份验证设置:

使应用程序池标识成为活动选项:


调整上载文件大小限制

IIS 7.0 和更高版本对内容大小进行了上限限制, 直接影响上载文件大小限制。 如果 IIS web 应用程序需要上载大文件, 则需要进行额外的调整。 调整此设置的最佳方法是使用 IIS 管理器编辑请求筛选功能设置和请求限制

下面是说明，

1.打开 IIS 管理器。

2.选择要配置的 web 应用程序。

3.确保您在 "功能" 视图中按经理底部的按钮进行查看。

4.选择请求筛选并通过双击图标打开它。 将显示 "请求筛选" 窗格。

5.从屏幕右侧的操作窗格单击 "编辑功能设置..." 链接。 将显示 "编辑请求筛选设置" 窗口。

6.在 "请求限制" 部分中, 输入适当的最大允许内容长度 (字节), 然后单击 "确定" 按钮。


Apache 2。2

Apache 2.2 用于 Windows 的 web 服务器允许运行 ISAPI 模块。 要启用此功能, 必须启用插件mod_isapi。

Apache 没有用于配置的可视界面。 您必须对httpd文件进行一些修改。

首先, 取消注释以下行:

LoadModule isapi_module modules/mod_isapi.so

接下来, 通过添加以下行来将dll文件与 ISAPI 模块关联:

AddHandler isapi-handler .dll

现在, 将您的模块目录添加到 Apache 目录条目中:

<Directory "C:/webapps">
Options Indexes FollowSymLinks ExecCGI
AllowOverride None
Order allow,deny
Allow from all
</Directory>

最后, 为目录创建一个新别名。

Alias /mywebapps "C:/webapps"

在对httpd文件进行必要的修改后, 请确保重新启动 Apache 服务器。


SSL 配置

uniGUI支持独立服务器和 Windows 服务应用程序的 SSL 协议。 isapi 模块要求为特定的 ISAPI 服务器配置 SSL;IIS、 Apache等。

配置 "总是" 的第一步是获取所需的证书文件。 在独立和 Windows 服务模式中, uniGUI使用 "印第" 作为底层 TCP 传输层。

您必须在此提供三个文件:

root.pem

cert.pem

key.pem

正如您所看到的, 文件是pem格式的。 Pem文件是人可读的base64 ASCII 文件, 可以在编辑器中打开和编辑。 pem 文件可能包含单个证书或多个证书。 为了与印第工作, 每个pem文件必须只包含一个证书。

下面的示例显示了pem文件的内容:

-----开始证书-----
MIIB8jCCAV gAwIBAgIQfjGd2Py0qZJGqdkPiRlDdjAJBgUrDgMCHQUAMBAxDjAM
BgNVBAMTBWVsaXRlMCAXDTEzMDYwMjE3NTA0OFoYDzIxMTMwNTA5MTc1MDQ4WjAQ
MQ4wDAYDVQQDEwVlbGl0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAt3pi
pMYHNzUueLZBb1eMrPop6Emta/KLyLaK94vlM1lV/6ITiuFtuSs9gq0s516s2th7
FUkBpgfQvrb 3b9h10WMca8MTbYrLGL dHqRk4jGt/8GAUeYHkKddk/NeXkZWqaD
3aMdURpTgE2iK/d86C1YdsxqXTxP Uax/eN4RUECAwEAAaNTMFEwFQYDVR0lBA4w
DAYKKwYBBAGCNwoDBDAtBgNVHREEJjAkoCIGCisGAQQBgjcUAgOgFAwSZWxpdGVA
RUxJVEUtUFJJTUUAMAkGA1UdEwQCMAAwCQYFKw4DAh0FAAOBgQCDSHm54tMh1sPY
aBrpZeZtbt9e1gPZ2B/Gd7U2KGK46yM8OQQ3LlnPaTc96q2ocD9sL3GP1B2itwX/
THOgUX7MpUipfUg6 8te6A7/gjiGyCf/OauJJrHal8p2QPwecGo3YnxUvTCu9gH
iGE3Yqxv/6YqgDjGnpNdAvvX9gEfQ==
-----结束证书-----

有两种类型的证书可以与您的 uniGUI 服务器一起使用。 您既可以使用证书颁发机构颁发的证书, 也可以使用自签名证书进行开发、测试和/或私有用途。


从颁发机构获得 SSL 证书

ssl 证书由专门颁发和承载 ssl 证书的公司发布。 为了获得这样的证书, 你需要签订合同, 并购买专门给你公司的证书。 此证书将被整个 web 所知道, 并将包含有关您的公司、证书颁发者和您的网站的信息。

Example for a secured web site

安全网站示例

首先创建一个OpenSSL (证书签名请求) 文件。 有几个工具可以创建 s CSR 文件。 OpenSSL是一种广泛用于该任务的工具。 创建 csr 文件后, 必须将其发送到证书颁发机构, 并验证您在 csr 文件中提供的信息。 如果验证成功, 他们将签署证书并颁发证书。

您的证书颁发机构将指导您完成创建 CSR 文件和接收证书的过程。

在成功的应用程序之后, 您将收到证书文件。 如上一节所述, 您需要三个文件与服务器一起分发:根. pem、证书 pem和密钥. pem。

注: 有时,证书 pem和密钥. pem进入一个文件。 在这种情况下, 您需要使用文本编辑器打开此文件, 并将各个证书保存在独立的pem文件中。


从 GoDaddy 获取 SSL 证书

免责声明: 我们不隶属于 GoDaddy。。 这里的示例仅用于使 uniGUI 开发人员熟悉为独立和 Windows 服务 uniGUI 应用程序设置 SSL 站点的实际步骤。

本节将介绍如何集成从GoDaddy获得的 SSL 证书与 uniGUI 独立或 Windows 服务服务器。 本指南将不包括 ISAPI dll 的 SSL 模式。 确保 ISAPI 站点的安全应该通过在 Microsoft IIS 中安装 SSL 证书 (超出本指南的范围) 来完成。

从选择适合您的应用程序的正确 SSL 计划开始。 可以为单个域、多个域或启用通配符的 ssl 证书获取 ssl 证书。 很可能, 单个域 SSL 证书对 uniGUI 服务器来说就足够了。

购买证书后, 创建证书签名请求或 CSR 文件。 此文件将包含证书的所有必要信息。 作为创建 CSR 文件的一部分, 还将为您的 SSL 证书创建一个私钥。 创建 CSR 的方法有多种。 这里我们描述了使用 OpenSSL 创建 CSR。

要使用 OpenSSL 工具创建证书签名请求, 请按照下列步骤操作:

启动具有管理权限的命令提示符。 在命令行类型中:

openssl req -new -newkey rsa:2048 -nodes -keyout key.pem -out req.csr

此命令将创建私钥, 并将其保存到名为key 的文件中。 此文件是您的证书私钥, 必须保存在安全的位置。 此密钥文件将与您的 SSL 证书一起使用。

在创建私钥后, OpenSSL 将开始询问有关您的证书的几个问题, 应仔细回答。

生成2048位 RSA 私钥

.........................................................................................

.............

正在将新私钥写入 "密钥. pem"

-----

您将被要求输入将被合并的信息

到您的证书请求中。

您要输入的内容称为 "可分辨名称" 或 "DN"。

有相当多的领域, 但你可以留下一些空白

对于某些字段, 将有一个默认值,

如果输入 ".", 该字段将留空。

-----

国家名称 (2 个字母代码) [AU]:TR

省/自治区名称 (全名) [某些状态]:安卡拉

地方名字 (例如, 城市) []:安卡拉

组织名称 (如公司) [互联网 Widgits 有限公司]:FMSoft

组织单位名称 (例如, 节) []:

公用名称 (例如服务器 FQDN 或您的姓名) []:www.elite-prime.com

电子邮件地址 []:farshad@fmsoft.net farshad@fmsoft 网

请输入以下 "额外" 属性

与证书申请一起发送

挑战密码 []:

可选公司名称 []:FMSoft

此命令将创建一个密钥. pem文件 (如上所述) 和一个要求的 csr文件。 请确保在询问公用名称时提供要保护的正确域名。 在我们的例子中, 我们要保护域名精英-素数. com。 如果 SSL 证书支持通配符, 请输入yourdomain.com。

下一步是将CSR文件的内容发送到您的注册服务商。 在我们的案例中, GoDaddy 将要求您将 CSR 文件的内容粘贴到其网站中的特殊备注区域中。 基本上, 任何 CSR 文件都是可以在编辑器中打开并复制其内容的文本文件。

在此步骤之后, GoDaddy 将检查和评估您的请求, 并指导您执行验证域名所有权所需的其他步骤。

当所有步骤都完成后, GoDaddy 将授予您在该域上使用新的 SSL 证书。 他们将引导您到可以下载证书文件的部分。

他们将要求您选择要与 SSL 证书一起使用的服务器类型。 在此步骤中, 选择 "其他" 并下载包含证书文件的 ZIP 文件。

ZIP 文件将包含两个文件。 根证书, 其名称类似于gd_bundle g2 g1. crt,以及像98d81da6dfe2095b. crt这样的站点证书文件。

最后一步是创建 uniGUI 所需的三个文件:证书 pem、根. pem和密钥. pem。

我们已经有钥匙了第一步创建的 pem。 如果您使用其他工具创建 CSR, 则会收到私钥文件。

很可能这个文件的格式正确, 你所要做的就是将它重命名为密钥. pem。

如果打开密钥文件, 它必须如下所:

-----开始私钥-----
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQC5I4QtWEREyDJM
ltWdd/2UL4j/s wZj81fFRJQkDn1X293VHwlIXuIgXI5v0ao3vYJupHaRR3YUm7y
H6 aGvhVI13fR7/M6QBJVO9RHmOFThBzHUYcIObT2CR4BSGkfmgJPX b45n8TIJf
NoX50WrXH0X9w8dBV0SX2dIT67caW/uVvjvGfKC62uPY71mWu ttRPwMeMbv0Kx1
BmqNAAtYVUiB2ZcpMfESaePX3vpAS9VS7i8NnRT/cPXsdNUcRbt2zp ooN22lNb
oRES8A65hyvwPBsfAUwKPnscqDZKJV4WsoAVYXkPIyglGglWq8hx7npC0nKNtEnX
aBex6XDVAgMBAAECggEBAKqFBdbl92sKoRldy75PqCV2as5pH0qj4lpzA/1j Z4A
mp/kFrv0TN17rIN3o1MimKZ5SqEpC8YUls83U/uyX4u7GVjvsr61F62df3W6X78

.

.

69efJH4sd0GO58FyFtN202FllBL9BCvTu6WWCC7SbMYsSe4GjuRvWTQuwloyrdwu

5HbTE0TYbQ1HkwqSASZvVF UgSg =

-----结束私钥-----

root. pem是您的根证书, 必须从您的 SSL 提供商那里获得。 对于我们的 GoDaddy 示例, 它位于文件gd_bundle g2 g1文件中。您可以在编辑器中打开该文件并检查其格式。 它必须从-----开始证书-----开始 。

只需将其重命名为root. pem。

-----开始证书-----
MIIE0DCCA7igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
MDAwMFowgbQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UE
.
.
.
91cxG7685C/b LrTW C05 Z5Yg4MotdqY3MxtfWoSKQ7CC2iXZDXtHwlTxFWMMS2
RJ17LJ3lXubvDGGqv QqG6EnriDfcFDzkSnE3ANkR/0yBOtg2DZ2HKocyQetawi
LXY2JtwE65/3YR8V3Idv7kaWKK2hJn0KCacuBKONvPi8BDAB
-----结束证书-----

证书 pem是您的网站证书。 您是从 GoDaddy 收到的, 文件名如98d81da6dfe2095b. crt。

将此文件重命名为证书 pem , 您就完成了。


生成自签名证书

如果不需要证书颁发机构 (如Verisign) 颁发的全局签名证书, 则此类证书很好。 您可以使用自签名证书进行开发, 或在 intranet 网络中或通过 internet 进行私人使用。 可以使用OpenSSL生成证书文件。

首先, 从这里下载并安装OpenSSL的 Windows 二进制文件 (我们建议下载二进制文件的精简版本)。 安装后, 打开命令提示符并按照说明操作 below:7yh h

a) 生成自签名的根证书。

如果已在 Windows 中安装了根证书, 则可以尝试导出它, 而不是生成新的。 只需转到控制面板, 然后单击Internet 选项-> 内容-> 证书。 选择要导出的根证书。 选择base64格式并选择要导出的文件夹和文件名。 这将导出您可以安全地重命名为. cer格式的根证书。pem。

也可以从头创建根证书。

启动具有管理权限的命令提示符。

第一步是创建自签名的根证书。

在命令提示符下发出以下命令:

openssl genrsa -out root.key 1024

这将创建一个新的根. 具有1024年位强度的密钥文件。 其他选项为2048和4096。 通常, 1024 位就足够了。

如果要使用密码创建根密钥, 请改用以下命令:

openssl genrsa-des3 出根键1024

下一步是对根证书进行自签名。

openssl x509 天365-新节点-键根. 键出根. pem

如果根密钥是用密码创建的, 请改用以下命令:

openssl x509 天365-新密钥根. 键出根. pem

现在, 系统将提示您提供签名证书所需的几个详细信息。 如果根. 密钥文件是在第一步中使用密码创建的, 则还会提示您输入密码。

输入密码短语为根键:

您将被要求输入将被合并的信息
到您的证书请求中.
您要输入的内容称为 "可分辨名称" 或 "DN".
有相当多的领域, 但你可以留下一些空白
为某些字段将有默认值,
如果输入 ".", 该字段将留空.
-----
国家名称 (2个字母代码) [AU]: TR
州或省名称 (全名) [某些国家]: 安卡拉
地方名字 (例如, 城市) []: 詹卡亚
组织名称 (如公司) [互联网 Widgits 有限公司]: FMSoft
组织单位名称 (如, 节) []: RD
常用名称 (例如, 您的姓名) []: Farshad Mohajeri
电邮地址 []: info@fmsoft.net info@fmsoft 网(&D)

注: 365是证书有效期的天数。

这将在当前文件夹中放置一个新的根. pem文件。 此文件将在您的 uniGUI 服务器中使用。

b) 生成自签名密钥。

下一步是生成自签名密钥。 此步骤将生成密钥. pem和证书 pem文件。

在命令提示符下发出以下命令:

openssl req -x509 -days 365 -nodes -newkey rsa:1024 -keyout key.pem -out cert.pem

再次, 你会被问几个问题。

将 "屏幕" 加载到随机状态完成
生成一个1024位 RSA 私钥
........
.....
正在将新私钥写入 "密钥. pem"
输入 PEM 密码短语:
验证-输入 PEM 密码短语:
-----
您将被要求输入将被合并的信息
到您的证书请求中.
您要输入的内容称为 "可分辨名称" 或 "DN".
有相当多的领域, 但你可以留下一些空白
为某些字段将有默认值,
如果输入 ".", 该字段将保留为空.
-----
国家名称 (2个字母代码) [AU]:TR
省/自治区名称 (全名) [某些状态]:安卡拉
地方名字 (例如, 城市) []:詹卡亚
组织名称 (如公司) [互联网 Widgits 有限公司]:FMSoft
组织单位名称 (例如, 节) []:RD
公用名称 (例如服务器 FQDN 或您的姓名) []:Farshad Mohajeri
电邮地址 []:info@fmsoft.net info@fmsoft 网(&D)

要用密码创建相同的密钥, 请使用以下命令:

openssl req -x509 -days 365 -newkey rsa:1024 -keyout key.pem -out cert.pem

这一次您必须输入密码:

Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

此密码应分配给 SSL-> SSLPassword 参数 UniServerModule。 (请参阅SSL 配置)

完成上述所有过程后, 您将得到三个名为root. pem、密钥的 pem和证书 pem , 这些文件是在SSL模式下安装和运行项目所必需的。 这些文件必须放在与服务器可执行二进制程序相同的文件夹中。


配置 SSL 参数

将所有三pem文件复制到uniGUI服务器可执行文件部署的文件夹中。

您还需要OpenSSL标准DLL库文件。

libeay32.dll
ssleay32.dll

这些DLL文件作为OpenSSL安装的一部分分发。 还可以在文件夹下找到最新版本的OpenSSL DLL文件:

[UniGUI Installation Folder]\..\Framework\uniGUI\SSL\dll\x86

[UniGUI Installation Folder]\..\Framework\uniGUI\SSL\dll\x64

根据部署应用程序的方式, 您可以在 Windows 系统文件夹 (如C:\Windows\System32) 下复制它们, 或者将它们放在与uniGUI服务器相同的文件夹中。

注:有可能这些 DLL 文件已经由其他程序安装, 并且已经存在于您的系统路径中。 在这种情况下, 请确保您具有最新版本。

现在您需要将服务器配置为使用 SSL。 打开项目的ServerModule并进行以下更改:

SSL->Enabled = True
SSL->SSLOptions->CertFile = cert.pem
SSL->SSLOptions->KeyFile = key.pem
SSL->SSLOptions->RootCertFile = root.pem

如果您使用密码保护了密钥文件, 则需要在此处分配它:

SSL->SSLPassword = [Password]

confgure 的下一个参数是 SSL 端口。 可以将其保留为默认值0.在这种情况下, 您可以通过在默认情况下为8077的ServerModule > 端口配置的端口访问服务器。

https://localhost:8077

或者, 您可以使用默认端口为443的 SSL。 因为443是默认值, 您可以省略地址中的端口号:

https://localhost

在这种情况下, 您需要将端口属性设置为443。

ServerModule->Port = 443

通常, 您只希望为安全的 web 应用程序使用https协议, 但如果出于任何原因, 您同时需要同一站点的http和https协议, 则可以通过为每个协议分配不同的端口来启用它们。

请考虑以下配置:

ServerModule-> SSL-> SSLPort = 443

ServerModule->Port = 8077

在上面的配置中, 您可以从端口8077访问标准http协议, 并通过端口443来访问https协议。

如果为 SSL 选择默认端口443 , 请确保IIS或任何其他 web 服务器软件都不侦听此端口。


调整文件夹访问权限

uniGUI web 服务器需要访问多个文件夹, 才能在服务器生存期内读取、写入或创建文件。 许多此类文件将在缓存文件夹下创建, 用于存储所有 uniGUI 临时文件 (包括会话临时文件)。 根据部署方法, web 服务器将绑定到不同的 Windows 用户帐户。

·对于独立服务器, 此帐户将是当前的 Windows 用户帐户。

·对于 Windows 服务, 这将是本地系统、本地服务或网络服务帐户之一。 它还可以是任何其他手动配置的用户帐户。

·对于 ISAPI 模块, 此帐户将是 IUSR、IIS_IUSRS、IUSR_ < ComputerName > 或 IWAM_ < ComputerName > 内置帐户之一。 实际帐户名称在不同的 IIS 版本之间有所不同。

o注意: 如果您使用了其他帐户进行应用程序池身份验证, 则必须对该特定帐户授予文件夹访问权限。 在正常情况下, 如果您使用了本地系统帐户, 则它必须已经对您的应用程序文件夹具有完全访问权限, 因此不需要进行其他调整。

·对于 IIS 7.0 和更高版本, 启用匿名身份验证方法时使用 IUSR 帐户。 有关详细信息, 请参阅ISAPI 模块部分。 IIS_IUSRS 是所有 IIS 用户的组, 因此需要对 IUSR 和 IIS_IUSRS 都授予对文件夹访问权限的任何更改。

确定正确的 Windows 帐户后, 向文件夹授予必要的权限。

必须向 web 应用程序物理文件夹授予完全权限。 转到 "安全" 选项卡, 并将正确的用户帐户添加到帐户列表中。 例如, 如果要调整 ISAPI 模块的访问权限, 则必须为 web 应用程序物理文件夹及其子文件夹授予对 IUSR 帐户的完全访问权限:

在 IIS 7.0 和更高版本中, 还需要为用户组 IIS_IUSRS 添加访问权限。


使用第三方调试工具

市场上有几种工具帮助开发人员广泛地调试其应用程序。 通过使用这些工具, 您可以在代码中出现错误时获得详细报告。 这些报告包括错误的准确地址、源代码行号、堆栈跟踪等。 EurekaLog是这种工具之一。 在下一节中, 我们将尝试解释如何使用 EurekaLog 来调试 uniGUI 应用程序。


EurekaLog

EurekaLog是德尔菲应用程序的高级调试工具。 根据 EurakaLog 网站: "" EurekaLog 是异常跟踪工具, 它使您的应用程序能够捕获代码中的所有 bug, 并使用调用堆栈生成详细的日志, 其中包括单元、类、方法和行号信息.。 此信息还会记录到文件中, 并且可以选择通过 Internet 转发给您 (应用程序开发人员)。

大多数情况下, 当应用程序中出现异常时, 您将在客户端屏幕上看到一条消息, 并且在 uniGUI 日志文件中也会显示日志行。 只有当已知异常类型并且可以重复重现时, 此信息才会有用。 如果异常是随机 AV (访问冲突) 或很少发生的任何其他异常, 则可能很难找到错误的确切来源。 EurekaLog 将帮助您找到源代码中触发异常的确切行号。

EurekaLog 在配置和应用程序中进行了一些更改后, 可以与 uniGUI 应用程序一起工作。 安装 EurekaLog 后, 打开 uniGUI 项目并执行以下操作:

转到 "项目-> EurekaLog 选项"。

选择 "导入", 然后选择 "在" 下可用的uniGUI文件. \ uniGUI \ Utils \ EurekaLog \文件夹。

导入完成后, 按 "确定" 继续。

在德尔菲中, 打开您的朝鲜文件并进行以下更改:

程序Project1doc;

使用
{$IFDEF EurekaLog}
EMemLeaks,
EResLeaks,
ExceptionLog7,
{$ENDIF EurekaLog}
EAppUniGUI, //添加此行
形式,
ServerModule在 ServerModule. pas " {UniServerModule: TUniGUIServerModule},
MainModule在 MainModule. pas " {UniMainModule: TUniGUIMainModule},
主要在 "Main. pas" {MainForm: TUniForm},
Unit1在 Unit1. pas ' {UniForm1: TUniForm};

{$R *. res}

开始
ReportMemoryLeaksOnShutdown:=真实;
应用.初始;
TUniServerModule.创造(申请);
应用.跑;
结束.

通常, EurekaLog 只将这些文件添加到朝鲜:

{$IFDEF EurekaLog}
EMemLeaks,
EResLeaks,
ExceptionLog7,
{$ENDIF EurekaLog}

您需要手动添加EAppUniGUI , 并将该文件保留如下:

使用
{$IFDEF EurekaLog}
EMemLeaks,
EResLeaks,
ExceptionLog7,
{$ENDIF EurekaLog}
EAppUniGUI,
形式,
ServerModule在 "ServerModule" {UniServerModule: TUniGUIServerModule},
MainModule在 "MainModule" {UniMainModule: TUniGUIMainModule},
主要在 "Main. pas" {MainForm: TUniForm},

注意: 根据您的 EurekaLog 版本, 条件定义{$IFDEF EurekaLog}可能不会添加到您的源代码中。

为了测试 EurekaLog 功能, 请添加一个有意引发 AV 异常的代码:

程序TMainForm.UniButton2Click(发件人: TObject);
var
P:指针;
开始
P:= 零;
PInteger(P) ^ : = 1; 在这里加注 AV
结束;

运行应用程序后, 当您按 Button2 时, 您将在屏幕上看到一个简单的 AV 消息, 但这次日志文件将在 EurekaLog 文件夹下创建 (在与应用程序相同的文件夹中创建)。

此日志文件可以由 EurekaLog 查看器打开:

您还可以在 web 应用程序客户端屏幕上显示更详细的错误日志。 要启用此功能, 请转到 EurekaLog 选项并选择对话框。

在对话框中, 检查详细模式。

在详细模式下, 您的错误窗口将显示有关堆栈跟踪的更多信息:

通过 EurekaLog 提供的详细日志的帮助, 您可以找到代码中引发异常的确切位置。 这将帮助您节省许多小时的调试。

重要说明:

EurekaLog 是在代码中查找 bug 的强大工具。 建议只在开发和早期生产阶段使用它。 不建议在生产环境中使用 EurekaLog 和其他类似工具。 这样的工具使用低级代码注入技术来调试您的应用程序, 因此它们可能会使您的应用程序更慢, 并会产生一些不稳定问题。

如上所述, 在您决定将其部署到生产环境中后, 必须在应用程序的稳定版本中从代码中删除 EurekaLog。

有关 ISAPI/独立组合项目的说明:

当项目是组合项目并且启用 EurekaLog 时, 项目只能在这些模式之一 (ISAPI 或独立) 中识别 EurekaLog。

发生这种情况的原因是 EurekaLog 与 uniGUI 组合项目文件不兼容。 要解决此问题, 您可能需要为 ISAPI 和独立模式创建单独的项目。

通过创建新的 ISAPI 项目并将窗体和模块导入此项目, 可以实现此目的。 一旦有了单独的项目, 就可以为每个项目添加和启用 EurekaLog。


压力测试工具

本章将介绍 uniGUI 压力测试工具, 它是检测 web 应用程序中的可伸缩性问题的非常有用的工具。


介绍

可伸缩性是开发 web 应用程序时的主要关注之一。 每个 web 应用程序都必须能够为多个用户提供服务。 用户的数量可以根据应用程序的类型而变化。 为小型 intranet 设计的应用程序最多可以是10-50 个并发用户, 而 internet web 应用程序必须同时为500-1000 多个用户提供服务。

为了确保 web 应用程序可以在多用户生产环境中进行部署, 您应该执行多个测试。 首先, 分析资源使用情况。

可伸缩性意味着在实际的世界系统中会话数量增加时, 项目的扩展能力。 可伸缩的 web 应用程序应该使用几种技术以最有效的方式管理和使用系统资源。 例如, 如果每个会话占用 1000 KB 内存 (~ 1 mb), 则运行400会话将需要至少 400 MB 的物理/虚拟内存才能正常运行。 但是, 如果每个会话消耗的时间超过 10MB, 那么当有超过100个活动会话时, 应用程序就很容易耗尽内存。 需要考虑其他系统资源, 如 CPU、磁盘空间和数据库连接。

对于开发人员来说, 在桌面上模拟生产环境可能不是一件容易的事情。 您的应用程序可以运行完美的服务几个会话, 但可能会开始遭受耗尽资源和可伸缩性问题时, 会话数量增加超过一定的门槛。 uniGUI 包括用于处理此类可伸缩性问题的压力测试工具。 此工具旨在模拟桌面上的多用户环境。 压力测试工具位于.....。 uniGui \ Utils \ StressTestTool文件夹, 它是用完整的源代码部署的, 以便开发人员可以根据需要自定义它。 此工具引入了许多高级功能, 使开发人员能够在繁重的负载下模拟生产环境。


使用

下面是压力测试工具的主要形式:

使用此工具, 您可以记录一个 web 会话, 然后再播放它以模拟生产环境中的多用户行为。 压力测试工具将每个测试环境存储在项目文件中, 以后可用于重复测试。 项目保存所有记录的会话和测试参数。 为每个 uniGUI 项目创建单独的压力测试项目是很好的。 uniGUI 压力项目存储在 "项目" 文件夹下, 扩展名为 (*. uprj)


录制会话

录制会话是一项简单的任务。

使用压力测试时你要先设置 ServerModule->Options->soAllowSessionRecording = True. 一旦应用程序准备好生产, 此参数必须设置为 False。

§生成并运行您的 uniGUI 服务器。

§在压力测试工具中, 将Web 应用程序的 url设置为服务器的 url 地址。 如果服务器在同一台计算机上运行, 则典型的地址将http://127.0.0.1:8077

§按 "记录 [image] " 按钮。 这将启动一个新的浏览器窗口, 它将显示您的 web 应用程序的主要形式。 此操作将使用您的系统默认 web 浏览器。

会话重新编码

§在此阶段, 您可以通过在菜单、窗体、网格等中导航来开始使用 web 应用程序。所有的行动都将被记录下来。

§您可以继续录制此会话, 直到您认为应用程序的菜单、窗体、网格和功能的覆盖率足够。

§完成后, 可以终止 web 应用程序。

§现在切换到压力测试工具, 然后按 "停止 [image] " 按钮。

§这将完成录制会话并将其添加到此项目的可用记录列表中。

§您可以使用 "保存" 按钮 [image] 保存项目。

重要:要对压力测试工具使用记录的会话, 它应该是可重复的。 换言之, 所有记录的操作都应始终生成相同的 HTML 内容。 例如, 按 Button1 应始终打开 Form1 并选择菜单项 User1 应始终显示名为 TUserFrame 的框架。 通常, 99% 的 web 应用程序的情况下, 每个用户操作都会产生相同的输出。 某些用户操作可能不会产生可预测的结果。 在 DBGrid 中插入行就是一个例子。 当用户插入新行时, 它会创建一个新的网格行, 也会新建一个网格页 (插入一定数量的行之后)。 因此, 此操作可以生成不同的 JavaScript 内容, 这是不可预知的, 并且依赖于所连接的数据库表中当前的行数。 因此, 建议在记录会话时避免将数据插入 DBGrid。 同样, 在会话回放过程中, 删除或编辑行可能无法正常运行。 请考虑您的表包含100行, 并且在录制时删除一行。 当您播放同一个会话的100个实例时, 它们都将尝试删除同一行, 而只有一个会话可以删除它。


运行压力测试

通过播放以前录制的会话来运行压力测试是一项高级功能, 它将帮助您模拟实时多用户环境。 在您有一个或多个记录的会话之后, 您可以继续进行实际的压力测试过程。

通过将已录制的 Ajax 请求序列发送到服务器, 压力测试工具将播放会话。 Ajax 请求将模拟许多用户使用 web 应用程序的环境。 重要的是要知道, 回弹进程不会在真正的浏览器窗口中运行您的 web 应用程序。 模拟是通过将先前记录的 Ajax 数据包以记录会话时生成的确切顺序发送到服务器来执行的。

要运行压力测试, 必须按照下列步骤操作:

·请确保在 IDE 和调试模式下不运行压力测试工具.。 在调试模式下运行将大大降低压力测试工具的速度和性能。 最好是编译它并直接运行可执行文件。

·要录制和回放移动会话, 必须显式添加 "/m"'到应用程序 Url 的末尾.

§如果没有可用的记录会话, 则加载保存的项目或记录新的会话。 (请参阅上一节以查看如何记录会话)。

§调整各种测试参数:

·每次运行的会话总数:这是将在每次运行中创建的会话数。 建议从低值开始, 并逐步增加。 此值应为小于或等于ServerModule-> ServerLimits-> MaxSessions属性的值。

·最大工作线程数: 用于与服务器通信的辅助线程数。 此值应小于或等于ServerModule-> ServerLimits-> MaxRequests属性。

·运行的总次数:播放录制的会话的总次数。

·运行之间的延迟:在新运行开始之前等待的秒数。

·回放速度:使用此参数可以调整模拟会话的速度。 如果选择 "最大速度", 则不会在 Ajax 调用之间插入延迟, 因此您的会话将以最大速度播放。 您也可以选择实时, 这将尝试模拟回放接近记录会话的原始时间。

§按下 [image] 按钮启动服务器监视器。

§现在, 您可以按 "开始 [image] " 按钮开始实际的压力测试。

§通过分析服务器监视器上显示的实时信息, 开始监视服务器:

·会话:它显示在您的 uniGUI 服务器中运行的会话数。 峰值值显示在服务器生存期内运行的会话的最大数目。

·要求:服务器当前正在处理的挂起 Ajax 请求的数量。

·清除的文件/文件夹:这些文件或文件夹不再属于会话, 但尚未删除。 框架将逐步删除后台线程中清除的文件和文件夹。 在正常情况下, 此值最终将变为零。

·字节:在服务器生存期内发送/接收的字节数。

·正常运行时间:服务器启动后的时间。

·内存:在进行压力测试时, 这是你需要经常注意的重要信息之一。 它将显示服务器应用程序消耗的内存总量。 重要的是, 特别是当您的服务器是一个32位应用程序时。 尽管32位应用程序在理论上可以消耗多达 2 gb 的 RAM, 但实际上, 在 1.0-1.1 GB 以上的任何内存使用值都意味着您的应用程序处于危险区域中。 这是因为 uniGUI 应用程序使用的内存将随着时间的推移而支离破碎。 为此, 出于其他原因, 德尔菲内存管理器不能对32位 uniGUI 服务器使用总共 2.0 GB 的 RAM。 当您的应用程序内存耗尽时, 它将开始记录 "内存不足" 消息, 并且在启动新会话时用户也会看到此警告。

这是一个严重的情况, 可能会产生不可预知的结果, 最终导致服务器崩溃。 如果您关心内存使用情况, 并且应用程序将使用超过1GB 的 RAM, 最好使用64位服务器。 在64位服务器中, 不太可能获得 "内存不足" 错误, 因为即使在使用物理 RAM 时, Windows 也会切换到虚拟内存, 这受硬盘上可用的交换空间的限制。 因此, 在64位服务器中, 您可以使用比可用物理内存更多的内存。 当然, 必须注意的是, 虚拟内存比物理内存慢得多, 您会发现 web 应用程序的性能和响应能力有很大的下降。

o内存 (内部/进程):内部内存是 uniGUI 应用程序内部使用的内存量。 进程内存是 Windows 进程 uniGUI 应用程序所消耗的总内存。 例如, 如果您的应用程序是 isapi DLL, 则主进程是一个 isapi 工作进程, 它使用的内存比应用程序本身所使用的内部内存多。 如果您的应用程序依赖于诸如迈达斯这样的外部 DLL 库, 则进程使用的内存将高于内部使用的内存。

·CPU 负载:这将显示当前正在使用的 CPU 功率量。 如果您的 cpu 有多个内核 (这是所有现代 cpu 的情况), 该值将显示所有 cpu 内核的总 cpu 使用率。 空闲的 uniGUI 服务器将使用非常低的 CPU 刻度, 这样当 OS 和 uniGUI 应用程序都闲置时, cpu 使用率不应超过1%。

·GDI 句柄:GDI 句柄是受操作系统限制的有限系统资源。 uniGUI 优化, 以保持 GDI 处理使用在一个非常低的水平。 如果您的应用程序使用依赖 GDI 句柄的组件, 则必须检查该值以查看它是否保留在安全范围内。 典型的 uniGUI 应用程序在空闲状态 (没有挂起的请求) 时, 即使有数以百计的活动会话, 也不会消耗超过100个 GDI 句柄。 uniGUI 的设计方式是, 只要不需要 GDI 句柄就会返回到 OS。 如果您看到 gdi 句柄不断增加, 则必须查找可能泄漏 gdi 处理或过度使用它们的组件。 当应用程序用尽 GDI 句柄时, 它将开始记录日志文件中的 EOutOfResources 异常。 这是一个严重的问题, 最终会导致服务器崩溃。

§另一个重要步骤是监视 web 应用程序日志文件。 压力测试在后台运行, 没有任何 GUI 输出, 日志文件是查看应用程序是否正常运行的唯一方法。 它也是查看运行压力测试时生成的任何错误日志的唯一方法。 日志文件放置在应用程序的二进制文件所在的. \ log文件夹下。 每个日历日都有一个单独的日志文件。 必须在文本查看器/编辑器中打开日志文件, 查看是否存在任何意外日志, 如访问冲突或其他重要异常。 通过仔细分析日志文件, 您可以更好地了解应用程序的稳定性和可伸缩性。 如果需要更详细的日志, 可以使用更高级的调试工具 (如EurekaLog).。

§重要:您必须考虑到每个压力测试在同一 URL 上多次运行同一线程中的同一记录会话;在测试服务器时, 必须将ServerModule > SessionRestrict参数设置为srNone 。


服务器防洪保护注意事项

压力测试工具将在很短的时间内发送大量的请求。 所有这些请求都从相同的 IP 发送到正在测试的 uniGUI 应用程序。 您的服务器 OS 可能将此大量请求解释为洪水 DOS (拒绝服务) 攻击, 并开始阻止此类请求。 当您从远程 PC 上运行压力测试工具时, 它在以最大速度设置的播放速度为长周期时更加可疑。 由于洪水检测算法从 os 到 os 都不同, 而且高度依赖于它们的特定配置和服务器软件, 因此很难预测何时会发生这种阻塞。 当服务器启动阻止请求时, 您将在压力测试工具的右侧备注中注意到许多 HTTP 错误消息。 这将表明请求被阻止并且没有达到目标。

例如, 在 IIS、独立 EXE 和 Windows 服务中运行 uniGUI 应用程序作为 ISAPI 模块时, 行为将有所不同。 在远程 PC 上运行压力测试工具可能会引发洪水保护, 同时在同一桌面上运行 uniGUI 应用程序和压力测试工具都应该在不触发任何防洪机制的情况下工作。

除了服务器操作系统之外, 某些第三方保护软件 (如卡巴斯基、赛门铁克等)可能会为服务器 IP 通信添加其他层的保护, 这可能会禁止来自压力测试工具的快速和连续请求。 在这种情况下, 您需要通过将播放速度设置为实时和/或在运行之间放置更长的延迟以允许请求通过反洪水检测层来减慢回放速率。


其他设置

根据性质,压力测试工具每秒创建数以百计的 HTTP 请求, 以便在测试的服务器上装载繁重的负载。 这可能超出了运行压力测试工具的操作系统的限制。 这些限制之一是默认情况下操作系统限制为特定值的 TCP 端口。 为了使压力测试工具顺利运行, 我们需要调整 TCP 端口限制并将其增加到更高的值。 这需要 Windows 注册表中的一些注册表设置。

好消息是,压力测试工具可以自动在注册表中对您进行更改。 所有您需要做的是选择其他-> 调整在压力测试工具的注册表设置。

上面的对话框将被打开, 它将提供有关设置的简要信息, 并将要求您确认操作。 这些更改完全无害, 将增加操作系统上的 TCP 端口创建限制。您必须确保已启动 "压力测试" 工具作为管理员, 才能进行此更改。

成功的操作将显示下一条消息, 并要求您重新启动 PC 以完成操作。


uniGUI HyperServer

uniGUI 版本1.50.0 介绍了 uniGUI HyperServer, 它主要旨在从根本上提高 uniGUI 应用程序的可用性、稳定性和可伸缩性。 它还介绍了其他重要功能, 例如允许远程更新应用程序, 而无需停止应用程序服务器。


什么是 HyperServer？

uniGUI HyperServer 是一种新的服务器 archirecture, 旨在高度提高 uniGUI 应用程序的可用性、稳定性和特定的可伸缩性。 这一目标是通过应用业界已知和广泛使用的技术 (如负载平衡和过程回收) 实现的。

传统的 uniGUI 应用程序服务器由一个单独的进程和多个线程组成。 单个进程可以是 uniGUI 可执行文件或 ISAPI 或 Apache 工作进程。 在所有情况下, 所有会话都驻留在单个进程中, 并且在同一进程中有多个线程用于处理传入的请求。

uniGUI HyperServer 改变了模型, 变成了一个多进程多线程模型。 在这个新模型中, 会产生几个工作进程来服务同一 web 应用程序。 会话将在工作进程之间划分, 并且基于配置, 您可能有多个工作进程用于相同的 uniGUI 应用程序。 工作进程由另一个实际上是 HyperServer 本身的进程管理和编排。 HyperServer 将是所有传入请求的主要入口点。 HyperServer 将接受所有请求并将它们分发到工作进程中。 HyperServer 还有责任在需要时生成新的工作进程, 并在清除后回收它们。 由于 uniGUI 会话是有状态的, 因此 HyperServer 的另一个重要职责是将传入的会话请求定向到创建该特定会话的正确的工作进程。 在 HyperServer术语中, 每个工作进程称为一个节点。


术语

HyperServer 介绍了几个新的术语。 在更进一步之前, 熟悉它们是很好的:

HyperServer

HyperServer 是所有请求的主要入口点。 它将筛选请求并将它们定向到 corresponsing 节点。 如果请求未与节点关联, 则 HyperServer 将根据负载分配算法将其定向到任意节点。 HyperServer 还负责创建新节点并在需要时回收它们。

不用说, HyperServer 本身是一个 uniGUI 的应用程序, 是为这一特定目的而设计的。 HyperServer 是以预编译的二进制文件 (Exe 和 Dll) 的形式部署的。 可用的部署选项有:独立服务器、 ISAPI 模块和Windows 服务。 将来, HyperServer 组件将包含在 uniGUI 库中, 因此开发人员将能够创建自己的自定义 HyperServers。

节点

HyperServer节点实际上是一个工作进程。 同时, 它是在独立 EXE 模式下部署的 uniGUI 应用程序本身。 开发人员将以节点的形式部署其应用程序。 开发人员在设计节点应用程序时不需要采取任何特殊的操作。 他们可以开发和测试他们的应用程序, 因为他们正在开发任何标准 uniGUI 应用程序。

节点 Id

每个节点都有一个唯一的节点 Id。 节点 Id 是在创建节点时分配的。 在回收节点时, 将重新使用节点 id。

运输

HyperServer 使用传输通道与节点进行内部通信。 当前仅实现了 HTTP 传输通道。 今后将根据各种技术 (如 Wiidows 管道、TCP 和 UDP) 实施各种其他传输渠道。

节点回收

节点将根据预定的方案进行回收。 应回收节点时, 将要求关联的进程终止自身。 确认此请求后, 节点将开始终止其所有会话, 并最终终止其进程。 如果节点不承认回收请求, 则它将被 HyperServer 强制终止。

活动节点

活动节点是那些主动为会话提供服务的节点。 他们能够接受新的会议。

挂起节点

挂起的节点是与 HyperServer 通信失败的节点。 在正常操作下, HyperServer 在一定的时间间隔内对其所有节点进行民意调查。 如果其中一个节点无法响应, 则它将被标记为挂起。 如果某个节点在经过一定的重试后无法响应, 那么它将被视为故障节点, 将被清除。

清除的节点

清除的节点是从活动节点列表中移除并发送到回收队列的节点。 清除的节点既不会接受新的会话, 也不会处理任何传入请求。 下次清理回收队列时, 将从进程空间中删除它。

丢弃的节点

丢弃的节点是一个实际拥有许多会话的节点, 但它不会接受新的会话。 它将继续存在, 直到其所有会话终止为止。 在没有剩余会话的时候, 它将被清除。

持久节点零

持久节点零是一个 Id 为零 (0) 的节点, 它保证连续运行, 即永远不会被永久卸载。 但是, 它有时会像其他节点一样被回收。 再次, 保证它将在回收后立即重新加载。 持久节点的主要目的是确保一次至少运行一个节点。 考虑一个场景, uniGUI 应用程序应该在一个线程中执行后台任务, 可能是位于 ServerModule 上的 TUniThreadTimer。 在经典的 uniGUI 应用程序中, 只有一个过程, 因此您的后台任务保证以单一的形式运行。 但是, 在 HyperServer 中同时运行同一进程的多个副本的情况下, 应该有一个机制来确保单例方法只在其中一个节点中运行。 持久节点零选项完全符合此目的。 您需要做的就是在 uniGUI 应用程序中启用此选项并检查节点 Id。 如果 ServerModule 名为 NodeZero 的布尔属性为 True, 则运行后台任务是安全的。

在下面的代码 ThreadTimer 事件仅在当前节点为节点零 (节点 Id = 0 ) 时执行。

procedure TUniServerModule.UniThreadTimer1Timer(Sender: TObject);
begin
if NodeZero then
begin

// Your task here

end;
end;


更深入地看看 HyperServer

在本节中, 我们将尝试深入了解 uniGUI HyperServer。 我们可以收集 HyperServer 的功能, 以不同的类别:

1.HyperServer 和稳定性

2.HyperServer 和可伸缩性

3.HyperServer 和远程部署

HyperServer 稳定

HyperServer 是一种新的服务器技术, 专门用于从根本上提高 uniGUI 应用程序的可伸缩性和稳定性。 眼前的问题是, 为什么我们首先需要 HyperServer？

为了首先回答这个问题, 我们需要让自己熟悉一个经典的 uniGUI 应用程序。 在经典的 uniGUI 应用程序中, 二进制文件由编译器生成, 通常是 DLL 或可执行文件。 此二进制文件在加载到进程内存空间时, 负责充当服务器。 它将无限期地运行, 直到它由系统操作员手动卸载, 或者由操作系统自动执行, 或者由于程序错误而导致应用程序崩溃。 在理想条件下, 您可能希望应用程序服务器二进制文件在内存中停留一段时间。 一个健壮稳定的应用服务器所期望的东西。

经典应用模型的另一个特点是单个过程负责创建、保存和服务所有会话。 它类似于把所有的鸡蛋放在一个篮子里。 在这种情况下, 会话是鸡蛋, 应用服务器就是篮子。 有几个缺点和缺点可以与此模型关联。 最明显的问题是, 一旦 web 应用程序服务器终止或重新启动, 所有活动会话都将被丢弃。 另一个可以极大地影响应用程序稳定性的问题是应用程序代码中的 bug 和缺陷。 这些 bug 可能导致严重的内存泄漏和其他问题, 最终可能导致内存损坏和 web 应用程序服务器崩溃。 这里的问题是, 即使是微小内存问题的累积效应也会增加加班时间并影响服务器操作。 因为在经典模型应用服务器注定要运行无限期的时间, 即使小问题可能会成为致命的一段时间。 当然, 对于那些受到隐藏 bug、内存泄漏、指针问题和开发人员可能不可见的问题的应用程序来说, 这是正确的。 如果将遗留应用程序代码从桌面 VCL 应用程序移植到 uniGUI, 可能很难检测和修复其中的某些问题。

不用说, 开发的应用程序是根据常见的设计模式开发的, 并且对内存问题进行了良好的测试, 漏洞和 bug 可以在经典的单一流程应用程序模型中无限期地运行。 众所周知, uniGUI 应用程序服务器非常稳定和健壮。 众所周知, 许多开发人员在 uniGUI 的稳定性方面几乎没有问题。 然而, 必须强调的是, 虽然单一过程模型可以被认为是稳定的 typcial uniGUI 应用程序, 同样的事情不能说的可伸缩性。 考虑到稳定性, 开发人员的主要问题是, 他们可能会面临许多级别的软件缺陷, 可能很难检测、修复或找到解决方法。 让我们看看这些缺陷的列表:

·当前项目代码中的 bug。

·遗留代码中的 bug 从旧项目移植。

·第三方库中的 bug。

·编译器 bug。

·错误在德尔菲标准库 (RTL, VCL, DB 等)

上面的一些源可能非常少见, 例如编译器 bug 和德尔菲库 bug, 我们不能否认它们的存在。 在上述错误中, 只有那些可能导致内存泄漏的 bug, 在代码中的内存损坏或死锁可能对服务器应用程序有害。 最常见的错误是那些直接存在于项目代码中的 bug。 德尔菲应用程序的本机特性使它们容易受到内存问题的影响, 直到所有这些问题都是固定的应用程序不能被接受为完全稳定的。 软件开发团队最大的问题, 尤其是大的, 是他们不能总是确保生成相同质量的代码, 并且每个新版本都是无缺陷的。 即使是长期的压力测试也不足以检测、隔离和修复和隐藏代码缺陷。

为了解决上述问题, 我们需要一个服务器模型, 它可以更好地容忍应用程序代码的 bug。 这里的主要诀窍是在生产环境中增加代码生存的机会, 即使在黄金时段还没有准备好。 HyperServer 的目的是在存在这种情况时提高稳定性。 HyperServer 通过实施不同的机制实现这个目标。 内部负载平衡和节点回收。 负载平衡将在节点之间分发整个服务器负载。 基于预定义的规则, 节点回收将减少节点生命周期。 回收将确保节点仅驻留在进程内存空间中一段明确的时间内。 与经典模型不同, 它将应用程序服务器进程无限期地保留在内存中。

必须说, 在这个意义上, HyperServer 不应该被认为是一个解决错误的代码或作为一个工具来处理内存问题。 HyperServer 既不会修复应用程序 bug, 也不会为它们提供解决方法。 HyperServer 的主要工作是确保整个应用服务器不会因内存缺陷或节点中的其他问题而崩溃。

也许现在你可以找出一个 HyperServer 应用程序被分成两个基本部分。 第一部分是 HyperServer 可执行文件, 它是 web 应用程序的入口点。 它将服务于来自客户的所有传入请求。HyperServer 应用程序的第二部分是一组节点。

uniGUI HyperServer

HyperServer 和可伸缩性

web 应用程序最大的顾虑之一是它们的扩展能力。 uniGUI 应用程序也是如此。 事实上, 可伸缩性可能是 uniGUI 应用程序更大的关注点, 因为 uniGUI 会话是有状态的, 并且将所有状态信息保留在进程内存中。 我们可以说, 经典的应用模式, 使用一个单一的过程来服务于所有的会话可以扩展到数以百计的会话。 这并不意味着你不能在经典模型中有数以千计的会话。 理论上, 只要您的服务器有足够的资源, 并且应用程序设计本质上是可伸缩的, 那么经典模型中的会话数就没有上限。 这里的问题是, 在一个篮子里放1000只鸡蛋安全吗？ 显然, 答案是不。然而, 每个人似乎都同意, 如果我们分配1000个鸡蛋在10个篮子每包含100个鸡蛋。 这就是 HyperServer 来到现场的地方。 HyperServer 内部将应用程序划分为多个节点, 并在这些节点之间分发会话。 由于每个节点都是单独的进程, 因此其会话将与其他节点中的会话隔离。 因此, 节点中的任何问题只能影响该节点及其会话。

鸡蛋和篮子问题不是唯一的问题与经典的单一流程应用服务器模型。 另一个问题是, 在同一进程中保持大量的会话可能导致其他问题。 一个问题是 uniGUI 应用程序是高度多线程的应用程序, 它自然需要使用锁来同步某些操作。 随着会话数量的增加, 这种锁可能会变成瓶颈。 在 uniGUI, 我们尽量使用锁尽可能少, 他们是不可避免的。在 RTL 代码和其他涉及的第三方库中也可能会出现代码锁。

不仅 HyperServer 可以管理同一服务器内的节点群集, 而且可以扩展到在多个服务器上管理多个节点群集。 这意味着您将能够在服务器场上运行 uniGUI 应用程序。 这将为使用 uniGUI 和德尔菲开发的 web 应用程序添加一个新的维度。

HyperServer 和远程部署

HyperServer 添加到 uniGUI 框架的另一个令人振奋的新功能是能够远程部署应用程序的新版本, 而无需停止服务器。 因此, HyperServer 不仅是关于稳定性和可伸缩性的。 它还与应用程序服务器的连续性和可用性有关。 在经典的单个过程中 uniGUI 应用程序更新应用程序相对困难。 首先, 您需要跟踪通信量低且活动会话很少的适当时间。 然后, 您需要删除服务器, 这意味着所有活动会话将被丢弃。 可能需要警告应用程序用户注销并等待更新完成。

与 HyperServer 以上情况下的情况更长。 您可以远程上传应用程序的新版本, 其余的工作将由 HyperServer 完成。 HyperServer 将逐步更新节点, 所有新会话将被重定向到运行新版本应用程序的节点。 与旧版本的节点将在所有会话注销后立即被丢弃和清除。


安装和配置 HyperServer

安装和配置 HyperServer 是非常简单和简单的。 uniGUI 安装程序在 "" 下分发 HyperServer 预编译的二进制文件. \\ uniGUI \\ HyperServerin \\文件夹。 此文件夹包含用于所有可用部署方法的单独二进制文件: ISAPI DLL、独立服务器和 Windows 服务:

..\\ uniGUI \\ HyperServerin \\

这些文件是通用 HyperServer 服务器二进制程序, 它们可以配置为运行任何 uniGUI 应用程序。 为了部署 HyperServer, 您需要将 HyperServer 二进制文件复制到 uniGUI 应用程序文件夹中。 您可以自由重命名 HyperServer 二进制文件。 实际上, 如果要在不同的端口上运行多个 HyperServer 实例, 则需要重命名它。 如果以 DLL 模式部署, 则还可以重命名 hyper_server.dll 文件以匹配应用程序名称。 HyperServer 需要一个配置文件, 这是为 HyperServer 提供各种参数所必需的。 配置文件的扩展名为.CFG, 应该退出与 HyperServer 二进制文件相同的文件夹。 如果此文件丢失, 将自动创建。 也可以在同一文件夹下查找默认配置文件。

第一步是根据部署方法复制正确的二进制文件。 对于独立服务器使用hyper_server.exe, 对于 ISAPI DLL 使用hyper_server.dll和 Windows 服务正确的文件是hyper_service的.exe。

下一步是对配置文件进行正确的更改。 复制hyper_server.cfg文件到您的应用程序文件夹, 并使用文本编辑器打开它。

快速配置和测试 HyperServer

为了快速测试 HyperServer 功能, 您可以通过在配置文件中配置最小参数来完成快速配置。

·将hyper_server和hyper_server. cfg复制到应用程序的可执行文件驻留的文件夹中。

·在独立服务器模式下编译应用程序。 您可能需要更改项目的朝鲜文件, 以便从其他模式切换到独立服务器。

·编辑hyper_server文件, 并将binary_name参数设置为应用程序的可执行文件名称。 例如binary_name = myapp

·再次hyper_server文件集prompt_login =0

·运行hyper_server

·在浏览器选项卡中导航到 url http://localhost:8077

·您应该能够在浏览器选项卡中看到您的应用程序的主窗体或登录窗体。

·为了访问 HyperServer 控制面板在另一个浏览器选项卡导航到地址http://localhost:8077/服务器

·选择 HyperServer 选项卡, 您将能够观察 HyperServer 节点和其他各种信息。

·现在在这个阶段, 您可以测试远程部署选项。

o返回您的项目并更改一个小的视觉细节。

o重新编译您的应用程序。

o在 HyperServer 控制面板中选择上传图标。

o选择新编译的可执行文件 (myapp.exe), 然后按 "上载" 按钮。

o完成上载并确认下一个对话框。

o您将注意到, 所有现有节点都将标记为已丢弃。

o在新的浏览器选项卡中启动新会话。 现在, 您必须能够在浏览器选项卡中看到应用程序的新版本, 而旧版本的应用程序仍将继续在其他选项卡中运行。 您已经使用 HyperServer 的远程部署功能成功地更新了应用程序。

详细配置

在成功地使用 HyperServer 测试应用程序之后, 您需要熟悉 HyperServer 配置文件中的各种参数。

HyperServer 配置参数

在配置文件中, 您将看到各种参数。 其中许多可以保留在默认值中。 我们来看看这些参数。

[transport]
pool_size=0

pool_size参数用于确定传输句柄池的最大大小。句柄总数不能大于此值。当设置为 0 HyperServer 将尝试计算最佳值。

Default value = 0

[http_transport]
start_port =16384

start_port参数用于 HTTP 传输。 传输是 HyperServer 用于内部与节点通信的通道。 每个节点都需要拥有唯一的传输 id, 而 HTTP 传输实际上是 TCP 端口号。 HyperServer 需要知道端口的起始值。 它将根据此起始值为节点分配端口号。 在 Windows 中, 可以将许多不同的值分配给start_port。 默认情况下, 范围内的端口 (1024-49151) 被定义为用户端口。 默认情况下, HyperServer 使用端口 16384 ($ 4000) 作为start_port。在这个方案节点 #0 将分配端口 16384, 节点 #1 将分配端口16385等。

如果计划在同一 OS 上运行多个实例 HyperServer, 则端口号应以它们不会相互冲突的方式进行排列。 必须确保所有节点都将分配唯一的端口号。 通过为每个 HyperServer 实例保留端口间隙, 可以很容易地完成此操作。 它可以用下面的公式计算:

<start_port for new HyperServer instance> = <start_port of previous HyperServer instance> + ( <max_nodes of previous HyperServer instance> × 3 ) + 8

请考虑您计划在同一台服务器上安装三 HyperServer 实例。 实例配置的 max_nodes 参数分别为8、16和16。

对于第一个实例, start_port配置为16384 (或任何其他所需值)

第二个实例start_port应配置为16384 ( 8x3 8 ) = 16416

第三个实例start_port应配置为16416 ( 16x3 8 ) = 16472

正如您所看到的, 我们在端口起始号之间留下一些额外的缺口。 其原因是, 在某些情况下, 节点的总数量可以超出max_nodes参数。

默认值 = 16384

[hyper_server]
binary_name =

binary_name是节点可执行文件的名称。 它是您的 uniGUI 应用程序, 您应该在独立服务器模式下进行编译。 此文件应驻留在与 HyperServer 相同的文件夹中。 不应为此文件指定任何路径。 只应指定文件名和扩展名。 条目应在< 名称 > .exe格式中。 例如: acounting_app

initial_nodes =2

initial_nodes是 HyperServer 启动时最初创建的节点数。 HyperServer 还将使用此值在待机模式下维护最少的节点数。 当节点数低于此值时, 将自动创建新节点以匹配initial_nodes值。

默认值 = 2

max_nodes =8

系统中活动节点的最大数目。 所有传入会话将在活动节点之间分发。 实际上, 节点总数可以高于max_nodes , 因为当节点被清除或丢弃时, 它们将继续留在内存中一段时间, 直到它们被回收。 在典型情况下, 节点总数不会超过(max_nodes x 2 )

默认值 = 8

max_sessions =0

此参数用于将在 HyperServer 实例下创建的会话总数设为最大限制。 这是通过调整单个节点的MaxSessions属性来实现的。 MaxSession是通过max_nodes除以max_sessions来计算的。 显然, 此方法不会保证会话总数不会超过max_sessions。 它只是一个简单的方法, 把一个上限的总数字会话。

max_sessions的默认值为 0, 这意味着 HyperServer 不会尝试为每个节点的MaxSessions属性设置值。 在这种情况下, 每个节点都将接受MaxSessions会话, 这是您在 uniGUI 应用程序中设置的。

sessions_per_nodes =0

(此参数尚未实现)

prompt_login =1

设置为 1, 服务器监视器将要求用户 Id 和密码。 使用用户帐户保护服务器监视器非常重要。 HyperServer 服务器监视器还嵌入了一个新的控制面板, 它允许多个附加功能, 如远程部署和节点监视。 您可以在 CFG 文件中创建多达10个不同的用户帐户。 请参见下面的[登录 x]部分。

如果将此参数设置为 0, 则在访问服务器监视器时不会询问任何凭据。 在调试和测试应用程序时, 在开发阶段这样做是合理的, 但在生产模式下prompt_login应该设置为1。

Server Monitor Login Screen

服务器监视器登录屏幕

HyperServer Control Panel in Server Monitor

服务器监视器中的 HyperServer 控制面板

persistent_node_zero =0

在术语节中, 我们引入了一个名为 "持久节点零" 的新术语。

如果应用程序需要一个持久节点, 则必须将此参数设置为1。

端口 =8077

此参数适用于在独立服务器和Windows 服务模式下运行的 HyperServer 实例。 您需要为在同一台计算机上运行的每个实例分配不同的端口。 对于ISAPI 模块, 此参数无效。

url_path =
url_referer =
ext_root = [ext] \
uni_mobile_root = [unim] \
uni_root = [单] \
uni_packages_root = [unipack] \

以上参数均直接分配给 HyperServer 的 ServerModule 分量。 如果需要自定义 ServerModule 的属性之一, 如ExtRoot , 则只需使用上面列表中的等效ext_root , 并指定一个适当的值。

例子:

ext_root = C: \ uniserver \ extjs \ [ext] \(有关详细信息, 请参阅调整路径部分。

max_requests =500

此参数设置可由 HyperServer 处理的最大并发请求。 此值上方的更多请求将被排队。 默认值为500的大小足够大, 对于许多典型的应用程序, 平均请求负载。 建议将其保留为默认值。

[node_recycling]
启用 =1

recycle_after_secs =3600

recycle_after_sessions =0

recycle_after_idle_secs =300

recycle_when_empty =1

以上参数用于微调节点的回收行为。 有些仍在执行之中。 请将它们全部保留在默认值上。

[登录-0]
以 "\ =
密码 =
管理 =0

可以创建多达10个可以登录到服务器监视器的不同用户帐户。 只有在设置了上面定义的prompt_login参数后, 此帐户才有效。

管理参数确定帐户的访问级别。 如果您设置admin =0 , 那么帐户将只有监视器访问, 否则设置admin =1将授予帐户的管理权限。

[登录-1]
以 "\ =
密码 =
管理 =0

和上面一样。

[custom_mimes]
mime_0_ext=
mime_0_type=
mime_1_ext=
mime_1_type=
mime_2_ext=
mime_2_type=
mime_3_ext=
mime_3_type=
mime_4_ext=
mime_4_type=

自定义默剧允许向 HyperServer 范围添加自定义文件类型.。 在正常情况下, 出于安全原因, 只允许从服务器传输安全文件类型。 您可以在这里定义尽可能多的默剧。

可以通过向 CFG 文件添加条目来简单地添加新的默剧。。 请考虑您希望启用 ""7z "扩展名的文件访问权限.

您需要添加以下条目:

mime_0_ext=7z
mime_0_type=application/x-7z-compressed


HyperServer 独立模式

独立 HyperServer 与 uniGUI独立服务器相同。 建议仅用于测试目的, 而不是用于生产使用。 它作为桌面应用程序运行, 可以通过右键单击其托盘图标来终止。 完成配置后, 只需从桌面上运行即可。

在上一节中, 我们演示了如何轻松地在独立服务器模式下配置和测试 HyperServer。 有关详细信息, 请参阅 "安装和配置 HyperServer主题" 下的 "快速配置和测试 HyperServer "。


HyperServer ISAPI 模块模式

ISAPI 模块是用于生产服务器的首选部署方法之一。 此模式下的 HyperServer 配置非常类似于独立服务器模式。 安装 IIS 和创建应用程序池和应用程序文件夹所需的其他步骤。 为 HyperServer 设置 IIS 与在ISAPI 模块主题下描述的步骤类似, 但存在一些差异:

标准IUSR内置帐户不授予足够的权限 HyperServer 应用程序, 因此我们需要将其更改为本地公司或具有广泛权限的其他帐户。 如果要设置现有池并希望将其转换为 HyperServer, 则需要更改用于该 ISAPI 池的 Windows 帐户。 如果要创建新池, 请确保在创建时将其标识设置为本地公司。 有关如何更改池标识的应用程序池主题, 请参阅 "使用其他帐户" 下的 "说明"。

另一个重要步骤是更改池, 以便它可以支持32位应用程序。 当前二进制文件是32位 dll, 因此它们不能由64位 ISAPI 池加载。

配置池后, 您可以继续设置 HyperServer。 请将文件hyper_server复制到应用程序文件夹中。 可以在 "" 下找到此文件。\\ uniGUI \\ HyperServerin \\

..\\ uniGUI \\ HyperServerin \\

现在需要将hyper_server文件重命名为任何所需的名称。 最好与您的退出模块同名。 实际上, hyper_server dll将替换应用程序 dll, 并且您自己的应用程序应在独立服务器模式下重新编译。

让我们用一个简单的例子来演示它:

请考虑您有一个名为myapp.dll的现有 uniGUI ISAPI 模块。 为了将其转换为 HyperServer 应用程序, 请按照以下步骤操作:

·通过将应用程序池的标识更改为本地系统并启用32位应用程序, 可以配置现有的应用程序池。 如果不想更改现有池, 也可以创建新池。

·将hyper_server和hyper_server文件复制到现有 dll 所在的文件夹中。 将现有的myapp.dll备份到另一个文件夹, 或将其重命名为myapp.dll.old。

·将hyper_server重命名为myapp.dll和hyper_server. cfg至myapp. cfg。

·编辑myapp和设置binary_name = myapp

·将现有的myapp转换为独立服务器。 在德尔菲中打开您的应用程序, 并编辑您的朝鲜文件, 以便将您的应用程序编译为exe文件而不是dll。

·将myapp.exe复制到与myapp.dll相同的文件夹中。

·在浏览器中导航到 myapp.dll web 应用程序。 您应该能够看到您的 web 应用程序主窗体 (或登录窗体)。

很明显, 在 HyperServer 应用程序模型中, 您自己的应用程序始终部署在独立服务器模式下。 它是 HyperServer 本身, 它负责其他部署方法, 如 Windows 服务和 ISAPI 模块。 这使得应用程序开发变得更容易。 你需要创建和测试你的应用程序只有 n EXE 格式。


HyperServer 窗口服务模式

Windows 服务是生产服务器的另一个首选部署方法。 同样, 在服务模式下设置和配置 HyperServer 的步骤与其他部署方法有许多共同步骤。

..\\ uniGUI \\ HyperServerin \\

第一步是将文件hyper_service和hyper_service. cfg复制到您的应用程序文件夹中。

下一步是安装服务。 您也可以重命名hyper_service。 如果要运行多个 HyperServer 服务实例, 最好为每个服务 exe 文件提供唯一的名称。 不要忘记, 对于每个服务可执行文件, 不需要一个同名的 CFG 文件名。

为了安装服务, 打开一个具有管理权限的命令行窗口。

按顺序命令行提示使用下面的命令来安装您的服务。

hyper_server.exe /install <ServiceName>

参数< ServiceName >将使用为此服务实例生成自定义服务名称。 HyperServer 将在您提供的服务名称前面添加 HyperService。

例子:

hyper_service.exe /install myapp

此命令将安装名称为HyperService_myapp的 Windows 服务。

卸载服务也很简单。

hyper_service.exe /uninstall myapp

请确保指定了在安装服务期间指定的< ServiceName >。

在启动服务之前, 请确保在服务 CFG 文件中进行了必要的更改。 为了快速测试您的服务, 您只需设置binary_name = < yourapp .exe >并启动新安装的服务。


关于 HyperServer 的重要说明

经典 uniGUI 应用模型与 HyperServer 之间存在差异。 最大的区别是 HyperServer 是一个多进程模型, 与传统的 uniGUI 应用的单一过程模型相比。 使用 HyperServer 多个节点, 实际上是应用程序的实例将同时运行。

缓存文件夹放大器;日志文件

每个节点都将创建自己的私有全局和专用缓存文件夹。 每个节点还将创建一个将对该节点可见的专用日志文件。

会话

经典模型的另一个重要区别是, 在经典模型中, 应用程序能够访问所有可用的会话对象, 而在 HyperServer 中, 每个节点只能访问由该特定节点创建的会话。 不用说, 每个节点都包含一组与其他节点隔离的会话。 如果需要查询所有会话, 则需要使用不同的方法, 例如在数据库表中保存会话信息。 可以在创建会话时在表中创建一行, 并在释放会话时将其删除。

其他特殊文件夹

请注意, 虽然节点有自己的专用缓存文件夹, 但它们使用文件夹和TempFolder的常用路径。 由于这些文件夹被用作只读, 因此不应为 uniGUI 节点带来问题。 因此, 一般情况下, 只应在缓存文件夹中创建文件和文件夹, 并且应将文件夹和TempFolder用作只读文件夹。


32位节点 vs 64 位节点

HyperServer 预编译的二进制文件被部署为32位可执行文件。 将来我们也可以部署这些二进制文件的64位版本。 节点可以是32位或64位。 HyperServer 可以使用这两种类型的节点。 在经典的 uniGUI 应用模型中, 我们有一个有效的理由使用64位节点。 这个原因是32位应用程序的内存限制。 在 HyperServer 模型中, 我们较少关注内存, 因为内存负载分布在节点之间。 因此, 更有可能的是, 我们可以使用32位节点, 而不存在任何内存问题。 即使在遇到内存限制问题的情况下, 也可以通过增加max_nodes参数的值来绕过它。

通常, 我们建议尽可能使用32位节点。 原因是32位应用程序的占用空间较小, 内存使用更保守。 与64位编译器相比, 德尔菲32位编译器似乎生成了更好的优化代码。 虽然在一般情况下, 64 位应用程序在64位 Windows 上表现得更好, 但对于德尔菲应用程序, 它可能不正确, 因为德尔菲32位编译器似乎能够生成更好的优化输出。 这里的一般规则是, 当您不需要64位时, 尝试使用32位节点。

这一建议可能会在将来改变, 有利于64位编译器根据更多的优化和改进, 预计在德尔菲64位编译器。


远程部署

能够在不中断应用服务器的情况下远程更新应用程序是 HyperServer 的重要特点之一。 此功能使开发人员能够以透明的方式更新其 web 应用程序, 而无需停止应用程序服务器。 所有他们需要上传新版本从 HyperServer 控制面板或发送一个新的二进制远程使用 FTP 或其他文件传输方法。

基本上, 远程部署可以用两种不同的方式进行。

使用 HyperServer 控制面板进行远程部署

要使用 HyperServer 控制面板更新应用程序, 请按照下列步骤操作:

通过导航到http://< yourApp >/服务器地址打开服务器监视器。 (例如: http://localhost:8077/服务器)

在登录屏幕上输入具有管理权限的服务器监视器帐户的 Id 和密码

选择 " HyperServer " 选项卡, 然后单击工具栏上的 "上载(上载新版本)" 图标

选择新的可执行文件, 然后单击 "上载" 按钮完成上载操作

在下一步, 您将被提示确认使用上载的可执行文件更新应用程序

确认更新操作后, 您将注意到所有现有节点都将标记为已丢弃

丢弃的节点将不接受新的会话。 所有新的会话都将定向到新的活动节点。 查看下面的图像, 您可以看到两个不同版本的应用程序正在并排运行。 虽然旧版本继续为现有会话提供服务, 但所有新会话都将定向到运行新版本应用程序的节点。 一旦所有旧版本会话注销, 丢弃的节点将被清除, 只有运行新版本应用程序的活动节点才会保留。

每个节点的版本信息都从您的德尔菲应用程序中嵌入的版本信息中检索出来, 因此, 如果尚未完成, 则必须在 delphi IDE 中启用版本信息和自动递增生成号选项。

使用特殊的 "部署" 文件夹进行远程部署

远程更新应用程序的另一种方法是使用名为 "部署" 的特殊文件夹。 此文件夹在首次运行 HyperServer 时自动创建。 它位于您的应用程序文件夹下。

为了部署新版本, 您必须遵循一个非常简单的过程:

o将新的可执行文件复制到部署文件夹 (例如hyper.exe )

o重命名复制的文件并将其扩展名从.exe更改为. dep (例如, 重命名hyper.exe-> 超级 dep )

可以使用批处理文件自动执行此过程。

也可以使用 FTP 将新文件发送到远程部署文件夹。 您还需要将文件扩展名更改为.完成转让后的dep。

ftp> cd webapp32/hyper/deploy
ftp> put hyper.exe
ftp> mv hyper.exe hyper.dep

要知道必须首先复制 EXE 格式的新二进制文件, 然后在复制后更改其扩展名, 这一点很重要。 HyperServer 不断监视部署文件夹, 一旦检测到具有 DEP HyperServer 扩展名的文件, 将开始部署新的二进制文档。 完成部署后, DEP 文件将被删除。

重要说明:

您不应该直接复制/发送文件的扩展名为.dep向该部署文件夹中.。 这样做可能会导致节点可执行文件损坏和部署失败。


HyperServer 中的 SSL 支持

 顶 以前

DLL 模式不需要 SSL 配置, 因为 web 服务器 (如 IIS) 将承担 ssl 配置和 ssl 证书的责任。

对于独立和服务模式, ssl 配置是使用< server_name >. 。

下面是默认ssl. cfg文件的内容:

object TUniHyperSSL
SSL.Enabled = False
SSL.SSLOptions.RootCertFile = 'root.pem'
SSL.SSLOptions.CertFile = 'cert.pem'
SSL.SSLOptions.KeyFile = 'key.pem'
SSL.SSLOptions.Method = sslvTLSv1_1
SSL.SSLOptions.SSLVersions = [sslvTLSv1_1]
SSL.SSLOptions.Mode = sslmUnassigned
SSL.SSLOptions.VerifyMode = []
SSL.SSLOptions.VerifyDepth = 0
SSL.SSLPort = 0
end

您需要根据特定的 SSL 设置来配置以上参数。 您可能已经注意到, 上述格式与德尔菲的 DFM 文本格式相同。 实际上, 它是您的 ServerModule 的序列化SSL属性。

您需要做的就是打开 ServerModule 并将当前的 ssl 配置复制到ssl.cfg文件。

object UniServerModule: TUniServerModule
OldCreateOrder = False
TempFolder = 'temp\'
Title = 'New Application'
SuppressErrors = []
Bindings = <>

SSL.Enabled = True
SSL.SSLOptions.RootCertFile = 'root.pem'
SSL.SSLOptions.CertFile = 'cert.pem'
SSL.SSLOptions.KeyFile = 'key.pem'
SSL.SSLOptions.Method = sslvTLSv1_1
SSL.SSLOptions.SSLVersions = [sslvTLSv1_1]
SSL.SSLOptions.Mode = sslmUnassigned
SSL.SSLOptions.VerifyMode = []
SSL.SSLOptions.VerifyDepth = 0
SSL.SSLPassword = 'fmsoft'

ConnectionFailureRecovery.ErrorMessage = 'Connection Error'
ConnectionFailureRecovery.RetryMessage = 'Retrying...'
Height = 396
Width = 560
end

要更清楚地说明, 如果您已经有了现有的 SSL 安装程序, 只需打开项目的 ServerModule DFM文件, 并将所有与 ssl 相关的行复制到ssl.HyperServer 的cfg文件。

如果要配置新的 ssl 安装程序, 请创建一个新的 uniGUI 应用程序, 进行必要的 ssl 配置并对新设置进行完全测试。 在所有操作都将 ssl 行从 ServerModule 复制到ssl之后.cfg文件。