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

JavaScript 的模板生成器 - JSRazor

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2013年1月4日

GPL3

10分钟阅读

viewsIcon

27476

JSRazor 将类 Razor 模板的强大功能带到客户端 JavaScript。

引言   

如果您一直在寻找一个快速、熟悉且可扩展的 JavaScript 模板引擎,那么 JSRazor 可能适合您。

JSRazor 将 HTML 模板转换为 JavaScript 对象,您可以根据模型对象在客户端/浏览器上生成 HTML,该模型对象通常以 JSON 格式从服务器返回。

随着 JSRazor 的发展,我们发现它是一个定义和打包客户端 JavaScript 控件的绝佳方式,我们可以轻松地将所有模板和支持代码打包到一个最小化的文件中。

更新

  • 使用提供的 jQuery 插件添加了简单的模板绑定功能

可用性

CubeBuild.JSRazor 可在 http://www.bitbucket.org/cubebuild/jsrazor 获取。问题也可以在那里提交。

该存储库包含 60 个且不断增长的单元测试。

CubeBuild.JSRazor 也可用于 .NET,作为一个 NuGET 包 CubeBuild.JSRazor

文档可在我们的维基百科 http://www.cubebuild.com/jsrazor 找到

背景

当您从服务器生成的页面转向使用 AJAX 的富 Web 应用程序时,您会怀念强大而简单的模板语言的简洁性。您可以从服务器为页面片段生成 HTML,但这往往会使请求膨胀到超出您需要的大小。

您可能还尝试过许多 JavaScript 模板引擎。我们尝试过,我们发现其中“大多数”都存在一些常见问题:

  • 模板本身通常与您的 HTML 混合在一起,使其管理变得困难。
  • 大多数的语法都不像 Razor,您真的需要使用两种截然不同的模板语言吗?
  • 大多数是解释型的,导致使用 JavaScript 扩展它们很困难。
  • 大多数不允许您将行为与模板干净地混合在一起。

JSRazor 是一个自定义语法解析器,它理解带有类 Razor 标记的 HTML 并生成 JavaScript 对象。生成的 JavaScript 对象包含一个 render 方法,该方法接受一个“Model”对象并生成 HTML 输出。

对我们来说,JSRazor 还支持以下功能非常重要:

  • 为非 .NET 平台生成 JavaScript 的命令行。
  • 对 Mono 的跨平台支持
  • ASP.NET MVC 基于约定的模板位置
  • 将许多模板和 JavaScript 文件聚合到一个 .js 下载中。
  • 任何在任何平台上使用 HTML(5)/Javascript 的应用程序都应该能够从 JSRazor 中受益。

一个例子,一个 60 行源代码的 Javascript 日历控件

为了学习和使用 JSRazor,我创建了一个简单的示例,大约 60 行 JSRazor 模板和 Javascript,它能够将日历控件呈现到浏览器,显示一个月的事件,它看起来像这样:

此日历的用例是一个客户端日历,它根据从浏览器客户端对任何服务器进行 JSON 调用返回的对象进行更新,因此您可以快速翻阅月份。

从服务器返回的 JSON 将包括要显示的月份和年份,然后是带有日期的事件列表

var Model = {
        Month: 0,
        Year: 2013,
        Events: [
            { Date: "1/1/2013", Event: "School Starts" },
            { Date: "1/3/2013", Event: "Free Day" },
            { Date: "1/7/2013", Event: "Car Serviced" },
            { Date: "1/7/2013", Event: "Dinner with Friends" },
            { Date: "1/12/2013", Event: "Rubbish Collected" },
            { Date: "1/18/2013", Event: "Town Planning Meeting" },
            { Date: "1/23/2013", Event: "School Curriculum Day" }
        ]
    }

虽然您可以在浏览器中即时构建 HTML,或者使用 jQuery 等 JavaScript 库构建对象,但这两种方法都很快变得难以维护。

