September 2015

第 30 卷,第 9 期

数据点 - 再探 JavaScript 数据绑定(现在使用 Aurelia)

作者 Julie Lerman

Julie Lerman我对前端开发者的工作从来都不太感兴趣,但时不时会去研究下 UI。我在看到 Knockout.js 上的用户组呈现后,便进行了深入研究,并在我的 2012 年 6 月专栏中,撰写了一篇有关在网站中使用 Knockout 实现 OData 数据绑定的文章 (msdn.microsoft.com/magazine/jj133816)。几个月后,我撰写了有关如何将 Breeze.js 添加到组合中,以简化通过 Knockout.js 实现的数据绑定的文章 (msdn.microsoft.com/magazine/jj863129)。我在 2014 年撰写了有关如何现代化旧的 ASP.NET 2.0 Web 窗体应用并再次使用 Knockout 的文章,有些朋友取笑我说,Knockout 都是“2012 年的东西了”。 Angular 等较新的框架也可以执行数据绑定及更多任务。但我对“执行更多任务”并不感兴趣,Knockout 足矣。

那么,现在是 2015 年了,尽管对于 JavaScript 数据绑定而言,Knockout 依旧有效、相关且精彩绝伦,但我花了一些时间来研究这些新框架中的一种,并选择了 Aurelia (Aurelia.io),因为我知道有许多 Web 开发者对 Aurelia 很感兴趣。Aurelia 最先由 Rob Eisenberg 推出,他是另一个 JavaScript 客户端框架 Durandal 的缔造者,不过他停止了对 Durandal 的研究,转而去了 Google 的 Angular 团队。最终,他离开了 Angular 并选择从头开始创建 Aurelia,而不是振兴 Durandal。关于 Aurelia,有许多有趣的事情。虽然我还有很多东西要学习(这是当然),但我想与大家分享一些我知道的数据绑定诀窍,以及 EcmaScript 6 (ES6)(JavaScript 的最新版本,并于 2015 年 6 月成为标准)的一点使用技巧。

用于向我的网站提供数据的 ASP.NET Web API

我使用的是自己构建的 ASP.NET Web API,以便公开我使用 Entity Framework 6 持久保存的数据。此 Web API 包含几个可通过 HTTP 调用的简单方法。

Get 方法(如图 1 所示)会接收一些查询和分页参数,并将这些传递到存储库方法,此方法使用 Entity Framework DbContext 检索 Ninja 对象及其相关 Clan 对象的列表。在 Get 方法收到结果后,它会将这些结果转换成一组在其他位置定义的 ViewListNinja 数据传输对象 (DTO)。这是重要的一步,因为 JSON 的序列化方式包含从 Clan 回到其他 Ninjas 的循环引用,这显得有点过度操作。借助 DTO,我可以避免浪费网络数据传输量,并能生成更符合客户端最新动态的结果。

图 1:Web API 中的 Get 方法

public IEnumerable<ViewListNinja> Get(string query = "",
  int page = 0, int pageSize = 20)
  {
    var ninjas = _repo.GetQueryableNinjasWithClan(query, page, pageSize);
    return ninjas.Select(n => new ViewListNinja
                              {
                                ClanName = n.Clan.ClanName,
                                DateOfBirth = n.DateOfBirth,
                                Id = n.Id,
                                Name = n.Name,
                                ServedInOniwaban = n.ServedInOniwaban
                              });
    }

图 2 展示了此方法根据检索了两个 Ninja 对象的查询生成的 JSON 结果视图。

通过 Web API 的 Ninjas 列表请求生成的 JSON 结果
图 2:通过 Web API 的 Ninjas 列表请求生成的 JSON 结果

使用 Aurelia 框架查询 Web API

Aurelia 范例将一个视图模型(JavaScript 类)与一个视图(HTML 文件)配对,并在二者之间执行数据绑定。因此,我准备了 ninjas.js 文件和 ninjas.html 文件。Ninjas 视图模型的定义为拥有 Ninjas 数组和 Ninja 对象:

