Incoding Framework:模板部分
免责声明:本文档是对客户端(浏览器)中 JSON 数据转换为 HTML 的转换进行调查,并揭示了 Incoding Framework 中模板(搜索、形成、本地存储和插入自定义引擎)的操作细节。
为什么服务器端不好?
在回答这个问题之前,应该研究一下 asp.net mvc 能提供什么。 Razor - 是一个服务器端模板,它是 asp.net mvc 的默认功能,它具有巨大的功能,并且可以执行在 cshtml 文件框架中声明的 C# 代码,因此它不会引起问题,那么问题出在哪里呢? 当 Action 的反馈结果是通过在服务器上形成( View 或 PartialView ) html 来构建时,您将获得现成的内容,而不是“纯”数据,这可能会导致以下问题:
- 开发移动应用程序或第三方客户端(API )时,可能无法使用 Action 。
- 与紧凑的 JSON 数据相比,数据传输使用的流量因庞大的 HTML 内容而增加。
- 形成(渲染)占用了服务器资源,这可以通过将它们委托给客户端来避免。(客户端数量更多,所以我们不介意)。
注意:就客户端而言,当然是开玩笑,我们不介意,实际性能将在本文后面讨论。我们可以说,服务器端的内容形成具有更多的机会,但也存在一组如果没有客户端部分就无法解决的问题。
有一个解决方案
虽然 Razor 很好,但现代应用程序需要以 JSON 或 xml 的形式提供数据,因此服务器端 HTML 构建并不适合所有场景。在使用客户端纯模板引擎时。制定了一系列需求,可以在我们的“包装器”中实现。
- 类型
- IML 集成
- 精简模板
- “热”模板的替代品
- 自定义内容
类型
魔法字符串
Razor 的优点是支持 intellisense 和重构工具 ( 重命名、删除 ) 。与客户端模板相比,让我们举个例子来查看区别。
@each(var item in Model) { First Name: @item .FirstName }
注意:服务器端实现允许接收模型模式,并随后使用已知类型而不是动态类型。
{{each data}} First Name: {{FirstName}} {{/data}}注意:这是 handlebars 模板的一个示例,其中使用了普通字符串,并且 Visual Studio 无法提前计算包含的区域。
通过特殊重构工具(如 R#, )将 FirstName 重命名为 Name 会影响 View,但这不受 handlebars 支持,因为代码和模板之间没有链接。为了解决这个问题,已经构建了一个生成器,用于适配选定的引擎 ( handlebars,mustaches 等)。
using (var template = Html.Incoding().ScriptTemplate<Model>(tmplId))
{
using (var each = template.ForEach())
{ First Name: @each.For(r= >r.FirstName) }
}
ScriptTemplate 与 Template
Incoding Framework 提供了两种创建模板的方法
- ScriptTemplate - 生成的标记放置在具有预设 Id 的 script 标签中
<script id="templateId" type="text/x-mustache-tmpl">
{{#data}}
<option {{#Selected}}selected="selected"{{/Selected}} value="{{Value}}">
{{Text}}
</option>
{{/data}}
</script>
注意:除 javascript 外,所有类型都应显示为 type ,以便浏览器不执行嵌入的代码。
- Template - “纯” 生成的标记。
注意:当模板 作为 Action 的结果返回时,此方法很有用( 详细的审查将在 IML 集成 块中介绍)。
语法
ITemplateSyntax 包含用于构建模板的方法,为了注册它,应该将一个注册添加到 IoC ( Bootstarpper.cs )。
registry.For<ITemplateFactory>().Singleton().Use<TemplateHandlebarsFactory>();
注意:目前设置了 Handlebars (默认通过 nuget )和 Mustaches 的实现,但可以根据它们的示例编写单独的 TemplateSyntax 。
- ForEach - 是一个集合上的循环( razor - 基于 @foreach(var item in Model) {} 的对应项 )
- 主要(响应中接收的数据)
using(var each = template.ForEach()) { //some template }
-
- 嵌入式
@using (var innerEach = each.ForEach(r = > r.Itmes))
{ // some template }
- NotEach - 如果没有数据则显示内容( razor - 基于 @if(Model.Count == 0) { } 的对应项 )
using (var each = template.NotEach()){ // some template }
注意:可以与 ForEach 一起使用。
<ul>
@using (var each = template.ForEach())
{ <li>@each.For(r = > r.Title) / @each.For(r = > r.Code)</li> }
@using (var each = template.NotEach())
{ <li> No data </li> }
</ul>
- For - 显示指定字段的内容( razor - 基于 Model.Title 的对应项 )
each.For(r = > r.Title)
- ForRaw - 显示指定字段的内容而不编码 HTML( razor - 基于 Html.Raw(Model.Title) 的模拟项 )
each.ForRaw(r = > r.Title)
- Is - 表示如果字段为 True 或 NOT NULL 则显示( razor - 基于 @if(Is != null || Is){} 的对应项 )
using(each.Is(r = > r.Property)) { // some template }
- Not - 表示如果字段为 False 或 NULL 则显示( razor - 基于 @if(Is == null || !Is) {} 的对应项 )
using(each.Not(r = > r.Property)) { // some template }
- Inline - 根据字段值显示 true 内容( True 或 NOT NULL )或 false 内容( False 或 NULL )( razor - 基于 @(Is ? “true-class” : “false-class”) 的对应项 )
@each.Inline(r = > r.Is, isTrue: "true-class", isFalse: "false-class")
注意: Is 和 Not 用于构建大块内容,而 Inline 会立即绘制回内容。注意: Inline 具有 IsInline, NotInLine 的对应项,它们只显示条件的一部分。
-
- 如果为 true,则将类设置为红色
<span class="@each.IsInline(r= >r.Is,"red")"></span>
or use special syntax Razor
<span class="@each.IsInline(r= >r.Is,@<text><span>red></span></text>)"></span>
-
- 如果为 true,则隐藏元素,否则显示
<span style="@each.Inline(r= >r.Is,isTrue:"display:block",isFalse:"display:none")"></span>
-
- 如果为 true,则显示标题
using (each.Is(r = > r.Is))
{ <h3>Header</h3> }
@each.IsInline(r= >r.Is,@<h3>Header</h3>)
- Up - 向上移动到上一级层级
each.Up().For(r = > r.Property)
注意:当需要在 ForEach 内部获取字段的值(条件)时,此方法是必需的。
using (var each = template.ForEach())
{
using (var innerEach = each.ForEach(r = > r.Items))
{ @each.Up().For(r= >r.ParentProperty) }
}
- 摘要
@using (var template = Html.Incoding().Template<ComplexVm>())
{
using (var each = template.ForEach())
{
<div>
<ul style="@each.IsInline(r = > r.IsRed, "color:red;")">
@using (var countryEach = each.ForEach(r = > r.Country))
{
<li>
@using (each.Up().Is(r = > r.IsRed))
{
<span>Country @countryEach.For(r = > r.Title) from red group @each.Up().For(r = > r.Group)</span>
}
@using (each.Up().Not(r = > r.IsRed))
{
<span>Country @countryEach.For(r = > r.Title) by group @each.Up().For(r = > r.Group)</span>
}
<dl>
@using (var cityEach = countryEach.ForEach(r = > r.Cities))
{ <dd> City: @cityEach.For(r = > r.Name) </dd> }
</dl>
</li>
}
</ul>
</div>
}
}
注意:可从 GitHub 下载示例。
IML 集成
@(Html.When(JqueryBind.InitIncoding)
.AjaxGet(Url.Action("FetchCountries", "Data"))
.OnSuccess(dsl = > dsl.Self().Core().Insert.WithTemplate(tmplId.ToId()).Html())
.AsHtmlAttributes()
.ToDiv())
IML 具有(AjaxGet, Submit, AjaxPost)方法来获取数据,这些数据可以进一步通过 Insert. 来插入。数据可以是 HTML 内容或 JSON 对象。为了插入 JSON 对象,使用了模板,其路径由 Selector 指定。注意:从 1.2 版本开始,首选使用 WithTemplateById 或 WithTemplateByUrl 方法。
- WithTemplateById - 通过 Id 查找包含模板的 DOM 元素。
dsl.Self().Core().Insert.WithTemplateById(tmplId) // by Selector.Jquery.Id(tmplId)
注意:在这种情况下,应该使用 ScriptTemplate(tmplId) 来构建模板。
@{
string tmplId = Guid.NewGuid().ToString();
using (var template = Html.Incoding().ScriptTemplate<CountryVm>(tmplId))
{
<ul>
@using (var each = template.ForEach())
{ <li>@each.For(r => r.Title) / @each.For(r => r.Code)</li> }
</ul>
}
}
@(Html.When(JqueryBind.InitIncoding)
.Do()
.AjaxGet(Url.Action("FetchCountries", "Data"))
.OnSuccess(dsl => dsl.Self().Core().Insert.WithTemplateById(tmplId).Html())
.AsHtmlAttributes()
.ToDiv())
- WithTemplateByUrl - 通过 ajax 下载布局。
dsl.Self().Core().Insert.WithTemplateByUrl(url) // by Selector.Incoding.AjaxGet(url)
控制器 (Controller)
public ActionResult Template()
{
return IncView();
}
查看模板
@using (var template = Html.Incoding().Template<AgencyModel>())
{
using (var each = template.ForEach())
{ <span> @each.For(r = > r.Name)</span> }
}
注意:在为 ajax 构建模板时,应使用 Template 而不是 ScriptTemplate 方法。
查看 IML
Html.When(JqueryBind.InitIncoding)
.Do()
.AjaxGet(Url.Action("GetAgencies", "IncAgency"}))
.OnSuccess(dsl = >
dsl.Self().Core().Insert.WithTemplateByUrl(Url.Action("Template", "IncAgency")).Append()
)
.AsHtmlAttributes()
.ToDiv()
每个模板都有自己的 Action 吗?有两种解决方案可以避免在 Action 中重复代码
- 共享 Action
public class SharedController : IncControllerBase
{
public ActionResult Template(string path)
{
return IncView(path);
}
}
注意:可以构建 Url 的扩展,以便通过 ReSharper 注释添加路径检查。
public static class UrlExtensions { public static string Template(this UrlHelper urlHelper, [PathReference] string path) { return urlHelper.Action("Template", "Shared", new { path = path }); } }
Url.Dispatcher().AsView("path to template")
注意:MVD 最初定位为一个通用的模板加载器。
Id 与 Url
最初,我们使用 DOM 元素(script)作为模板布局的存储,但逐渐转向通过 ajax 加载,后者具有以下优点:
- 在不同的 View 中重用模板
注意:对于 Id,是通过将模板提取到 Layout(另一个主页面)来实现的。
- Layout 被分解出 View
注意:对于 Id,是通过部分视图(partial view)实现的。
- 延迟加载(模板按需加载)
注意:在使用选项卡时尤其有用。
精简模板
我们选择支持逻辑较少模板的引擎,如 Mustaches、Handlerbars,因为这种方法可以简化 View,将复杂的逻辑移到服务器端,在那里更容易“处理复杂性”。为了清楚起见,任务将在“正常”方式下解决,然后使用逻辑较少的方式解决。
- 普通
if(Model.Count <= 5 && Model.Type == TypeOf.Product) {//something code }
- 逻辑较少
if(Model.IsLimitProduct) // public bool IsLimitProduct { get {return Count <= 5 && TypeOf == Product }} { //something code }
在第一种情况下,我们在 View 中计算值,但在逻辑较少的情况下,我们在服务器端预先计算表达式,这具有以下优点:
- 在其他场景中重用
- 可以进行单元测试覆盖
- View 布局中的代码更少
当任务复杂度增加时,逻辑较少模板的优势就显现出来了,因为在服务器端扩展和维护条件比在 View 中更容易。
“热”模板的替代品
在发现 mustaches 存在问题后出现了一个任务,它在 ie 8 及以下版本中运行缓慢,并且在插入大量数据(超过 30 条记录)时也存在问题。由于 mustaches 的实现已在多个项目中广泛使用,因此选择引擎应该很简单,以便任何引擎都可以使用。注意:下面描述的代码可在 GitHub 上找到。
JavaScript代码
function IncJqueryTmplTemplate() {
this.compile = function(tmpl) {
return tmpl;
};
this.render = function(tmpl, data) {
var container = $('<div>');
$.tmpl(tmpl, data).appendTo(container);
return container.html();
};
}
注意:由于并非所有引擎都支持预编译,因此 tmpl 可以保持不变。
布局代码
<script type="text/javascript">
ExecutableInsert.Template = new IncJqueryTmplTemplate();
</script>
注意:代码位于布局中,因为它必须在第一次调用 Insert.WithTemplate 之前。
模板代码
@{ string tmplId = Guid.NewGuid().ToString(); }
<script type="jquery-tmpl" id="@tmplId">
{{each data}}
<li>${Title}</li>
{{/each}}
</script>
注意:模板基于“纯”jquery tmpl 构建,未使用 ITemplateSyntax,但也可以编写实现并注册到 IoC 中。
IMl 代码
@(Html.When(JqueryBind.InitIncoding)
.Do()
.AjaxGet(Url.Action("FetchCountries", "Data"))
.OnSuccess(dsl => dsl.Self().Core().Insert.WithTemplateById(tmplId).Html())
.AsHtmlAttributes()
.ToTag(HtmlTag.Ul))
注意:IML 没有改变,因此模板引擎可以轻松更换,而无需进行重大修改。
自定义内容
更快,快很多!!
对于框架的早期版本,我们将模板存储在 DOM 元素(script)中,但这种方法不允许构建延迟加载,所以后来我们切换到 ajax 加载,这带来了其他问题:
- 如果在同一主页上立即加载了许多元素,那么浏览器请求池就会被填满。
注意:这可以通过使用缓存部分解决,但请求仍然会发出,尽管状态为 304。
- 引擎缺乏预编译
注意:当插入的数据量很大(超过 30 个对象)时尤其如此。
一劳永逸
解决方案在于 Local Storage(本地存储),它允许将 template 保存在浏览器中,然后一直使用它。模板会一直存在吗?– 为了回答这个问题,让我们看一下这个机制运行的代码(伪代码)。
if(storage.Contain(selector.ToKey() + TemplateFactory.Version)
return storage.Get(selector.ToKey() + TemplateFactory.Version);
var template = selector.Execute();
storage.Add(template.PreCompile());
return storage.Get(selector.ToKey() + TemplateFactory.Version);
- 第 1 行 - 检查本地存储中是否存在模板。
注意:选择器(selector)和版本(version)是键(有关版本细节见下文)。
- 第 2 行 - 从本地存储返回模板。
- 第 4 行 - 获取选择器值。
- 第 5 行 - 将模板添加到本地存储。
注意:在添加之前应该进行预编译。
- 第 6 行 - 从本地存储返回模板。
如果我更改了布局模板,但键仍然相同怎么办? - 为了解决这个问题,我们增加了全局版本控制。要指定所有模板字段的当前版本,必须使用 JavaScript 设置 TemplateFactory.Version。
<script>
TemplateFactory.Version = '@Guid.NewGuid().ToString()';
</script>
注意:代码应在调用 Insert.WithTemplate 之前执行,因此最好将其放置在 Layout 中。在示例中,我设置了一个 Guid,它保证在完全(F5)页面重新加载后版本会更新,但这种行为仅适用于开发过程,当代码部署到生产环境时,需要固定版本。
- Debug - 在开发过程中,标记活动速率 在模板中的速率很高,因此版本更新得尽可能频繁(我们使用 Guid 来保持其唯一性)。
- Release - 项目部署到服务器后,必须安装固定版本。
布局
<script type="text/javascript">
TemplateFactory.Version = '@CurrentSettings.CurrentVersion';
</script>
当前版本
public string CurrentVersion
{
get
{
#if DEBUG
return Guid.NewGuid().ToString();
#else
return Assembly
.GetExecutingAssembly()
.GetName()
.Version.ToString();
#endif
}
}
注意:TeamCity build.number 用作固定版本。我们将在后续文章中详细讨论持续集成( TeamCity, rake )的方法。
各司其职,协同作战
在构建模板时,只需指定模型类型即可,但不会考虑它是集合还是单个对象。一些模板引擎要求指定它将是集合还是单个对象(例如 handlebars {{# with data}} 或 {{# each autors}}),因此我们决定移除此条件,在构建模板时也不考虑项目的数量。得益于此功能,我们无需添加方法 With(并非所有引擎都支持)。
Collection
[HttpGet] public ActionResult FetchCountries() { return IncJson(GetCountries()); }
Single
[HttpGet] public ActionResult FetchCountry() { return IncJson(GetCountries().FirstOrDefault()); }
共享模板
@using (var template = Html.Incoding().Template<CountryVm>()) { <ul> @using (var each = template.ForEach()) { <li>@each.For(r = > r.Title) / @each.For(r = > r.Code)</li> } </ul> }
客户端不是服务器,或我们的钩子(重复的错误)
在培训员工使用客户端模板时,我列出了最常见的错误列表,这些错误是因为他们像编写服务器端模板一样编写代码。
GUID - 在视图中,它用于指定页面内元素的唯一名称,但在客户端模板的情况下,它存在陷阱。条件:每个 li 标签都必须有一个唯一的 Id。
@using (var each = template.ForEach()) { var uniqueId = Guid.NewGuid().ToString(); <li id="@uniqueId"></li> }
代码存在错误,因为变量 uniqueId 将在服务器上计算一次,集合的遍历将在客户端进行。要测试这一点,我们必须查看生成的标记。
{{each data}} <li id="EAAF08F8-0F09-4DA7-BE64-6D0075749D76"> </li> {{/each data}}
为了使代码正常工作,GUID 计算逻辑必须转移到 Model。
@using (var each = template.ForEach()) { <li id="@each.For(r= > r.UniqueId"></li> // UniqueId {get {return Guid.NewGuid().ToString(); }} }
结论
我们不断开发 Incoding Framework 的每个组件,并在每个版本中为其添加新功能,“打磨”旧功能,模板当然也不例外,您可以查看我们的 BugTracker 来确认。本文中描述的所有内容都经过了个人经验的验证,这些构造和实践允许一开始就开发跨平台应用程序,而不是在成本高昂时重建架构。