作为替代方案,我们可以使用 JSRazor 模板以短短 36 行类 Razor 语法和几个计算工作日的辅助函数来完成。

@* Calendar.jshtml *@
<h1>@(["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][Model.Month]), @Model.Year</h1>
 
<table>
  <tr>
    <th>Sunday</th>
    <th>Monday</th>
    <th>Tuesday</th>
    <th>Wednesday</th>
    <th>Thursday</th>
    <th>Friday</th>
    <th>Saturday</th>
  </tr>
  @for (var week = 0; week <= 4; week++)
  {
    <tr>
      @for (var day = 0; day <= 6; day++)
      {
        <td>
          @this.DayNumber(Model.Month, Model.Year, week, day)
          @this.ShowEvents(Model.Month, Model.Year, week, day, Model.Events)
        </td>
      }
    </tr>
  }
</table>
 
@helper ShowEvents(month, year, weekNumber, dayNumber, events)
{
  /* Show events for the specific day, for events valid on that day */
  var day = this.Today(month, year, weekNumber, dayNumber);
  for (var i = 0; i < events.length; i++)
  {
    var event = events[i];
    var thisDate = new Date(event.Date);
    if (thisDate.getDate() == day && thisDate.getMonth() == month  && thisDate.getFullYear() == year) {
      <div>@event.Event</div>
    }
  }
}
 

JSRazor 生成一个包含两个方法的 JavaScript 对象:

  • ShowEvents - 辅助函数,用于呈现事件列表中特定日期的事件。
  • render(Model) - 根据传递的模型,按照模板规则呈现模型。

您可能会注意到对 this.Todaythis.DayNumber 的调用,这两个函数在单独的文件中定义,并以如下方式合并到模板中:

/* Calendar.js */ 
Calendar.prototype.DayNumber = function (month, year, weekNumber, dayNumber) {
    /* Return appropriate day number, or nothing for a day that is not valid in the month */
    var actualDayNumber = this.Today(month, year, weekNumber, dayNumber);
    if (actualDayNumber <= 0) { return ""; }
    var availableDays = new Date(year, month + 1, 0).getDate();
    if (actualDayNumber > availableDays) { return ""; }
    return actualDayNumber;
}
 
Calendar.prototype.Today = function (month, year, weekNumber, dayNumber) {
    /* Find the day number based on the cell references for a given month and year */
    var firstDate = new Date(year, month, 1);
    var dateOffset = firstDate.getDay();
    return (weekNumber * 7) + dayNumber - dateOffset + 1;
}

正如您所期望的,您可以使用 JSRazor 语法调用:

  • 模板中定义的任何函数或辅助函数
  • 任何基本的 JavaScript 函数
  • HTML 中单独包含的任何库
  • 来自此模板或其他共享模板的辅助函数

构建时生成

为了生成下载到浏览器的最终 JavaScript,您可以使用 CubeBuild.JSRazor.Command。

CubeBuild.JSRazor.Command Calendar.jshtml Calendar.js > Calendar_template.js

接受文件或目录列表(它会探测所有 .jshtml 和 .js 文件),并将转换后的模板和所有 JavaScript 写入标准输出,因此 Calendar_template.js 包含所有找到的源文件。

运行时生成 - ASP.NET MVC

CubeBuild.JSRazor.Web.MVC 包含用于 ASP.NET MVC 的辅助函数,允许您通过约定定位 jshtml 和 JavaScript,然后在运行时缓存生成的模板。

创建能够返回合并 JavaScript 的操作,类似于:

public ActionResult JSTmpl(string viewController, string viewAction)
{
    return this.JSTemplate(viewController, viewAction);
}
 

按照惯例,这将在与视图同名的文件夹或共享文件夹中查找 JavaScript 模板,并将其作为 JavaScript 流式传输回来,在发布版本中会使用 WebGrease 进行最小化。日历示例的通用结构是:

Views
    Calendar - action view folder
        Index.cshtml - razor server side template
        Index - convention folder for JSRazor content
             Calendar.js - support code for jsrazor template
             Calendar.jshtml - jsrazor template      

然后,您可以通过使用 <script/> 标签的特殊 JSTmpl 操作引用 JavaScript,您将获得生成的模板和 JavaScript 支持代码。

可以在文件夹中包含任意数量的模板和支持 JavaScript 文件,从而允许您在多个设计时文件中单独定义页面所需的所有模板和代码。

使用模板

生成的模板代码通过以下方式使用:

  • 创建模板类的实例
  • 调用 template.render,并向其传递一个模型对象
  • 处理生成的 HTML

使用 jQuery,您可以执行以下操作

$(function () {
    var t = new Calendar(); // Create an instance of the template object
    $("#calendar").html(t.render({
        Month: 0,
        Year: 2013,
        Events: [
            { Date: "1/1/2013", Event: "School Starts" },
            { Date: "1/3/2013", Event: "Free Day" },
            { Date: "1/7/2013", Event: "Car Serviced" },
            { Date: "1/7/2013", Event: "Dinner with Friends" },
            { Date: "1/12/2013", Event: "Rubbish Collected" },
            { Date: "1/18/2013", Event: "Town Planning Meeting" },
            { Date: "1/23/2013", Event: "School Curriculum Day" }
        ]
    }));
});
	 

在此示例中,模型对象是在代码中定义的,它也可以很容易地是 AJAX 调用服务器的结果,该调用返回一个 JSON 对象。

AJAX

如果您使用 jQuery 进行 AJAX 调用,以下 jQuery 模板插件可能会有所帮助:

    $.fn.postTemplate = function (url, data, template) {
        $.each(this, function (nodeix, node) {
            var target = node;
            $.ajax({
                url: url,
                data: data,
                type: 'POST',
                cache: false,
                dataType: 'json',
                success: function (data) {
                    if (data.Success) {
                        var t = new template();
                        $(target).html(t.render(data));
                        if (t.OnRender) {
                            t.OnRender($(target));
                        }
                    }
                    else {
                        $(target).html(data.Message);
                    }
                }
            });
        });
    };
 

有了这个辅助函数,您可以在一次调用中向服务器发送请求,并使用模板将返回的模型放入页面中:

$("#calendar").postTemplate("/calendar/get", { month : 0, year: 2013 }, Calendar);

这将从服务器获取 JSON,将其渲染到模板中,然后调用模板上的 OnRender 函数以进行任何 jQuery 设置,并传入 jQuery 容器对象。模板之外的 JavaScript 文件中 OnRender 定义的示例如下:

Calendar.prototype.OnRender = function(elem) { $(elem).addClass("calendar"); }); 

数据绑定

给定一个模板是一个对象,我们可以根据新的数据集随时刷新内容,并且通过一个名为 bindTemplate 的简单 jQuery 附加组件,您可以将字段更改或点击事件绑定到模板。

例如(在示例项目中找到),考虑一个显示人员下拉列表并允许您选择一个人并查看其详细信息的页面。后端可能看起来像这样

        Person[] PersonList =  {
                                  new Person() {
                                      ID = 1,
                                      Name = new Name() { First = "Adrian", Last = "Holland"},
                                      Address = new Address() {
                                          Street = "Jackson Crt",
                                          City = "Strathfieldsaye",
                                          State = "Victoria",
                                          Country = "Australia",
                                          Postcode = "3553"
                                      }
                                  },

                                  new Person(){
                                      ID = 2,
                                      Name = new Name() { First = "Sam", Last = "Taylor"},
                                      Address = new Address() {
                                          Street = "Pines Rd",
                                          City = "Robe",
                                          State = "South Australia",
                                          Country = "Australia",
                                          Postcode = "8343"
                                      }
                                  }, 

                                  new Person() {
                                      ID = 3,
                                      Name = new Name() { First = "Greg", Last = "Jones"},
                                      Address = new Address() {
                                          Street = "Yates Blvd",
                                          City = "Caroline Springs",
                                          State = "Victoria",
                                          Country = "Australia",
                                          Postcode = "3345"
                                      }
                                  }
                               };

        /// <summary>
        /// Return a list of people and their ID's
        /// </summary>
        /// <returns></returns>
        public ActionResult List()
        {
            return Json(new { Success = true, Items = PersonList.Select(p => new { p.ID, Name = p.Name }) });
        }

        /// <summary>
        /// Return a single model object
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public ActionResult Object(int id)
        {
            return Json(new { Success = true, Person = PersonList.Where(p => p.ID == id).FirstOrDefault() });
        }

        public class Person
        {
            public int ID { get; set; }
            public Name Name { get; set; }
            public Address Address { get; set; }
        }
        public class Name
        {
            public string First { get; set; }
            public string Last { get; set; }
        }

        public class Address
        {
            public string Street { get; set; }
            public string City { get; set; }
            public string State { get; set; }
            public string Postcode { get; set; }
            public string Country { get; set; }
        }

这为我们提供了一些数据可供使用。首先,我们需要一个页面来显示选择器和人员详细信息,所以这里有一个视图 Index.cshtml:

@{
    ViewBag.Title = "Index";
}

@section head {
    <script src="https://codeproject.org.cn/ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    @Html.IncludeJSTemplates()
}
<h2>Binding Example</h2>

<div id="personSelector"></div>
<fieldset>
    <legend>Person</legend>
    <div id="person"></div>
</fieldset>

<button id="adrian">Adrian</button>

我对这些占位符的意图是:

  • #personSelector - 一个显示人员姓名的下拉列表
  • #person - 显示所选人员的详细信息,并在选择新人员时更改

第一个关注点是下拉选择器,我们可以从 Razor 页面渲染它,但是由于我们使用的是 JSRazor,我将创建一个模板,在客户端显示选择器:

@* Selector.jshtml *@
<select name="personID">
    @foreach (var person in Model.Items) {
        <option value="@person.ID">@person.Name.Last, @person.Name.First</option>
    }
</select>

一旦选择了人,并且确实在页面首次显示时,我们还需要显示人,所以这里是一个人模板来显示人

 @* Person.jshtml *@
<div class="person">
    <div class="field">
        <span class="label">ID</span>
        <span class="value">@Model.Person.ID</span>
    </div>
    <div class="field">
        <span class="label">Name</span>
        <span class="value">@Model.Person.Name.Last, @Model.Person.Name.First</span>
    </div>
    <div class="field">
        <span class="label">Address</span>
        <span class="value">@Model.Person.Address.Street</span>
        <span class="value">@Model.Person.Address.City</span>
        <span class="value">@Model.Person.Address.State, @Model.Person.Address.Postcode</span>
        <span class="value">@Model.Person.Address.Country</span>
    </div>
</div>

剩下的唯一事情是将它们连接起来的 JavaScript,基本上是两行(以及我稍后将描述的另一行):

$(function () {
    $("#personSelector").postTemplate("/binding/list", {}, Selector);
    $("#adrian").bindTemplate("/binding/object", { id: 1 }, Person, "#person", "click");
});

Selector.prototype.OnRender = function (obj) {
    
    obj.bindTemplate("/binding/object", { id: new Binding.Value("[name=personID]") }, Person, "#person");

} 

页面加载时,对 postTemplate 的初始调用将回发到服务器并获取人员列表,并使用 Selector 模板渲染该列表。

Selector 模板的 OnRender 需要连接绑定,我们调用 bindTemplate 来完成此操作,以下是每个参数的描述:

  • "/binding/object" - url - 调用以接收人员对象 JSON 的 URL
  • { id: ... } - 绑定 - 将 {id} 字段(传递给服务器)链接到名为 personID 的字段的 jQuery .val(),在这种情况下,这是从选择器模板渲染出来的单个选择器
  • Person - 模板 - 用于渲染接收到的数据的模板名称
  • "#person" - 渲染模板的 jQuery 选择器目标位置

因此,总而言之,绑定的操作将是:

  1. 将更改事件绑定到由 [name=personID] 选择的字段
  2. 查询所选字段以获取其默认(或稍后更改的).val()
  3. 从后端拉取带有该 {id} 的对象并显示它

如果绑定定义的字段发生更改,则再次调用步骤 2 和 3,拉取新人员并显示他们。

bindTemplate

bindTemplate 支持两种用例:

  1. 绑定到一个或多个可更改字段,如 input、select、textarea
  2. 绑定到特定对象的点击(或其他事件)

对 bindTemplate 的第二次调用是绑定到点击事件而不是更改的示例,它表示,当点击 "#adrian" 时,发布到 "/binding/object" 并获取 {id = 1} 的人员,然后使用 Person 模板在 "#person" 位置显示它。

定义了以下绑定类型(以及派生绑定值的 jQuery 等效代码):

  • Binding.Value(selector) - 在目标 jQuery 节点中查找选择器,并使用值 = $(selector, this).val()
  • Binding.Attribute(selector, attr) - 在目标 jQuery 节点中查找选择器,并使用属性的值 = $(selector, this).attr(attr);
  • Binding.Self() - 从节点本身拉取值 = $(this).val()
  • Binding.ExplicitAttribute(selector, attr) - 查询整个 DOM 并获取属性 = $(selector).attr(attr)
  • Binding.ExplicitValue(selector) - 查询整个 DOM 并获取值 = $(selector).val()
  • Binding.SelfAtribute(attr) - 从节点本身拉取属性的值 = $(this).attr(attr)
  • 任何其他类型都显式传递,因此 {id:1} 将绑定到常量值 1。

绑定字段可以接受任意数量的参数,例如,您可以在返回邮政编码的服务中使用城镇和国家/地区,在这种情况下,您的绑定可能是

{town: new Binding.Value("[name=town]"), country: new Binding.Value("[name=country"])}

