65.9K
CodeProject 正在变化。 阅读更多。
Home

Incoding Framework:模板部分

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.23/5 (4投票s)

2015 年 9 月 6 日

CPOL

10分钟阅读

viewsIcon

8759

免责声明:本文档是对客户端(浏览器)中 JSON 数据转换为 HTML 的转换进行调查,并揭示了 Incoding Framework 中模板(搜索、形成、本地存储和插入自定义引擎)的操作细节。

Article-8_Small

“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 来确认。本文中描述的所有内容都经过了个人经验的验证,这些构造和实践允许一开始就开发跨平台应用程序,而不是在成本高昂时重建架构。

© . All rights reserved.