18. MVC模式

  mORMot框架支持编写富Web MVC应用程序,依靠常规的ORM和SOA方法来实现其业务模型及其应用程序层,并为HTML呈现提供可选的专用MVC模型。

18.1. 模型

  根据模型-视图-控制器(MVC)模式,请参阅模型-视图-控制器,数据库模式应与用户界面分开处理。

  TSQLModel类集中了应用程序所需的所有TSQLRecord继承类,包括数据库相关和业务逻辑相关。

  有关如何定义应用程序模型的信息,请参阅ORM数据模型

18.2. 视图

  mORMot框架可以生成两种用户界面,对应于MVC视图:

  • 对于使用Delphi编写的桌面客户端,它允许创建类似于Ribbon的界面,具有完整数据视图和导航作为可视表格,报告和编辑窗口可以自动生成。整个用户界面是通过在代码中定义的一些常量来设计的。
  • 对于Web客户端,已经集成了纯Delphi的优化的Mustache模板引擎,并且可以使用清晰的MVC设计轻松创建HTML视图。
DB Server
Rich Client 3
Server
Web client 2
Web client 1
Data Tier
FMX
Presentation Tier
Web
Presentation Tier
Application Tier
Business Logic Tier
HTML Browser
HTML Browser

  Web Presentation Tier将在后面详述,我们现在将介绍项目范围的实施方案。

18.2.1. 桌面客户端

18.2.1.1. RTTI

  Delphi语言(又名Object Pascal)在十多年前提供了运行时类型信息(RTTI)。简而言之,运行时类型信息是有关在运行时设置到内存中的对象数据类型的信息。 Delphi中的RTTI支持首先被添加到允许设计时环境完成其工作,但开发人员也可以利用它来实现某些代码简化。我们的框架大量使用RTTI,从数据库级到用户界面。因此,生成的程序具有快速开发(类似Rails)的优点,但具有强类型语法的稳健性,以及可用的最佳编译器速度。

  简而言之,它允许从代码本身中提取软件逻辑。以下是使用此技术的地方:

  • 所有数据库结构都是通过普通类定义在代码中设置的,并且大多数所需的SQL代码都是在调用SQLite3数据库引擎之前由框架动态创建的,从而产生真正的对象关系映射(ORM)框架;
  • 所有用户界面都是由代码生成的,使用一些简单的数据结构,依赖于枚举;
  • 由于Camel方法,屏幕上显示的大部分文本都依赖于RTTI,随时可以翻译成本地语言;
  • 所有内部事件过程(例如按下按钮)都依赖于枚举RTTI;
  • 选项和程序参数使用RTTI进行数据持久性和屏幕显示(如程序的“设置”窗口可以通过纯代码创建):添加选项只需几行代码即可。

  在Delphi中,枚举类型提供了一种定义值列表的方法。这些值没有固有的含义,它们的标准遵循列出标识符的顺序。这些值在代码中写入一次,然后在程序中的任何位置使用,甚至用于生成用户界面。

  例如,某些工具栏操作可以使用以下内容定义:

type
  /// item toolbar actions
  TBabyAction = (
    paCreateNew, paDelete, paEdit, paQuit);

  然后,这个TBabyAction枚举类型用于创建主窗口的用户界面功能区,只需创建一个这种类型的数组:

BarEdit: array[0..1] of set of TBabyAction = (
    [paCreateNew, paDelete, paEdit],
    [paQuit] );

  然后框架使用“Camel Case”提取要在屏幕上显示的按钮的标题:由源代码中的paCreateNew标识符定义的第二个按钮在屏幕上显示为"Create new" ,并且"Create new" 直接用于软件的i18n。有关“Camel Case”及其在Object Pascal、Java、Dot Net、Python中的用法的更多信息,请参阅http://en.wikipedia.org/wiki/CamelCase

  因此,RTTI的优点可以归结为:

  • 软件可维护性,因为整个程序逻辑是基于代码的,并且用户界面是从它创建的。因此,它避免了RAD(快速应用程序开发)滥用,它将用户界面与数据逻辑混合在一起,并可能导致“快速编写,努力维护”的情况;
  • 由于Object Pascal强类型语法,增强了代码安全性;
  • 从语言对象模型直接访问数据库,无需编写SQL或使用MVC框架;
  • 用户界面一致性,因为大多数屏幕都是即时创建的;
  • 易于i18n的软件,无需额外的组件或系统。