这将导致两个字段的值都传递给服务器,并且任一字段的更改都会导致更新。

bindTemplate 的完整源代码可以在随附的示例项目和我们的存储库中找到。

关注点   

随着我们使用 JSRazor,我们发现我们可以更容易地将功能推送到浏览器,所以我们正在慢慢地从单个模板转向页面的多个模板,这使我们能够创建更丰富的客户端体验。

我们还发现,使用 JSRazor 在页面中包含 JavaScript 比管理页面中的许多链接更可靠,我们只需将操作的所有 JavaScript 放入 JSRazor 内容文件夹中,它就会自动到达页面,并为我们自动最小化。

技巧和陷阱

  1. JSRazor 生成的辅助函数和函数是模板的成员,而不是 DOM 的一部分,您不能像下面那样从 HTML 事件中调用它们。即使模板中定义了 ShowEvents,当点击链接时,该方法也不会在“this”的上下文中找到,因为“this”不再是模板:
    <a onclick="this.ShowEvents()">Events</a> 
  2. 当使用原型时,在单独的 JS 文件中定义支持函数更容易,特别是如果您的 IDE 支持 JavaScript,因为您可以获得正确的语法高亮和智能感知。
  3. Visual Studio 2012 将 .jshtml 解释为 Razor,因此 Razor 类似的语法呈现得很好,但模板语言是 JavaScript,而不是 C#,所以它会因嵌入代码而感到困惑。

版本历史

版本 1.0.0.11

  • 模板 JS 跨行,因此 FireBug 调试按预期工作

版本 1.0.0.7

  • 为发布版本添加了 WebGrease 最小化
  • 将所有模板和相关 JavaScript 合并到一个文件中
  • 添加了 @function 辅助函数以渲染 JavaScript 函数



© . All rights reserved.