export class Ninja {
  searchEntry = '';
  ninjas = [];
  ninjaId = '';
  ninja = '';
  currentPage = 1;
  textShowAll = 'Show All';
  constructor(http) {
    this.http = http;
  }

ninjas.js 中最关键的方法是 retrieveNinjas,它可调用 Web API:

retrieveNinjas() {
  return this.http.createRequest(
    "/ninjas/?page=" + this.currentPage +
    "&pageSize=100&query=" + this.searchEntry)
    .asGet().send().then(response => {
      this.ninjas = response.content;
    });
  }

在 Web 应用中的其他地方,我设置了基 URL,以供 Aurelia 查找并合并到请求的 URL 中:

x.withBaseUrl('http://localhost:46534/api');

值得注意的是,ninjas.js 文件只是 JavaScript。如果您已经使用过 Knockout,您可能记得,您需要使用 Knockout 表示法设置视图模型,以便当对象绑定到标记时,Knockout 将知道该如何处理此对象。不过,这并不适用于 Aurelia。

响应现在包括 Ninjas 列表,我将其设置为我的视图模型的 ninjas 数组,它会返回至触发相应请求的 ninjas.html 页面。用于标识模型的标记中没有任何内容,由于模型与 HTML 配对,因此受到关注。事实上,网页的大部分内容包含的是标准 HTML 和一些 JavaScript,只有少量特殊命令可供 Aurelia 查找和处理。

数据绑定、字符串内插和格式设置

ninjas.html 的最有意思的部分是 div,它用于显示 ninjas 列表:

<div class="row">
  <div  repeat.for="ninja of ninjas">
    <a href="#/ninjas/${ninja.Id}" class="btn btn-default btn-sm" >
      <span class="glyphicon glyphicon-pencil" />  </a>
    <a click.delegate="$parent.deleteView(ninja)" class="btn btn-default btn-sm">
      <span class="glyphicon glyphicon-trash" />  </a>
    ${ninja.Name}  ${ninja.ServedInOniwaban ? '[Oniwaban]':''}
    Birthdate:${ninja.DateOfBirth | dateFormat}
  </div>
</div>

此代码中的第一个 Aurelia 标记是 repeat.for"ninja of ninjas",其遵循 ES6 范例进行循环。由于 Aurelia 包括视图模型,因此它知晓“ninjas”是定义为数组的属性。可以对变量“ninja”进行任意命名,例如“foo”。 它只是表示 ninjas 数组中的每一项。现在,这只关乎遍历 ninjas 数组中的项。向下跳至显示这些属性的标记,例如“${ninja.Name}”。 这是 Aurelia 利用的 ES6 功能,称为字符串内插。字符串内插通过将变量嵌入其中(而不是通过其他方式,例如,连接),让字符串的编写变得更加容易。因此,对于变量名称“Julie”,我可以用 JavaScript 进行编写:

`Hi, ${name}!`

它会作为“Hi, Julie!”得到处理。 Aurelia 会利用 ES6 字符串内插,并在遇到相应语法时推断单向数据绑定。因此,以 ${ninja.Name} 开头的最后一行代码将输出 ninja 属性,以及 HTML 文本的其余部分。如果您是用 C# 或 Visual Basic 编写代码,请注意,字符串内插是 C# 6.0 和 Visual Basic 14 的新功能。

在此期间,我确实要多了解一点 JavaScript 语法,例如,ServedInOniwaban 布尔的条件评估,它具有与 C# 相同的语法 (condition? true : false)。

我应用到 DateOfBirth 属性的日期格式是另一项 Aurelia 功能,如果您使用的是 XAML,则可能会感到熟悉。Aurelia 使用值转换器。我喜欢使用目前的 JavaScript 库来帮助完成日期和时间格式设置,并在 date-format.js 类中利用这一功能:

import moment from 'moment';
export class dateFormatValueConverter {
  toView(value) {
  return moment(value).format('M/D/YYYY');
  }
}

请注意,在类名称中,您确实需要使用“ValueConverter”。

在 HTML 页面的顶部(在初始 <template> 元素的正下方),我对相应文件进行了引用:

<template>
  <require from="./date-format"></require>

现在,字符串内插能够在我的标记中找到 dateFormat­[ValueConverter],并将它应用于输出,如图 3 所示。

显示所有 Ninjas,其中包含通过字符串内插由 Aurelia 实现单向绑定的属性
图 3:显示所有 Ninjas,其中包含通过字符串内插由 Aurelia 实现单向绑定的属性

我想指出的是 div 中的另一个绑定实例,但它是事件绑定,而不是数据绑定。请注意,在第一个超链接标记中,我使用了常见语法,同时在 href 特性中嵌入了 URL。不过,在第二个标记中,我没有使用 href。相反,我使用了 click.delegate。虽然 Delegate 不是新命令,但 Aurelia 却按特殊方式对其进行处理,这种方式要比标准的 onclick 事件处理程序强大得多。若要了解详情,请访问 bit.ly/1Jvj38Z。我将继续侧重介绍与数据相关的绑定。

编辑图标会将您引导至包含 ninja 的 ID 的 URL。我已经指示 Aurelia 路由机制路由至 Edit.html 网页。这与 Edit.js 类中的视图模型绑定。

Edit.js 的最关键方法用于检索和保存选定的 ninja。让我们从 retrieveNinja 开始:

retrieveNinja(id) {
  return this.http.createRequest("/ninjas/" + id)
    .asGet().send().then(response => {
      this.ninja = response.content;
    });
  }

和先前一样,这会生成对我的 Web API 的类似请求,但这一次请求中追加有 ID。

在 ninjas.js 类中,我将结果绑定至我的视图模型的 ninjas 数组属性。此时,我要将这些结果(单个对象)设置为当前视图模型的 ninja 属性。

下面是由于追加到 URI 的 ID 而会被调用的 Web API 方法:

public Ninja Get(int id)
  {
    return _repo.GetNinjaWithEquipmentAndClan(id);
  }

此方法生成的结果要比针对 ninja 列表返回的结果丰富得多。图 4 展示了通过其中一个请求返回的 JSON。

一个 Ninja 的 WebAPI 请求的 JSON 结果
图 4:一个 Ninja 的 WebAPI 请求的 JSON 结果

在我将这些结果推送到我的视图模型中之后,我便可以将 ninja 的属性绑定至 HTML 页面中的元素。这次,我使用的是 .bind 命令。Aurelia 将推断是应该单向绑定,还是双向绑定,亦或是通过其他某种方式绑定。实际上,如您在 ninjas.html 中所见,它会在发现字符串内插时使用其基础绑定工作流。在这种情况下,它使用了一次性单向绑定。此时,因为我使用的是 .bind 命令并将绑定至输入元素,所以 Aurelia 推断我需要的是双向绑定。这是它的默认选择,我可以使用 .one-way 或其他命令来替代 .bind 命令,以此来覆盖此选项。

为了简洁起见,我将只提取相关的标记,而不包括四周的元素。

下面是一个输入元素,它绑定到了模型中 ninja 属性的 Name 属性,该模型是我从 modelview 类传回的:

<input value.bind="ninja.Name" />

下面是另一个输入元素,这一次它绑定到了 DateOfBirth 字段。我希望能利用我之前学到的语法,轻松地重复使用日期格式值转换器,即使是在这样的上下文中:

<input  value.bind="ninja.DateOfBirth | dateFormat"  />

我还想在同一网页上列出设备(方法类似于我是如何列出 ninjas 的),以便可以对其进行编辑和删除。为了方便本文演示,我甚至将此列表显示为字符串,但我尚未实现编辑和删除功能,也并未实现设备添加功能:

<div repeat.for="equip of ninja.EquipmentOwned">
  ${equip.Name} ${equip.Type}
</div>

图 5 展示了具有数据绑定的窗体。

显示设备列表的编辑页面
图 5:显示设备列表的编辑页面

Aurelia 还具有一项名为自适应绑定的功能,可允许其根据浏览器的功能或传入的对象调整绑定功能。这个设计太酷了,即便浏览器和库随着时间的推移而不断演进,它也始终可以与二者配合使用。有关自适应绑定的详细信息,请访问 bit.ly/1GhDCDB

目前,您只能编辑 ninja 名称、出生日期和 Oniwaban 指示器。当用户取消选中“在 Oniwaban 中提供”并单击“保存”按钮后,此操作会调用我的视图模型的 save 方法。在我将数据推送回我的 Web API 之前,此方法会执行些有趣的操作。目前,如您在图 4 中所见,ninja 对象是一个深层图形。我无需发送回所有内容进行保存,只需发送回相关属性。由于我在另一端使用的是 EF,因此我想确保我没有编辑的属性也会返回,以便其不会被数据库中的 null 值取代。所以,我要创建一个名为 ninjaRoot 的动态 DTO。我已经将 ninjaRoot 声明为我的视图模型的属性。不过,ninjaRoot 的定义将通过我在 Save 方法中生成它的方式予以表明(见图 6)。我一直很小心地使用我的 WebAPI 预期的相同属性名称和大小写,以便其可以将这些内容反序列化为 API 中的已知 Ninja 类型。

图 6:编辑模型视图中的 Save 方法

save() {
        this.ninjaRoot = {
          Id: this.ninja.Id,
          ServedInOniwaban: this.ninja.ServedInOniwaban,
          ClanId: this.ninja.ClanId,
          Name: this.ninja.Name,
          DateOfBirth: this.ninja.DateOfBirth,
          DateCreated: this.ninja.DateCreated,
          DateModified: this.ninja.DateModified
        };
        this.http.createRequest("/ninjas/")
          .asPost()
          .withHeader('Content-Type', 'application/json; charset=utf-8')
          .withContent(this.ninjaRoot).send()
          .then(response => {
            this.myRouter.navigate('ninjas');
          }).catch(err => {
            console.log(err);
          });
    }

请注意此调用中的“asPost”方法。这可确保将请求转到我的 Web API 中的 Post 方法:

public void Post([FromBody] object ninja)
{
  var asNinja =
    JsonConvert.DeserializeObject<Ninja>
    (ninja.ToString());
  _repo.SaveUpdatedNinja(asNinja);
}

JSON 对象反序列化为本地 Ninja 对象,然后传递给我的存储库方法,此方法知道对数据库中的这一对象进行更新。

当我在我的网站上返回到 Ninjas 列表后,更改也会反映在输出中。

不仅仅是数据绑定

请注意,Aurelia 是一个功能更加丰富的框架(不仅仅只有数据绑定),但出于我的本性,我在探究此工具的初期阶段将重点放在了数据上。您可以通过 Aurelia 网站了解到更多信息,而 gitter.im/Aurelia/Discuss 则是一个活跃的社区。

我要衷心感谢 tutaurelia.net 网站,上面的系列博客探讨了如何将 ASP.NET Web API 和 Aurelia 结合使用。依赖于作者 Bart Van Hoey 的第 6 部分,我第一次显示了 ninjas 列表。可能在本文发表时,这一系列中会新增更多博文。

本文的下载内容包括 Web API 解决方案和 Aurelia 网站。您可以在 Visual Studio 中使用 Web API 解决方案。我已使用 Entity Framework 6 和 Microsoft .NET Framework 4.6 在 Visual Studio 2015 中构建了网站。如果您想运行此网站,则需要访问 Aurelia.io 站点,了解如何安装 Aurelia 和运行此网站。您还可以在我的 Pluralsight 课程“Entity Framework 6 入门”(bit.ly/PS EF6Start) 中,观看我对此应用程序的演示。


Julie Lerman 是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 .NET 主题的演示。她是《Programming Entity Framework》(2010) 以及“代码优先”版 (2011) 和 DbContext 版 (2012)(均出自 O’Reilly Media)的作者,博客网址为 thedatafarm.com/blog。通过她的 Twitter(网址为 twitter.com/julielerman)关注她,并在 juliel.me/PS-Videos 上观看其 Pluralsight 课程。


衷心感谢以下技术专家对本文的审阅: Rob Eisenberg (Durandal Inc.)
Rob Eisenberg 专注于用户界面体系结构和工程已十年。他是多个 UI 框架的创建者,如全球数千名开发者采用的 Caliburn.Micro 和 Durandal。作为 Angular 2.0 团队的前成员,Rob 目前是 Aurelia 项目的负责人。Aurelia 是面向移动、桌面和 Web 的下一代 JavaScript 客户端框架,通过简单约定即可激发您的无穷创造力。有关更多信息,请访问 http://aurelia.io/