18.2.1.2. 用户界面

  在演示主示例应用程序设计期间,从RTTI生成用户界面和报表集成功能将在下面阐述。

  简而言之,包括用户界面自动创建在内的这种复杂模型可以这样编写,从单元FileTables.pas中提取:

function CreateFileModel(Owner: TSQLRest): TSQLModel;
begin
  result := TSQLModel.Create(Owner,
    @FileTabs,length(FileTabs),sizeof(FileTabs[0]),[],
    TypeInfo(TFileAction),TypeInfo(TFileEvent));
end;

  所有需要的TSQLRecord类都在常量数组中声明:

 FileTabs: array[0..4] of TFileRibbonTabParameters = ( ...

  并将使用TFileAction/TFileEvent枚举类型来处理用户界面活动和业务逻辑。

18.2.2. Web客户端

18.2.2.1. Mustache模板引擎

  Mustache,请参阅http://mustache.github.io,是一个众所周知的无逻辑模板引擎。

  有很多开源实现(包括JavaScript,对于客户端的AJAX应用程序来说非常方便)。对于mORMot,我们创建了它的第一个纯Delphi实现,与框架的其他模块完美集成。

  一般来说,模板系统可用于规范分离输出格式,这些规范控制输出文本和数据元素的外观和位置,从执行逻辑准备数据和决定在输出中出现的内容。

  大多数模板系统(例如PHP,smarty,Razor ......)实际上都是模板内容中的完整脚本引擎。它允许在HTML内容中使用强大的结构,如变量赋值或条件语句。它可以轻松地独立修改模板系统中应用程序的外观,而无需修改任何底层的“应用程序逻辑”。然而,他们这样做是以分离为代价,将模板本身转变为应用程序逻辑的一部分。

  Mustache继承自Google的ctemplate库,并在许多著名应用程序中使用,包括Google网络搜索或Twitter网站。

  Mustache模板系统倾向于保持逻辑和表示的分离,因此确保了完美的MVC设计(模型-视图-控制器),并能够使用SOA服务。

  Mustache被特意限制在它支持的功能中,因此,应用程序往往需要相当多的代码来实例化模板:所有应用程序逻辑都将在控制器代码中定义,而不是在视图中定义。这可能不是每个人的口味。但是,虽然此设计限制了模板语言的功能,但它并不限制模板系统的功能或灵活性。该系统支持任意复杂的文本格式。

  最后,Mustache的设计注重效率。模板实例化非常快,着眼于最小化内存使用和内存碎片。因此,它看起来像是我们的mORMot框架的完美模板系统。

18.2.2.2. Mustache原理

  Mustache模板系统有两个主要部分:

  • 模板(纯文本文件);
  • 数据字典(又名上下文)。

  例如,给出以下模板:

<h1>{{header}}</h1>

{{#items}}
  {{#first}}
    <li><strong>{{name}}</strong></li>
  {{/first}}
  {{#link}}
    <li><a href="{{url}}">{{name}}</a></li>
  {{/link}}
{{/items}}

{{#empty}}
  <p>The list is empty.</p>
{{/empty}}

  以及以下数据上下文:

{
  "header": "Colors",
  "items": [
      {"name": "red", "first": true, "url": "#Red"},
      {"name": "green", "link": true, "url": "#Green"},
      {"name": "blue", "link": true, "url": "#Blue"}
  ],
  "empty": true
}

  Mustache引擎将如下呈现此数据:

<h1>Colors</h1>
<li><strong>red</strong></li>
<li><a href="#Green">green</a></li>
<li><a href="#Blue">blue</a></li>
<p>The list is empty.</p>

  实际上,您在模板中没有看到任何“if”和“for”循环,但Mustache约定使得将提供的数据呈现为预期的HTML输出变得容易。 由MVC控制器根据模板的需求渲染数据,如格式化日期或货币值。

18.2.2.3. Mustache模板

  Mustache模板无逻辑语言有五种类型的标记:

  • 变量;
  • 块;
  • 反向块;
  • 注释;
  • 局部模版。

  所有这些标签都用mustaches标识,即{{{...}},在模板中找到的这些标识都将被解释为模板标记。 所有其他文本都被视为格式化文本,并在模板扩展时逐字输出。

标记 描述
{{variable}} 将在当前上下文中递归地搜索变量名(可能有带点的名称),如果找到,将被写成转义的HTML。
如果没有这样的键,就不会呈现任何内容。
{{{variable}}}
{{& variable}}
将在当前上下文中递归搜索变量名,如果找到,将直接写入,不需要任何HTML转义。
如果没有这样的键,就不会呈现任何内容。
{{#section}}
...
{{/section}}
定义一个文本块,即section,并根据当前上下文中搜索到的section变量值进行呈现:
-如果section = false或list[]为空,则不会呈现整个块;
-如果section为非false但不是列表,它将用作块的单个呈现的上下文;
-如果section是一个非空列表,那么块中的文本将为列表中的每个项目呈现一次,即块的上下文将逐个迭代设置当前项目。
{{^section}}
...
{{/section}}
定义一个文本块,也就是反向section,并根据当前上下文搜索到的section变量的反向值来呈现:
-如果section = false或为空列表,将呈现整个block;
-如果section为非false或非空列表,它将不会被呈现。
{{! comment}} 注释文本将被忽略。
{{>partial}} partial名称将在已注册的partial列表中搜索,然后在运行时使用当前执行上下文执行(因此可以使用递归部分)。
{{=...=}} 分隔符(即默认的{{…}})将被指定的字符替换(当文本中可能出现双大括号时,这可能很方便)。

  除了那些标准标记之外,Mustache的mORMot实现还具有以下功能:

标记 描述
{{helperName value}} 表达式助手,能够在呈现之前动态更改值。它可以用来根据TDateTimeTTimeLog值显示文本形式的日期。
{{.}} 这个伪变量引用上下文对象本身,而不是它的某个成员。这在遍历列表时特别有用。
{{-index}} 这个伪变量在遍历列表时返回当前项目编号,从1开始计数({{-index0}}}将从0开始计数)
{{#-first}}
...
{{/-first}}
定义的文本块(pseudo-section),它将呈现列表的首项,或者{{^-first}}反向呈现尾项
{{#-last}}
...
{{/-last}}
定义的文本块(pseudo-section),它将呈现列表的尾项,或者{{^-last}}反向呈现列表首项
{{#-odd}}
...
{{/-odd}}
定义的文本块(pseudo-section),它将遍历呈现并呈现奇数项,或者{{^-odd}}反向呈现偶数项,它对如交替行颜色显示一个列表等可能是非常有用的
{{<partial}}
...
{{/partial}}
在当前模板的范围内定义一个内联的局部模版(稍后通过{{>partial}}调用)
{{"some text}} 这个伪变量将把给定的文本提供给回调函数,回调函数在将文本输出之前转换其内容(如翻译)

  这组标记可以轻松编写任何类型的内容,无需任何显式逻辑或嵌套代码。 作为一个主要的好处,模板内容可以编辑和验证,而无需任何Mustache编译器,因为所有这些{{...}}标记将非常清楚地标识最终的布局。

18.2.2.3.1. 变量

   典型的Mustache模板:

Hello {{name}}
You have just won {{value}} dollars!
Well, {{taxed_value}} dollars, after taxes.

   给出以下哈希:

{
  "name": "Chris",
  "value": 10000,
  "taxed_value": 6000
}

   将产生以下内容:

Hello Chris
You have just won 10000 dollars!
Well, 6000 dollars, after taxes.

  您可以注意到默认情况下会为HTML转义{{variable}}标记。 这是强制性安全功能。 实际上,创建HTML文档的所有Web应用程序都容易受到跨站点脚本(XSS)攻击,除非插入到模板中的数据被适当地清理、转义。 使用Mustache,默认情况下会这样做。 当然,您可以使用{{{variable}}}{{&variable}}覆盖它并强制不转义值。

  例如:

模版 上下文 输出
* {{name}}
* {{age}}
* {{company}}
* {{{company}}}
{
"name": "Chris",
"company": "<b>GitHub</b>"
}
* Chris
*
* &lt;b&gt;GitHub&lt;/b&gt;
* <b>GitHub</b>

   变量使用可选的虚线语法解析当前上下文中的名称,例如:

模版 上下文 输出
* {{people.name}}
* {{people.age}}
* {{people.company}}
* {{{people.company}}}
{
"people": {
"name":"Chris",
"company":"<b>GitHub</b>"
}
}
* Chris
*
* &lt;b&gt;GitHub&lt;/b&gt;
* <b>GitHub</b>
18.2.2.3.2. 块

  块会一次或多次呈现文本块,具体取决于当前上下文中键值。

  在我们上面的“奖励模板”中,如果我们想要隐藏税务细节,会发生什么?

  在大多数脚本语言中,我们可以在模板中编写if ...块,这是Mustache避免的。,所以我们定义一个块,它将根据需要进行渲染。

  模板变为:

Hello {{name}}
You have just won {{value}} dollars!
{{#in_ca}}
Well, {{taxed_value}} dollars, after taxes.
{{/in_ca}}

  在这里,我们创建了一个名为in_ca的块。

  给定in_ca的哈希值,将呈现该块,或者不呈现:

上下文 输出
{
"name": "Chris",
"value": 10000,
"taxed_value": 6000,
"in_ca": true
}
Hello Chris
You have just won 10000 dollars!
Well, 6000 dollars, after taxes.
{
"name": "Chris",
"value": 10000,
"taxed_value": 6000,
"in_ca": false
}
Hello Chris
You have just won 10000 dollars!
{
"name": "Chris",
"value": 10000,
"taxed_value": 6000
}
Hello Chris
You have just won 10000 dollars!

  块还会更改其内部块的上下文。 这意味着块变量内容成为最顶层的上下文,用于标识任何提供的变量键。

  因此,以下上下文将完全有效:我们可以将taxed_value定义为in_ca的成员,并且它将直接呈现,因为它是新的上下文的一部分。

上下文 输出
{
"name": "Chris",
"value": 10000,
"in_ca": {
"taxed_value": 6000
}
}
Hello Chris
You have just won 10000 dollars!
Well, 6000 dollars, after taxes.
{
"name": "Chris",
"value": 10000,
"taxed_value": 6000
}
Hello Chris
You have just won 10000 dollars!
{
"name": "Chris",
"value": 10000,
"taxed_value": 3000,
"in_ca": {
"taxed_value": 6000
}
}
Hello Chris
You have just won 10000 dollars!
Well, 6000 dollars, after taxes.

  在上面最新的上下文中,有两个taxed_value变量。 引擎将使用in_ca块中定义的上下文,即in_ca.taxed_value; 在根上下文(等于3000)中定义的那个被忽略。

  如果块名称指向的变量是列表,则块中的文本将针对列表中的每个项目呈现一次。 块的上下文将根据每次迭代的当前项进行设置。

  通过这种方式,我们可以循环集合。 Mustache允许任何深度的嵌套循环(如任何级别的主/详细信息)。

模版 上下文 输出
{{#repo}}
<b>{{name}}</b>
{{/repo}}
{
"repo": [
"name": "resque" ,
"name": "hub" ,
"name": "rip"
]
}
<b>resque</b>
<b>hub</b>
<b>rip</b>
{{#repo}}
<b>{{.}}</b>
{{/repo}}
{
"repo":
["resque", "hub", "rip"]
}
<b>resque</b>
<b>hub</b>
<b>rip</b>

  最新模板使用{{.}}伪变量,该变量允许呈现列表的当前项。

18.2.2.3.3. 反向块

  反向块在标准(非反向)块起始插入符号(^),可以根据键的反向值呈现文本。 也就是说,如果键不存在,为假,或者是空列表,则将呈现文本块。

  反向块通常在标准块之后定义,以便在标准块中不写入任何信息时呈现某些消息:

模版 上下文 输出
{{#repo}}
<b>{{.}}</b>
{{/repo}}
{{^repo}}
No repos :(
{{/repo}}
{
"repo":
[]
}
No repos :(
18.2.2.3.4. 子模版

  Partials是某种外部子模板,可以包含在主模板中,如在几个地方遵循相同的渲染,就像代码中的函数一样,它们可以简化模板的可维护性并节省开发时间。

  子模版在运行时呈现(与编译时相反),因此子模版递归是可以的,但要避免无限循环,它们还继承了调用上下文,因此可以在列表块中轻松地重用,或者与纯变量一起使用。

  在实践中,子模版应与数据上下文一起提供,它们可被视为“模板上下文”。

  例如,此主模板通过{{> user}}使用子模版:

<h2>Names</h2>
{{#names}}
  {{> user}}
{{/names}}

  将注册以下"user"模版:

<strong>{{name}}</strong>

  可以视为如下被扩展为单个的模板:

<h2>Names</h2>
{{#names}}
  <strong>{{name}}</strong>
{{/names}}

  在mORMot的实现中,您还可以创建一些内部子模版,定义为{{<partial}} ... {{/ partial}}伪块。 它可以减少维护多个模板文件的需要,并优化渲染布局。

  例如,可以重新定义先前的模板:

<h2>Names</h2>
{{#names}}
  {{>user}}
{{/names}}

{{<user}}
<strong>{{name}}</strong>
{{/user}}

  同一文件将定义子模板和主模板。请注意,我们在主模板之后定义了内部子模版,但我们可以在主模板逻辑中的任何位置定义它:在呈现主模板时忽略内部子模版定义,就像注释一样。

18.2.2.4. SynMustache单元

  作为我们的mORMot框架的一部分,我们在SynMustache单元中实现了优化的Mustache模板引擎:

  • 这是Mustache的第一个Delphi实现;
  • 它有一个单独的解析器和渲染器(所以你可以提前编译你的模板);
  • 解析器具有已编译模板的共享缓存;
  • 它通过了所有官方的Mustache规范测试,如http://github.com/mustache/spec中所定义,包括所有异常的空白处理;
  • 外部子模版可以作为TSynMustachePartials字典提供;
  • {{.}}{{-index}}{{"some text}}伪变量被添加到标准的Mustache语法中;
  • {{#-first}}{{#-last}}{{#-odd}}伪块被添加到标准的Mustache语法中;
  • 内部子模版可以通过{{<partial}}定义,这也是Mustache标准语法的一个很好的补充;
  • 它允许数据上下文以JSON或我们的TDocVariant自定义变体类型提供;
  • 渲染期间几乎不执行任何内存分配;
  • 它本身就是UTF-8,对任何字符串数据转换进行了优化;
  • 性能已经在SynCommons.pas的优化代码中得到调整和强化;
  • 每个解析的模板都是线程安全且可重用的;
  • 它遵循开放/封闭原则,参见SOLID设计原则,以便可以定制和扩展过程的任何方面(如用于任何类型的数据上下文);
  • 它与我们的mORMot框架的其他模块完美集成,可以实现真正的基于模型-视图-控制器设计的动态网站,以及在Mustache中编写视图与其它关注点的完全分离,如控制器、基于接口的服务、模型等;
  • API灵活且易于使用。
18.2.2.4.1. 变量

  现在,让我们来看一些代码。

  首先,我们定义需要的变量:

var mustache: TSynMustache;
    doc: variant;

  要解析模板,您只需要调用:

  mustache := TSynMustache.Parse(
    'Hello {{name}}'#13#10'You have just won {{value}} dollars!');

  它将返回模板的编译实例。

   Parse()类方法将使用共享缓存,因此完成后不需要释放mustache实例:无需编写try ... finally mustache.Free; end块。

  您可以使用TDocVariant来提供上下文数据(使用后期绑定):

  TDocVariant.New(doc);
  doc.name := 'Chris';
  doc.value := 10000;

  作为替代方案,您也可以使用如下方式定义上下文数据:

  doc := _ObjFast(['name','Chris','value',1000]);

  现在,您可以使用此上下文渲染模板:

  html := mustache.Render(doc);
  // now html='Hello Chris'#13#10'You have just won 10000 dollars!'

  如果要将上下文数据作为JSON提供,然后渲染它,您可以写:

  mustache := TSynMustache.Parse(
    'Hello {{value.name}}'#13#10'You have just won {{value.value}} dollars!');
  html := mustache.RenderJSON('{value:{name:"Chris",value:10000}}');
  // now html='Hello Chris'#13#10'You have just won 10000 dollars!'

  请注意,这里,JSON提供了类似MongoDB的扩展语法(即字段名称不加引号),并且TSynMustache能够在执行上下文中标识句点命名的变量。

  作为替代方案,您可以使用以下语法将数据上下文创建为JSON,并使用一组参数,因此更容易在实际代码中使用存储数据的变量(例如,任何字符串变量都按照JSON的预期引用, 并转换为UTF-8):

  mustache := TSynMustache.Parse(
    'Hello {{name}}'#13#10'You have just won {{value}} dollars!');
  html := mustache.RenderJSON('{name:?,value:?}',[],['Chris',10000]);
  html='Hello Chris'#13#10'You have just won 10000 dollars!'

  您可以在mORMot.pas单元中找到ObjectToJSON(0函数,该函数能够将任何TPersistent实例转换为有效的JSON内容,随时可以提供给TSynMustache编译实例。

  如果对象的发布属性具有一些getter函数,则它们将被动态调用以处理数据(例如,通过连接'FirstName Name'两个子字段作为FullName返回)。

18.2.2.4.2. 块

   按预期处理块:

  mustache := TSynMustache.Parse('Shown.{{#person}}As {{name}}!{{/person}}end{{name}}');
  html := mustache.RenderJSON('{person:{age:?,name:?}}',[10,'toto']);
  // now html='Shown.As toto!end'

  请注意,这些部分会更改数据上下文,因此在#person块中,您可以直接访问数据上下文person成员,即直接写{{name}}

  它还支持反向块:

  mustache := TSynMustache.Parse('Shown.{{^person}}Never shown!{{/person}}end');
  html := mustache.RenderJSON('{person:true}');
  // now html='Shown.end'

  要呈现项目列表,您可以使用{{.}}伪变量:

  mustache := TSynMustache.Parse('{{#things}}{{.}}{{/things}}');
  html := mustache.RenderJSON('{things:["one", "two", "three"]}');
  // now html='onetwothree'

  {{-index}}伪变量允许在渲染时对列表项进行计算:

  mustache := TSynMustache.Parse(
    'My favorite things:'#$A'{{#things}}{{-index}}. {{.}}'#$A'{{/things}}');
  html := mustache.RenderJSON('{things:["Peanut butter", "Pen spinning", "Handstands"]}');
  // now html='My favorite things:'#$A'1. Peanut butter'#$A'2. Pen spinning'#$A+
  //          '3. Handstands'#$A,'-index pseudo variable'
18.2.2.4.3. 子模版

  可以使用TSynMustachePartials定义外部子模版(即标准的Mustache子模版)。 您可以定义和维护TSynMustachePartials实例的列表,也可以对给定的呈现使用一次性子模版,如下所示:

  mustache := TSynMustache.Parse('{{>partial}}'#$A'3');
  html := mustache.RenderJSON('{}',TSynMustachePartials.CreateOwned(['partial','1'#$A'2']));
  // now html='1'#$A'23','external partials'

  这里TSynMustachePartials.CreateOwned()期望为子模版提供名/值对。

  内部子模版(SynMustache扩展之一)可以直接在主模板中定义:

  mustache := TSynMustache.Parse('{{<partial}}1'#$A'2{{name}}{{/partial}}{{>partial}}4');
  html := mustache.RenderJSON('{name:3}');
  // now html='1'#$A'234','internal partials'
18.2.2.4.4. 表达式助手

   表达式助手是Mustache标准定义的扩展。它们允许定义您自己的一组函数,这些函数将在渲染过程中调用,以将一个值从上下文转换为要渲染的值。

  TSynMustache.HelpersGetStandardList将返回标准静态助手列表,能够将TDateTimeTTimeLog值转换为文本,或将任何值转换为JSON表示。当前的注册助手列表是DateTimeToTextDateToTextDateFmtTimeLogToTextBlobToBase64JSONQuoteJSONQuoteURIToJSONEnumTrimEnumTrimRightPowerOfTwoEqualsIfWikiToHtml。例如,{{TimeLogToText CreatedAt}}TCreateTime字段值转换为待显示的文本。

  mustache标记语法是{{helpername value}}。提供的value参数可以是当前上下文中的变量名,也可以是常数({{helpername 123}}),常量JSON字符串({{helpername "constant text"}}),JSON数组({ {helpername [1,2,3]}})或JSON对象({{helpername {name:"john",age:24}}})。该值也可以是一个逗号分隔的集合值,它将被转换为相应的JSON数组,这些值是从当前上下文中提取的,如{{DateFmt DateValue,"dd/mm/yyy"}}

  您可以递归调用助手,就像嵌套函数一样:{{helper1 helper2 value}}将使用提供的值调用helper2,该结果将作为值传递给helper1

   您可以通过TSynMustache.HelperAdd方法创建并注册自己的表达式助手列表,甚至包括一些业务逻辑,以便在渲染过程中计算任何数据。

  助手可以用这样的方法实现:

class procedure TSynMustache.JSONQuote(const Value: variant; out result: variant);
var json: RawUTF8;
begin
  QuotedStrJSON(VariantToUTF8(Value),json);
  RawUTF8ToVariant(json,result);
end;

  这里,提供的Value参数将来自上下文的变量,或来自JSON数字,字符串,数组或对象的常量,编码为TDocVariant自定义变体类型。

  如果参数以逗号分隔列表的形式提供,您可以编写多参数函数:

class procedure TSynMustache.DateFmt(const Value: variant; out result: variant);
begin
  with _Safe(Value)^ do
    if (Kind=dvArray) and (Count=2) and (TVarData(Values[0]).VType=varDate) then
      result := FormatDateTime(Values[1],TVarData(Values[0]).VDate) else
      SetVariantNull(result);
end;

  所以你可以这样使用这样的表达式助手:

 La date courante en France est : {{DateFmt DateValue,"dd/mm/yyyy"}}

  Equals助手定义如下:

class procedure TSynMustache.Equals_(const Value: variant; out result: variant);
begin // {{#Equals .,12}}
  with _Safe(Value)^ do
    if (Kind=dvArray) and (Count=2) and
       (SortDynArrayVariant(Values[0],Values[1])=0) then
      result := true else
      SetVariantNull(result);
end;

  您可以在模板中使用它来提供其他视图逻辑:

{{#Equals Category,"Admin"}}
Welcome, Mister Administrator!
{{/Equals Category,"Admin"}}

  块结尾可以选择只包含助手程序名称,因此以下语法也是正确的,并且可能不太容易出错:

{{#Equals Category,"Admin"}}
Welcome, Mister Administrator!
{{/Equals}}

  #If助手更强大,因为它允许定义一些视图逻辑,通过在两个值之间设置= <> <=> = <>运算符:

{{#if .,"=",6}} Welcome, number six! {{/if}}
{{#if Total,">",1000}} Thanks for your income: your loyalty will be rewarded. {{/if}}
{{#if info,"<>",""}} Warning: {{info}} {{/if}}

  作为替代方案,您可以放置运算符而不使用字符串参数:

{{#if .=6}} Welcome, number six! {{/if}}
{{#if Total>1000}} Thanks for your income: your loyalty will be rewarded. {{/if}}
{{#if info<>""}} Warning: {{info}} {{/if}}

  这种最新的语法可能非常方便。 当然,由于Mustache是一个无逻辑的模板引擎,在大多数情况下最好不要使用#if助手,而是在提供的数据上下文中添加一些专用标志:

{{#isNumber6}} Welcome, number six! {{/isNumber6}}
{{#showLoyaltyMessage}} Thanks for your income: your loyalty will be rewarded. {{/#showLoyaltyMessage}}
{{#showWarning}} Warning: {{info}} {{/#showWarning}}

  该框架还提供了一些与其ORM绑定的内置可选助手,如果您使用mORMotMVC.pas创建MVC Web应用程序,您可以注册一组表达式助手,让您的Mustache视图从其中检索给定ID的TSQLRecord,或在自动生成的表中显示给定的实例字段。

  例如,你可以写:

aMVCMustacheView.RegisterExpressionHelpersForTables(aRestServer,[TSQLMyRecord]);

  这将为指定的表定义两个表达式助手:

  • 任何{{#TSQLMyRecord MyRecordID}} ... {{/TSQLMyRecord MyRecordID}}Mustache标记将从提供的ID值读取TSQLMyRecord,并将其字段放在当前渲染数据上下文中,准备在视图中显示。
  • 任何{{TSQLMyRecord.HtmlTable MyRecord}}Mustache标记,它将创建一个HTML表,其中包含所提供的MyRecord字段的所有信息(来自当前数据上下文),具有复杂的字段处理(如TDateTimeTTimeLog,集或枚举),以及正确的显示字段名称(和i18n)。
18.2.2.4.5. 国际化

  您可以在模板中定义{{"some text}}伪变量,这些文本将提供给回调,随时可以进行转换:它可能对i18n的Web应用程序很方便。

  默认情况下,文本将直接写入输出缓冲区,但您可以定义一个可以使用的回调,例如, 用于文本翻译:

procedure TTestLowLevelTypes.MustacheTranslate(var English: string);
begin
  if English='Hello' then
    English := 'Bonjour' else
  if English='You have just won' then
    English := 'Vous venez de gagner';
end;

  当然,在实际应用程序中,您可以分配一个TLanguageFile.Translate(var English:string)方法,如mORMoti18n.pas单元中所定义。

  然后,您将能够如此定义模板:

  mustache := TSynMustache.Parse(
    '{{"Hello}} {{name}}'#13#10'{{"You have just won}} {{value}} {{"dollars}}!');
  html := mustache.RenderJSON('{name:?,value:?}',[],['Chris',10000],nil,MustacheTranslate);
  // now html='Bonjour Chris'#$D#$A'Vous venez de gagner 10000 dollars!'

  所有文本确实已按预期翻译。

18.2.2.5. 与基于方法的服务集成

  您可以轻松地将Mustache模板引擎与框架的ORM集成。 要避免任何不必要的临时转换,可以使用TSQLRest.RetrieveDocVariantArray()方法,并将其TDocVariant结果作为TSynMustache.Render()的数据上下文提供。

  例如,您可以使用任何基于方法的服务编写:

var template: TSynMustache;
    html: RawUTF8;
 ...
  template := TSynMustache.Parse(
    '<ul>{{#items}}<li>{{Name}} was born on {{BirthDate}}</li>{{/items}}</ul>');
  html := template.Render(
    aClient.RetrieveDocVariantArray(TSQLBaby,'items','Name,BirthDate'));
  // now html will contain a ready-to-be-displayed unordered list

  当然,此TSQLRest.RetrieveDocVariantArray()方法接受可选的WHERE子句,以根据您的需要使用。 您甚至可以使用分页来将列表拆分成较小的部分。

  在这种基于方法的低级服务流程之后,您可以使用mORMot轻松创建高性能Web服务器,遵循MVC模式:

MVC mORMot
Model 对象关系映射(ORM)及其TSQLModel/TSQLRecord定义
View Mustache模板引擎
(可存储为独立文件或存储在数据库中)
Controller 基于方法的服务,请参阅基于方法的客户端-服务端服务

  但是,仍然需要很多代码来粘合MVC部件。

18.2.2.6. MVC/MVVM设计

  实际上,基于方法的服务MVC模式很难使用,你自己有很多代码工程,例如: 参数编码,渲染或路由。

  mORMotMVC.pas单元提供了一个真正的MVVM(Model View ViewModel)设计,更高级,它依赖于接口定义来构建应用程序:

MVC mORMot
Model 对象关系映射(ORM)及其TSQLModel/TSQLRecord定义
View Mustache模板引擎
(可存储为独立文件或存储在数据库中)
ViewModel 接口服务,请参见基于接口的客户端-服务端服务

  在MVVM模式中,Model和View组件都与经典的Model-View-Controller布局相匹配。 但ViewModel将定义某种“视图模型”,即从视图中发送和检索数据上下文。

  在mORMot实现中,接口方法用于定义任何请求的执行上下文,遵循我们框架的约定优于配置的约定。

  实际上,使用以下约定定义ViewModel:

ViewModel mORMot
Route 使用接口名和方法名
Command 由方法名定义
Controller 由方法实现定义
ViewModel上下文 以JSON形式传输,包括复杂值,如TSQLRecord、记录、动态数组或变体(包括TDocVariant)
Input上下文 作为方法输入参数(const/var)从视图传输
Output上下文 将方法输出参数(var/out)发送到视图
Actions 使用方法输出参数渲染相关视图,或使用另一个命令(可选的EMVCApplication)

  这可能看起来很不寻常(如果您来自RubyOnRails,AngularJS,Meteor或.Net实现),它已被确定为非常便于使用。主要好处是您不需要为ViewModel层显示定义数据结构。方法参数将在接口级别为您声明执行上下文,并在TMVCApplication类中实现。实际上,此实现使用接口输入和输出参数是定义AngularJS应用程序的$scope内容的另一种方法。

  ViewModel数据上下文作为JSON内容传输的事实,正如REST表示形式,可以产生很好的附加作用:

  • 视图对执行上下文一无所知,因此与各种业务逻辑分离,这将增强应用程序的安全性和可维护性;
  • 您可以选择实时查看正在运行的应用程序的JSON数据上下文(使用伪root/methodname/jsonURI),以便更轻松地调试Controller或Views;
  • 您可以使用伪静态JSON内容测试任何View,而无需真正的服务;
  • 事实上,Views甚至可以不依赖于Web模型,而是运行在经典的富应用程序中,使用VCL/FMX用户界面(我们仍然需要自动化绑定到UI组件的过程,但这在技术上是可行的,而几乎没有其他MVC Web框架支持这种方式);
  • 由于接口用于定义Controller,您可以mock和stub它们,用于正确的单元测试;
  • 在Controller代码中,您可以访问mORMot ORM方法和服务来编写各种命令,从而可以非常轻松地实现任何SOA项目的Web前端(也可以共享许多高级域类型);
  • 关联数据模型是mORMot的ORM,它也针对JSON处理进行了优化,因此在渲染过程中大部分内存碎片都减少到最小(参见下面的RawJSON的使用);
  • Controller将大部分时间托管在Web服务器应用程序中,但可以物理托管在另一个远程进程中,这个远程控制器服务甚至可以在Web和VCL/FMX客户端之间共享;
  • 可以基于JSON内容实现多级缓存,以利用服务器资源并扩展到大量客户端。

  下一章将介绍如何使用mORMot构建此类可靠的MVC/MVVM Web应用程序。