Knockout-js-Web-开发-全-

Knockout.js Web 开发(全)

原文:zh.annas-archive.org/md5/6b99267fda1a75d6dc87c17a215139ea

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

好的工具使互联网变得响应和互动,并将控制权交到了开发者手中。Knockout 库因其是一对在拳击场上表现良好的拳击手套而闻名!多年前 jQuery 为 JavaScript 带来的,Knockout 正在为智能数据管理中的浏览器元素带来。有了这个库,你将找到另一种享受构建网络技术的方式,同时构建的网站最终用户也会同样喜欢。如果你了解 JavaScript 和 HTML 的基础知识,这本书很快就会让你成为冠军。

本书涵盖的内容

第一章, KnockoutJS 入门,教你什么是 Knockout 以及为什么它与众不同。这一章让你开始编码,并打开了一条更简单、更强大的构建网页的方法。

第二章, 使用数组、嵌套和网格,继续打开你的思路,用更少的代码获得更多的力量。这一章特别关注数据集合的动态排序,这将把强大的编码能力交到新开发者手中。

第三章, 给表单添加 Knockout 风格,展示了如何使用 Knockout 使我们的网页表单更容易、更有趣地构建。这包括简单和基于网格的表单。

第四章, 编码 – AJAX、绑定属性、映射和工具,证明了 Knockout 不仅仅是一个独立的解决方案。它是一种连接并简化我们交互的技术,并且它拥有许多令人兴奋的插件,让你渴望在项目中使用它。

第五章, 模板的乐趣,涵盖了原生模板、增强的集合处理、渲染事件、第三方模板以及一些其他模板选项。

第六章, 打包的优雅,带你进入网络开发的未来。这一章涵盖了当今在所有流行浏览器中自定义组件的用法。我们将利用这个机会介绍另一种构建简化且强大的单页应用(SPA)的方法,以及解决方案。

为本书所需的条件

首先,你需要一台装有网络服务器的电脑。示例将在任何常见的服务器上运行,因此不需要特殊的服务器端语言。你需要一个文本编辑器,如果你想要使用练习文件,你需要能够解压缩 ZIP 文件。大多数现代电脑都内置了这一功能。最后,你需要一个浏览器来查看你的作品。

本书面向的对象

这本书是为使用 HTML 的 Web 开发人员或 Web 设计师而写的,他们希望在简化事物的同时获得知识。这将向您展示如何通过一些简单的标记自动化您数据与浏览器视觉部分之间的交互。如果您希望获得更干净、更可持续的代码的最佳体验,这本书将非常适合您。

术语约定

在这本书中,您将找到许多不同的文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“我们需要使用我们绑定数组集合的push方法来添加这个。”

代码块设置如下:

<button data-bind="click: sort('name')">Sort By Name</button>
<button data-bind="click: sort('item')">Sort By Item</button>

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

this.orderVM = new ko.simpleGrid.viewModel({
data: this.orders,
columns: [
{ headerText: "Customer", rowText: "name"},
{ headerText: "Item", rowText: "item"},
{ headerText: "Count", rowText: "qty"}
],
pageSize: 3
});

任何命令行输入或输出都按照以下方式编写:

> pagedOrderModel.orders()

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“点击添加员工按钮。”

注意

警告或重要注意事项以如下框的形式出现。

技巧

技巧和窍门如下所示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。

要发送给我们一般性的反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果您在某个主题领域有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 图书的骄傲拥有者,我们有一些东西可以帮助您从您的购买中获得最大价值。

下载示例代码

您可以从www.packtpub.com下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

错误清单

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接mailto:copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。

询问

如果您对本书的任何方面有问题,您可以通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决问题。

第一章. 使用 KnockoutJS 入门

欢迎来到 KnockoutJS 世界中的力量与奇迹。在本章中,我们将开始踏上使用 AJAX HTML 应用程序的道路。本章将重点关注:

  • 安装 KnockoutJS

  • 理解 MVVM 的含义

  • 使用 Knockout 库绑定元素

  • 使用现代浏览器中的开发者工具加速和简化编码

  • 使用 Knockout 创建消费函数

  • 使用 Knockout 直接自动化值的计算

  • 在 Knockout 之外使用函数

安装 KnockoutJS

首先,确保你在服务器上设置了一个可以正常工作的网站。如果你只想学习那些功能,或者使用任何现成的生产型网络平台(如 ASP.NET、ColdFusion/Railo、NodeJS、PHP、Python、Ruby 等),它可以是简单的 HTML;只需在安装 Knockout 之前确保服务正在运行。然而,有一个例外。如果你使用 Knockout 为 PhoneGap 或类似的应用程序创建 HTML 应用程序,那么你不需要 Web 服务器来处理内部功能。

安装 Knockout 有多种方式。你需要选择最适合你的方式。本书文件已被打包成一个 ZIP 文件,可在 knockout.developers.zone/books/knockout-js-web-development/ 下载。点击链接下载产品的当前稳定版本。如果有任何需要修复的,我们将保持更新。我们还会在该页面上创建一个视频,展示如何在 Mac 和 Windows 上至少安装这些内容。

如果你直接从 Knockout 网站下载页面 knockoutjs.com,将文本复制并粘贴到你的网站文件夹中的 JS 文件中。

现在,可能还有其他人将来会使用更复杂的工具。有几个包安装器会拉取所需的文件。如果你使用这些工具来安装本书,请检查 KnockoutJS 的版本是否为 3.2 或更高。我建议除非你已经熟悉它们,否则现在不要使用这些工具。了解它们是有价值的,但不是运行 Knockout 所必需的。

我的示例中包含了 Bootstrap 库,因为我想要改善展示效果,并为本书的页面赋予独特的个性,这也可以说成是使创建这个资源对我而言更加有趣的一种方式。

我将本书中的所有示例和练习链接到网站根目录。我建议你在那里学习如何使用 Knockout,并在转向基于 Web 的应用程序开发时使用这些知识。

文件加载完成后,你应该能够点击几个示例。我的一个目标是将课程设置得如此,以便你在完成课程后,需要查找如何使用 Knockout 做不同事情时,可以将此作为快速参考文档。

您的工作页面应该放在与本书章节编号相匹配的适当文件夹中。我已经在done文件夹中创建了完整的工作示例,您的应该放在对应章节编号下的do文件夹中。

查看 MVVM

MVVM 是一种构建软件的设计方法。这个名字有点令人困惑,会让你想知道他们是否试图清晰表达,或者只是喜欢这些字母创造的对称性。这种设计模式在微软和苹果的开发产品中被广泛使用。

MVVM代表模型-视图-视图模型

让我们从视图开始。这是 HTML 被转换为文档对象模型DOM)的地方。

视图与一个名为 ViewModel 的对象交互。ViewModel是存储展示逻辑的地方。Model是存储数据和业务逻辑的部分。这通常以 JSON 对象的形式传递到浏览器中。

让我们澄清一个令人困惑的点。当绘制图表时,大多数人会将关系绘制为 View-ViewModel-Model,这将变成 VVMM。不要陷入语义的陷阱;只需看看以下图表,了解他们所说的 MVVM 是什么意思:

查看 MVVM

您的数据存储在 Model 中,但您并不直接与数据交互。您使用 ViewModel 与数据交互。您还应该注意,您不需要成为 MVVM 模式的专家,因为在实践中这些概念非常简单。这一点的证据就是将元素绑定到 ViewModel 的简单性。

使用 Knockout 绑定 DOM 元素

将您的视图元素绑定到 ViewModel 有两种基本方法。您可以通过元素的data-bind属性绑定,或者使用 JavaScript 中的友好代码。让我们首先在\ko\ko-1\do\文件夹中创建一个名为binding.htm的页面。以下是本书中所有do页面的基本标准代码:

<!DOCTYPE html>
<html lang="en">
<head> 
</head>
<body>
<script src="img/knockout.js"></script>
<script type="text/javascript">
var viewModel = {
}
ko.applyBindings(viewModel);
</script>
</body>
</html>

提示

下载示例代码

您可以从www.packtpub.com下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

使用 Knockout 的第一步是包含库的 JS 文件。然后我们需要创建 ViewModel。在这个时候,ViewModel 类似于一个类文件,因为它在我们使用 Knockout 的applyBindings方法绑定之前不做任何事情。对于 Knockout 来说,Model 的名称并不重要,只要它不与 JS 或其他当前运行的库冲突即可。

绑定文本

要将文本绑定到 ViewModel,请按照以下步骤操作:

  1. 将以下代码添加到 ViewModel 中:

    var viewModel = {
      myVariable: ko.observable()}
    
  2. 我们将使用data-bind属性来告诉 Knockout 如何通过 ViewModel 绑定我们的数据。我们将此 DOM 元素的文本属性绑定到 ViewModel 变量myVariable。在body标签后添加以下代码:

    My first KnockoutJS uses text binding: <span data-bind="text: myVariable"></span>
    <hr>
    
  3. 现在我们已经准备好运行代码。当你查看浏览器中的代码时,似乎没有任何事情发生。这是因为 ViewModel 变量没有分配任何值,所以没有东西被注入到视图中。它在浏览器窗口中给出了以下文本:绑定文本

  4. 让我们在绑定 ViewModel 的脚本中的下一行添加一行:

    ko.applyBindings(viewModel);
    viewModel.myVariable("Awesome simple!");
    

代码给出了要分配的值,当我们运行页面时,它显示了绑定到 DOM 元素的数据。以下文本是浏览器窗口中的输出:

绑定文本

在这里,我们可以看到当变量更新时,HTML DOM 元素的文本属性也更新了。没有必要直接更新 HTML DOM 元素,因为 Knockout 的 ViewModel 功能自动化了这一功能。在大型和复杂的数据集中,Knockout 已被测试为最快的绑定库。当然,这种考虑可能会随时间而改变。

绑定 HTML

要将 HTML 绑定到 ViewModel,请使用以下步骤:

  1. 将 HTML 代码添加到 ViewModel 中:

    var viewModel = {
      myVariable: ko.observable(),
      myHTML: ko.observable()
    }
    
  2. 在绑定 ViewModel 之后设置myHTML变量的值:

    viewModel.myVariable("Awesome simple!");
    viewModel.myHTML("<strong>Awesome</strong> simple!");
    
  3. 现在,我们需要绑定 DOM 元素的 HTML 属性。正如你所看到的,语法与我们上次使用的文本绑定非常相似:

    My second KO uses HTML binding:
    <div data-bind="html: myHTML"></div>
    <hr>
    

如果我们将 HTML 传递给文本元素,它将无法正确显示,这就是为什么 DOM 有一个特定的 HTML 属性用于适当的标签。当我们使用文本方法时,Knockout 会转义结果,并使用 HTML 将结果放置在编辑器中看起来一样。HTML 属性并不总是存在于文本属性存在时,但当我们在这个点上找到 HTML 时,可以相当安全地假设文本确实存在。当我们再次渲染时,Knockout 将文本渲染如下所示:

绑定 HTML

绑定 CSS

要将 CSS 绑定到 ViewModel,请按照以下步骤操作:

  1. myLeftmyRight变量添加到 ViewModel 中:

    var viewModel = {
      myVariable: ko.observable()
      ,myHTML: ko.observable()
      ,myLeft: ko.observable()
      ,myRight: ko.observable()
    }
    
  2. 在绑定 ViewModel 之后设置myLeftmyRight变量的值:

    viewModel.myVariable("Awesome simple!");
    viewModel.myHTML("<strong>Awesome</strong> simple!");
    viewModel.myLeft("pullLeft");
    viewModel.myRight("pushRight");
    
    
  3. 使用 CSS 属性到data-bind设置,通过 ViewModel 动态管理 CSS。这可以在任何时候更改,元素会根据浏览器如何响应这些 CSS 设置来反映 CSS 设置。

    <div data-bind="css: myLeft">LEFT&nbsp;</div>
    <div data-bind="css: myRight">&nbsp;RIGHT</div>
    My third KO uses CSS binding:
    <hr>
    

当我们再次渲染时,Knockout 将文本渲染如下所示:

绑定 CSS

绑定数字

以下步骤将解释如何将数字绑定到 ViewModel:

  1. 将以下代码添加到 ViewModel 中:

    var viewModel = {
      myVariable: ko.observable()
      ,myHTML: ko.observable()
      ,myLeft: ko.observable()
      ,myRight: ko.observable()
      ,myBalance: ko.observable()
    }
    
  2. 在绑定 ViewModel 之后设置myBalance变量的值:

    viewModel.myVariable("Awesome simple!"); 
    viewModel.myHTML("<strong>Awesome</strong> simple!");
    viewModel.myLeft("pullLeft");
    viewModel.myRight("pushRight");
    viewModel.myBalance(-47.23);
    
    

在这里,我们探索我们的第一个data-bind,我们通过 HTML 标记同时绑定到多个设置。请注意,我们还用额外的外部元素包装了该元素,以便我们可以根据余额是否为负来设置颜色。当我们这样做时,我们可以在设置中插入一些 JavaScript。当使用 JavaScript 时,我们将其作为函数而不是变量来引用myBalance,因为这是我们与 JavaScript 交互的方式。请看以下代码:

My fourth KO uses Style binding:<br>
Balance = <span data-bind="style: { color: myBalance() < 0 ? 'red' : 'black' }"><span data-bind="text:myBalance"></span></span>
<hr>

当我们再次渲染时,Knockout 将文本渲染如下:

绑定数字

管理可见性

要管理 ViewModel 中元素的可见性,请使用以下步骤:

  1. 将以下代码添加到 ViewModel 中:

    var viewModel = {
      myVariable: ko.observable()
      ,myHTML: ko.observable()
      ,myLeft: ko.observable()
      ,myRight: ko.observable()
      ,myBalance: ko.observable()
      ,isVisible: ko.observable()
    }
    
  2. 在 ViewModel 绑定后设置isVisible变量的值:

    viewModel.myVariable("Awesome simple!"); 
    viewModel.myHTML("<strong>Awesome</strong> simple!");
    viewModel.myLeft("pullLeft");
    viewModel.myRight("pushRight");
    viewModel.myBalance(-47.23);
    viewModel.isVisible(true);
    
    
  3. 脚本可以是一个非常强大的技术,随着 Knockout 技能的提升而使用。它可以增加自动化和用户体验的价值。在body标签后插入以下代码:

    My fifth KO uses visible binding:
    <div data-bind="visible: isVisible">Warning, the visible property is set to true.</div>
    <hr>
    

当我们再次渲染时,Knockout 将文本渲染如下。当然,尝试将值更改为false并再次运行,以查看它是否正确地为您工作。

管理可见性

多绑定控件

这次 ViewModel 不需要更新,因为我们讨论的技术是从 HTML DOM 元素侧管理的。我们需要设置颜色和文本的data-bind变量的值如下:

viewModel.myVariable("Awesome simple!");
viewModel.myHTML("<strong>Awesome</strong> simple!");

在这里,我们探索我们的第一个data-bind,我们通过 HTML 标记同时绑定到多个设置。使用括号形式,我们在标记中嵌入了少量的 JavaScript。再次提醒,当您使用 JavaScript 功能时,您必须将 ViewModel 属性作为函数而不是变量来处理。这对于新 Knockout 开发者来说是一个常见的疏忽。以下是添加到body标签后的代码:

My sixth KO uses multi-binding:
Balance = <span data-bind="style: { color: myBalance() < 0 ? 'red' : 'black' },text:myBalance"></span>

当我们再次渲染时,Knockout 将文本渲染如下:

多绑定控件

尝试更改数字并运行它,以使数字显示黑色和红色,当然,这取决于代码中正确的数字设置。您甚至可以选择更改逻辑。

使用浏览器开发者工具进行功率开发

我建议使用 Google Chrome 中的工具来阅读这本书。第一个真正做得好的工具是 Firefox 中的Firebug工具。Firebug 仍然是一个很好的工具,Firebug 内置的开发者工具已经取得了巨大的进步。如果您是 Mac 用户,您也应该享受使用 Safari 中的工具。如果您是 Internet Explorer 的忠实粉丝,他们在开发者工具方面也做得很好。我的观点是,最好在所有地方都使用开发者工具,所以不要找借口不使用您正在使用的浏览器的开发者工具。

注意

如果你正在工作或想要学习如何构建 Chrome 扩展和应用程序,那么以下是如何启用开发者模式的方法。在 Chrome 浏览器中,点击地址栏旁边的菜单按钮。这是一个有三个线条堆叠在一起的图标。在侧边选择项下,点击扩展,你会在右上角看到一个开发者模式的复选框。在 Google 上搜索更多详细信息,因为这只是一个激发你兴趣的小贴士。

当在 Chrome 浏览器中时,你只需按 PC 上的F12键或在 Mac 上使用 Command + Alt + I来加载开发者工具。你还可以通过查看 Chrome 菜单来找到它们。这个按钮位于地址栏的右侧,有三个堆叠的线条。在更多工具下,你会看到开发者工具的选项。

你可以在那里看到 Windows、Mac,甚至是 Chrome OS 上的浏览器。当你点击它时,以下窗口会出现在浏览器底部:

使用浏览器开发者工具进行强大开发

我们不会深入探讨这个工具的所有功能,因为这需要整整一本书来介绍。我们只是会关注几个关键功能,这样在学习 Knockout 的同时更容易调试你的代码。你可能想要将工具从浏览器底部分离出来。要做到这一点,点击右上角关闭(x)按钮旁边的堆叠框。这会使工具出现在一个单独的窗口中。

目前我们主要关注的是控制台,所以如果你没有选中它,请点击开发者工具顶部的控制台。这会给你一个看起来非常类似于命令行提示符的提示。当你开始输入文本时,如果浏览器 DOM 中有活动变量或结构,它将带有代码提示将其显示出来。

使用浏览器开发者工具进行强大开发

选择viewModel然后按住句号。注意在提示中列出了你创建的所有模型属性,以及一些其他变量设置。选择viewModel.myHtml然后按Enter键。你看到的是一个函数。要获取所需的结果,需要在变量末尾添加括号——例如,viewModel.myHtml()——然后再次尝试。你可以全部重新输入,也可以使用上下箭头在所需的命令提示符条目之间循环,如果需要的话。这会返回 ViewModel 变量设置中当前持有的值。

使用浏览器开发者工具进行强大开发

现在我们将其提升到下一个层次,并直接从命令提示符使用相同的变量来更改值。我们需要做的是在按下Enter键时密切注意屏幕,因为我们可以看到通过 ViewModel 将数据绑定到视图的强大功能。在命令提示符中输入以下内容:

viewModel.myHTML("Knockout is <strong>Awesome</strong>!")

你应该看到的是我们输入的第二个绑定的 HTML 自动更新。这是一个比传统的 JavaScript 更容易与 View 一起工作的方法。对于那些想知道的人来说,它也非常兼容 jQuery。它可以与各种库一起使用,尽管在考虑它们是否匹配之前,你应该小心检查它们,以确保它们是一个好的组合。

注意

你还应该注意,虽然我们一直在将我们的 ViewModel 命名为viewModel,但它可以是任何有效的变量名。我们这样做只是为了帮助强化这个概念,即这是我们 ViewModel 的变量。

现在让我们使用命令控制台中的计算平衡。让我们尝试输入以下内容:

viewModel.myBalance()*2 

当我们这样做时,我们应该得到-94.46 作为结果,或者如果你输入了不同的数字,则可能是另一个数字。

Knockout 调试器

当你在元素视图中时,这是 Knockout 上下文显示的内容:

Knockout 调试器

该插件可在 Chrome Web Store 中找到。这是一个很棒的商店,许多值得拥有的项目都是免费的。其中之一是KnockoutJS 上下文调试器,它是在 MIT 许可下发布的。这个工具的源代码在 GitHub 上,如果你处理大型嵌套 ViewModel 时,这非常有用。它具有几个功能,在学习 Knockout 和用它构建网站时提供了极大的便利。要下载此插件,请按照以下步骤操作:

  1. 前往chrome.google.com/webstore/

  2. 在搜索框中输入knockout

  3. 扩展部分,你应该能看到 timstuyckens 的Knockoutjs 上下文调试器

另一件在处理 Knockout 或任何数据绑定库时会有所帮助的事情是追踪正在发生的事情的能力。但这个工具将我们的能力扩展到了这个能力之外。从开发者工具的顶部选择KnockoutJS,你会看到一个写着启用跟踪的按钮。点击它,然后转到控制台标签。现在输入以下代码,你将看到所有对 DOM 的更改都会在控制台窗口中显示出来:

for(i=0;i<10;i++){viewModel.myBalance(1.1*i);}

Knockout 调试器

快捷键

对于那些从未在浏览器中打开开发者工具的人来说,这可能是一个很大的进步,但让我们看看一些可以使使用这些工具的工作变得更好的事情。其中之一就是快捷键。就在打开工具到它们自己的窗口或将它们放回浏览器窗口底部部分的按钮旁边,有一个设置按钮。点击它,你会看到三个侧菜单项。最后一个是快捷键。浏览一下这个,它将帮助所有快捷键爱好者更快地完成任务。你还可以在developer.chrome.com/devtools/docs/shortcuts找到完整的快捷键列表,你可以将其打印出来,或者也许只是将页面添加到书签中。

DOM 检查

在包含我们div标签中“LEFT”内容的浏览器窗口中,右键单击它并选择检查元素。(在 Mac 上,您可以使用双指进行右键单击。)这会选择该元素,您将能够看到该元素可能存在的任何data-bind属性设置以及任何其他 DOM 功能。

您甚至可以与 DOM 玩耍,并在元素面板内部双击以实时编辑 DOM。实时编辑是我最喜欢的功能之一。您需要理解的是,当您通过脚本实例化它时,视图绑定到 ViewModel。您不能通过在该面板中编辑 DOM 代码来更改这一点。您还可以以惊人的方式编辑页面上的 CSS 和 JavaScript 进行测试和调试,在我看来,这是必须学习的开发者技能。

AJAX 检查

当我处理 AJAX 时,这是一个不可或缺的工具,可以查看使用网络选项卡发送和返回的内容。您可以看到列表中有一个 XHR 项目,它允许您查看在页面工作中出现的单个请求。

资源检查

我们在 SOSensible 公司,我的公司,构建了各种网络应用程序。一种网络应用程序允许用户使用网络应用程序离线。我们需要在基于浏览器的数据库存储中存储数据,而这个工具让我们能够在资源选项卡下检查所有这些资源。

设备模拟

在开发者工具的底部部分,有一个名为抽屉的选项。如果它没有显示,右上角的菜单按钮有一个按钮可以隐藏或显示抽屉。一旦可见,您将有一个模拟菜单项。这可以让您刷新屏幕,以显示特定设备大小和旋转的视图的近似效果。

侧边栏中还有一些项目可以帮助其他功能。有一个名为设备的设置,用于选择要模拟的设备。有一个名为屏幕的设置,用于管理旋转以及您可能想要定制的许多其他功能。还有一个用户代理,可以欺骗服务器,使其认为它是一个不同的设备。最后,还有传感器,允许您设置地理位置和加速度计值。

使用 Knockout 的内部函数

在之前解释的绑定部分中,applyBindings方法使用了一种常见的结构。我们包含了这个例子,以防您遇到那种风格的代码。如果您在其他人的代码中看到使用该方法的代码,这将有助于您理解它。另一种编码风格是使用函数声明来声明模型。DOM 标记或视图代码在两种情况下都不会改变。让我们将我们的绑定示例转换为功能声明。复制binding.htm文件,并在同一文件夹中创建一个名为functions.htm的新文件。以下是原始声明:

var viewModel = {
  myVariable: ko.observable()
  ,myHTML: ko.observable()
  ,myLeft: ko.observable()
  ,myRight: ko.observable()
  ,myBalance: ko.observable()
  ,isVisible: ko.observable()
};

注意,我们可以在声明的同时立即设置可观察者的值。如果我们那样做了,它看起来会是这样:

var viewModel = {
  myVariable: ko.observable("Awesome simple!")
  ,myHTML: ko.observable("<strong>Awesome</strong> simple!")
  ,myLeft: ko.observable("pullLeft")
  ,myRight: ko.observable("pushRight")
  ,myBalance: ko.observable(-47.23)
  ,isVisible: ko.observable(true)
};

我们这样做是为了使本书的代码更加紧凑。你应该在选择代码中这种方式时使用良好的逻辑。有时使用这种编码风格很重要,但通常这只是程序员风格的问题。当编写代码时,要小心不要让你的风格阻碍你考虑哪种方式最好。

现在,我们将查看将代码移动到函数声明的过程。我们当然从不同类型的声明开始,因为这里是一个函数:

function viewModel() {
// add declarations here}

将 ViewModel 声明如下也是同样有效的。没有显著的区别:

viewModel = function() {
// add declarations here}

现在,我们将查看将我们的 ViewModel 项重新添加回去。在我们刚才描述的结构化方法中,项作为集合项以经典逗号分隔符输入。在这个模型中,每个项都是一个参数,并以分号结束:

viewModel = function() {
this.myVariable = ko.observable("Awesome simple!");
this.myHTML = ko.observable("<strong>Awesome</strong> simple!");
this.myLeft = ko.observable("pullLeft");
this.myRight = ko.observable("pushRight");
this.myBalance = ko.observable(-47.23);
this.isVisible = ko.observable(true);}

注意,我们使用this作用域声明声明了这些项,它指的是 ViewModel 对象。在 JavaScript 编程中,将this别名以避免作用域混淆是一种常见做法。我们将再次重写之前的代码,使用self而不是this作为基础作用域:

viewModel = function() {
  var self = this;
self.myVariable = ko.observable("Awesome simple!");
self.myHTML = ko.observable("<strong>Awesome</strong> simple!");
self.myLeft = ko.observable("pullLeft");
self.myRight = ko.observable("pushRight");
self.myBalance = ko.observable(-47.23);
self.isVisible = ko.observable(true);}

小贴士

注意,我们使用 var 声明设置了self变量。这防止了外部命名冲突的问题。

现在,如果我们从我们的浏览器运行functions.htm页面,它应该与我们的binding.htm文件运行相同。然而,这里有一个区别。这将帮助你理解为什么我们在课程中引入了开发者工具。打开工具,在命令提示符中,输入viewModel.isHTML()以查看你得到的结果:

使用 Knockout 的内部函数

预防隐藏功能

在之前的屏幕截图中,我们得到了你可能认为的意外结果,因为视图显然绑定到了 ViewModel 上。这里的问题是闭包的概念问题。如果你愿意,可以探索更多关于闭包的内容,但只需意识到这意味着对象的部分或项目内容存在但隐藏。当以这种方式声明时,你无法从 JavaScript 中与之交互。声明应该使用new来从函数创建对象,如下所示:

ko.applyBindings(new viewModel());

如果你现在运行浏览器并尝试连接到 ViewModel,你会看到它仍然存在闭包的相同问题。我们发现这是在我公司解决这个问题的最佳方式:

vm = new viewModel();
ko.applyBindings(viewModel);

现在,我们将使用vm而不是viewModel来引用模型,这将是我们得到的结果:

预防隐藏功能

我们看到,通过在传递给我们的 Knockout applyBindings 方法的参数外声明对象,我们避免了闭包问题。在使用 ViewModel 声明的结构化风格时,这不是一个问题。希望这能让你从数小时的代码问题或怀疑 Knockout 是否出错中解脱出来。我们不会回答我第一次遇到这个问题时浪费了多少时间,但那是在我开始使用 Knockout 之后很久。这说明即使是专家也可能犯新手错误。我谦虚地请求社区的帮助,答案很快就来了。

使用 Knockout 自动计算

在本节中,我们将函数提升到一个更深的层次。

在第一章的代码包中的 do 文件夹(/ko_1/do/),将 base.htm 文件复制为 computed.htm 用于此部分。在 body 标签之后,输入以下视图代码:

<table>
    <tr>
        <th style="width:20%;">
            Item
        </th>
        <th style="width:40%;">
            Qty
        </th>
        <th>
            Price
        </th>
        <th>
            Tally
        </th>
    </tr>
    <tr>
        <td style="width:20%;">
            <em data-bind="text:item_1">Item</em>
        </td>
        <td style="width:40%;">
            <input type="text" data-bind="value:qty_1" />
        </td>
        <td>
            <span data-bind="text:price_1"></span>
        </td>
        <td>
            <span data-bind="text:tally_1"></span>
        </td>
    </tr>
</table>

现在我们准备创建 Knockout ViewModel 代码的脚本部分。在包含 KnockoutJS 库之后,将此代码放入 script 标签内:

calcModel = function(){
    var self = this;
    self.item_1 = ko.observable('Cell Phone');
    self.qty_1 =  ko.observable(0);
    self.price_1 = ko.observable(149.95);
    self.tally_1 = ko.computed(function(){
        var rslt = (+self.qty_1() * self.price_1()).toFixed(2);
        return rslt;
    },self);
}
vm = new calcModel();
ko.applyBindings(vm);

注意,计算函数现在正在进行实时计算。我们还添加了两位固定小数位。输入一个数量进行测试:

使用 Knockout 自动计算

创建表格的第二行和与第一行特征匹配的 ViewModel。我希望你自己创建它,以确保你在这个过程中获得技能。当然,你可以先查看 done 文件夹中的示例,但你应该先自己尝试。

小计计算

现在将此行添加到表格底部,以创建一行用于小计。你还可以看到我们在表格中创建了一个单元格来总计 qty_1qty_2 中的项目数量:

<tr>
    <td style="width:20%;">
        &nbsp;
    </td>
    <td style="width:40%;">
        <span data-bind="text:t_qty"></span>
    </td>
    <td>
        <em>subTotal</em>
    </td>
    <td>
        <span data-bind="text:subTotal"></span>
    </td>
</tr>

你还需要将脚本代码放入正确的位置:

    self.item_2 = ko.observable('Cell Case');
    self.qty_2 = ko.observable(0);
    self.price_2 = ko.observable(19.45);
    self.tally_2 = ko.computed(function(){
        var rslt = (+self.qty_2() * self.price_2()).toFixed(2);
        return rslt;
    },self);
    self.t_qty = ko.computed(function(){
        return +(self.qty_1()) + +(self.qty_2());
    },self);
    self.subTotal = ko.computed(function(){
        var rslt = (+(self.tally_1()) + +(self.tally_2())).toFixed(2);
        return rslt;
    });

你可能已经注意到,我们在一些变量之前放置了一个额外的 +。这是标准的 JavaScript 方法,确保输入框中输入的数字以数字的形式输出。在某些浏览器和某些条件下,数字是数字的字符串表示。添加 + 解决了这个问题。

现在运行页面并输入数量,以确保所有计算都正常工作:

小计计算

纳税时间

没有支付税款,买东西还有什么乐趣?好吧,无论如何,这是我们都需要做对的功能。将以下代码添加到表格中,以创建一行用于税款:

    <tr>
        <td style="width:20%;">
            &nbsp;
        </td>
        <td style="width:40%;">
            ( rate  in decimal <input data-bind="value: taxRate;" /> )
        </td>
        <td>
            <em>Tax</em>
        </td>
        <td>
            <span data-bind="text:taxed"></span>
        </td>
    </tr>

现在将此代码添加到页面的 script 部分中,使我们的 ViewModel 运行得更加智能。我们的目标不是编写世界上效率最高的代码,而是让你学习如何以 Knockout 的方式思考:

    self.tax = ko.observable(.05);
    self.taxed = ko.computed(function(){
        return +(self.subTotal()*self.tax()).toFixed(2);
    });
    self.taxRate = ko.computed({
        read: function(){
            return self.tax()*100 + '%';
        },
        write: function(value){
            value = parseFloat(value.replace(/[^\.\d]/g, ""));
            self.tax(+value/100);
        }
    });

如果我们使用标准的 JavaScript 或甚至 jQuery 来编写页面代码以获得我们所实现的功能量,那么代码量将是我们在页面上现有代码的许多倍。这种方法更加优雅,且代码量更小。

注意我们的 taxRate 是如何稍微不同地实现的。Knockout 中的计算函数可以读写。你也可以调用其他外部资源,因为它是标准脚本代码。请注意,在我们的脚本代码中,我们必须将值放在括号中以进行赋值,就像我们对税值所做的那样。

你还应该注意到,我们在输入框中转换数值的十进制格式,并给显示的税值添加一个百分比。如果你在更新税率时输入了百分号,它还会智能地将其去除。

小贴士

类似于我们在 write 方法中使用的那种正则表达式是让你的应用更加强大的方式。确保至少学习基本的正则表达式。如果你不知道如何进行高级操作,你通常可以在谷歌搜索中找到所需的内容,或者某个“快乐的导师”会在在线论坛中自愿提供有价值的答案来帮助你。

刷新页面以应用更新,你将看到实际税额也已经在 总计 列中计算出来了:

税务时间

最终总计

在这里,我们有我们计算示例的视图代码的最后一部分:

<tr>
    <td style="width:20%;">
        &nbsp;
    </td>
    <td style="width:40%;">
        &nbsp;
    </td>
    <td>
        <em>Total</em>
    </td>
    <td>
        <span data-bind="text:total"></span>
    </td>
</tr>

最后一段脚本代码应该添加到 ViewModel 中。这部分代码除了其完成功能的能力外,没有其他特别之处。我们在这里添加了应税项目的值,并且再次用括号包裹数字,使用 toFixed 函数将答案设置为两位小数。

    self.total = ko.computed(function(){
        var rslt = (+self.subTotal() + self.taxed()).toFixed(2);
        return rslt;
    });

现在我们可以运行代码,并玩转输入框,以查看一切是否按预期工作。对于新开发者来说,看到制作这样一个页面所需的代码如此之少可能不会让你感到惊讶。过去,需要编写如此多的代码,以至于几乎没有人会花时间去尝试构建这样的工具。由于不同浏览器之间的差异,这个问题变得更加复杂。虽然这仍然是事实,但像 Knockout 这样的库消除了许多这些痛苦,并让我们能够专注于结果而不是平台。

现在运行代码应该会给出类似于以下屏幕截图的结果:

最终总计

使用非 Knockout 函数

现在,我们将添加一个外部函数来展示在视图中格式化值的另一种方式。你将再次修改我们刚刚工作的 computed.htm 示例。首先在脚本顶部添加一个具有以下代码的函数:

dollarFormat = function(value){
    return "$ "+value;
}

现在进入视图并按以下方式更改最终总计。测试以确保你做对了,然后如果你希望,更改所有这些值:

<span data-bind="text:dollarFormat(total())"></span>

这是页面最后一条项目格式化为美元金额的样子:

使用非 Knockout 函数

我们本可以在计算中添加一个外部函数来展示它在 ViewModel 中的使用。您仍然需要做一些工作,因为您将不得不选择最佳位置放置类似的东西。也许在几个版本之后,这些工具将神奇地为我们完成所有工作。好吧,即使它们真的做到了,也会有新的需求,我们仍然有机会通过代码为服务对象解决问题。

摘要

您现在已经尝到了 KnockoutJS 的好处。这仅仅是 KnockoutJS 力量的开始。Knockout 的好处在于它解决了 jQuery 等库无法解决的问题,同时如果需要,它还有能力与之并行工作。Knockout 提供了绑定和功能智能,它们可以自动化并简化 HTML 和编码,就像 jQuery 简化 JavaScript 编码一样。

在下一章中,我们将通过学习如何使用条件绑定、可观察数组、简化嵌套项以及一些关于如何处理集合的技巧来在此基础上构建知识。

第二章:使用数组、嵌套和网格

现在我们已经尝到了 KnockoutJS 的滋味,我们现在准备好学习新的编程技能。在本章中,我们将看到如何扩展我们的 MVVM 技能,以创造更多甜蜜的数据交互体验。本章将重点关注:

  • 条件绑定

  • 简单嵌套绑定

  • 可观察数组

  • 分页网格

  • 数据集合排序

条件绑定

数据绑定是许多与 HTML 标记交互的新库的核心。我们在第一章中看到了绑定,使用data-bind="..."绑定。在这里,我们将探讨条件绑定。

我们首先要做的是理解条件绑定的概念。我们的例子旨在说明这个概念。在本章中,我们将很快展示一个更实际的例子。

if绑定在 DOM 中的角色与 visible 属性非常相似。不同之处在于,if绑定实际上会在 DOM 中添加和移除内容,而 visible 则是交换 CSS 显示样式在可见和不可见之间。

通过复制位于ko_2/do文件夹中的_base.htm文件,并在do文件夹中将其命名为condition.htm来创建一个新文件。如果你遇到困难,done文件夹中有一个完成的副本。为了使事物看起来更好,我们现在将使用更大的 Bootstrap 模板基础,因为它将给我们更好的展示。你将在标记代码中看到以下区域,这是你将放置本书其余部分代码的地方:

<!-- 
markup code here 
-->

你将把其余的代码放在第一章中解释的相同位置。再次强调,这只是为了让我们的工作在阅读本书的其余部分时看起来更好。现在,在htm文件的标记部分输入以下代码:

                <label><input type="checkbox" data-bind="checked: showDetails" />&nbsp;Include Condition</label>
                &nbsp;
                <span data-bind="if:showDetails">
                    ( This section shows when the condition is selected. )
                </span>

这将给我们一个复选框来切换我们的内容。我们将在页面的script部分创建一个名为showDetails的变量来绑定内容切换。当复选框被勾选时,内容将被添加,当复选框未被勾选时,内容将被移除。以下是.htm文件script部分的代码:

myVM = {
    showDetails: ko.observable(false)
}
ko.applyBindings(myVM);

这里没有什么复杂的。我们只有一个变量,它将保存我们的真假状态。当变量为真时,内容将被添加,当变量为假时,它将从页面中移除。以下是当值为真时的样子:

条件绑定

在浏览器中运行代码,并通过将复选框设置为勾选和不勾选来切换内容几次,以确保你的代码正在工作。

Knockout 中数组简介

我们将从一个未绑定的数组开始工作。你会看到 Knockout 足够智能,仍然可以与数组一起工作并正确显示内容。实际上,我们将开始的数组将是一个包含嵌套数据的数组。将高亮的数据添加到我们页面的script部分:

myVM = {
showDetails: ko.observable(false),
employee: [
{name:"John Jones", spouse: { name: "Mary Jones"}},
{name:"Bill Williams", spouse: null},
{name:"Sandy Rivers", spouse: { name: "Mark Rivers"}},
]
}

当我们在页面上运行applyBindings函数时,员工数组数据将自动绑定到 MVVM 系统。我们需要一些标记来告诉我们它实际上已经工作。我建议使用<hr/>标签来分隔页面上的内容部分,以便更清晰。现在将以下代码添加到标记部分:

<ul data-bind="foreach: employee">
    <li>Employee: <strong data-bind="text:name"></strong>
        <div data-bind="if: spouse">( Spouse: <strong data- bind="text: spouse.name"></strong> )</div>
        <div data-bind="ifnot: spouse">( AVAILABLE )</div>
    </li>
</ul>

现在我们可以使用 Knockout 学习一些新的命令。第一个是foreach命令。如果我们有一个数组,我称之为集合,这个命令将会遍历每个项目。我们告诉foreach循环遍历员工数组集合。

你接下来会注意到,集合的元素是在项目级别被引用的。换句话说,我们不需要使用myVM.employee.name;我们只需简单地使用name。这使得代码更加简洁。

你还会看到我们再次使用了if命令。我们还加入了相反的逻辑命令ifnot。如果返回空结果,那么在功能上它被视为与 false 值相同。这意味着如果一个人没有配偶,他们将被标记为可用。提示:如果你在你的公司页面上运行一个声明谁可用的页面,这可能会引起社会波动,所以这绝对不是公司网页的最佳实践。

Knockout 中数组介绍

当我们运行代码时,我们得到的结果如前一个截图所示。我们看到比尔没有结婚,这与预期不符。由于我们知道发布某人可用的信息不是一个好主意,因此有一个更简单的方法来显示相同的信息,同时跳过有风险的分类。我们将复制代码部分,并使用with命令代替if命令,如下所示:

<ul data-bind="foreach: employee">
    <li>Employee: <strong data-bind="text:name"></strong>
        <div data-bind="with: spouse">( Spouse: <strong data- bind="text:name"></strong> )</div>
    </li>
</ul>

当我们运行with命令时,页面看起来是这样的:

Knockout 中数组介绍

现在我并不是说从技术角度来看withif更安全。你需要像思考这个例子中的社会问题一样,聪明地思考你的业务逻辑。花几分钟时间思考业务需求,因为那里的问题并不总是技术问题。我们需要确保技术代码处理我们在工作中应该知道的所有业务问题。

与数组集合一起工作

让我们将_base.htm文件复制到do文件夹中的arrays.htm,以继续我们在 Knockout 中学习数组集合。首先,将我们的核心标记添加到新页面,如下所示:

<ul data-bind="foreach: employee">
    <li>Employee: <strong data-bind="text:name"></strong>
        <div data-bind="if: spouse">( Spouse: <strong data- bind="text: spouse.name"></strong> )</div>
    </li>
</ul>

我们还需要在 script 部分创建一个数据模型才能使其工作。以下是为数据部分编写的代码。这里我们将开始使用可观察数组。Knockout 使用比简单变量类型更多的逻辑来处理数组。这些数组可以是简单变量的集合或复杂结构的嵌套行:

myVM = {
    showDetails: ko.observable(false),
    employee: ko.observableArray([
          {name:'John Jones', spouse: { name: "Mary Jones"}},
          {name:'Bill Williams', spouse: null},
          {name:'Sandy Rivers', spouse: { name: "Mark Rivers"}}
    ]),
    alt: ko.observableArray()
}
ko.applyBindings(myVM);

运行页面并确保你得到以下结果。当然,如果你修改了数据,你将因探索和享受工作而获得加分。在这种情况下,你的结果将略有不同。

处理数组集合

我们将添加四个不同的按钮来体验使用可观察数组在 Knockout 中与数组集合一起工作。在我们这样做之前,这里有我们可以对数组方法执行的操作:

函数 描述
push() 将项添加到集合的末尾
pop() 从集合中移除最后一个项
unshift() 在集合的开始处插入一个新项
shift() 从集合中移除第一个项并返回它
reverse() 交换集合中项的顺序
sort() 对集合的顺序进行排序(需要排序函数)
splice() 从集合中从指定的起始点开始移除给定数量的元素
remove() 移除所有等于某个值的值并将它们作为数组返回;这也可以作为函数运行以识别项
removeAll() 从数组列表中移除所有项或移除所有内容并返回移除的项作为数组
destroy() 这是 Ruby on Rails (RoR) 的 remove() 版本,以便让 RoR 开发者更熟悉
destroyAll() 这是 removeAll() 的 RoR 版本,以便让 RoR 开发者更熟悉

我们将首先向页面添加一个按钮来处理反转数组集合顺序。在这个页面上,我们将通过使用 Knockout 的 MVVM 绑定之外的功能来与数据模型进行交互。我们仍然将与绑定结果进行交互。这将有助于构建内部和外部编码技能,以便在不同场景中使用 Knockout。使用以下代码创建一个按钮:

<button onclick="myVM.employee.reverse()">Reverse Sort Staff</button>

你在这里可以看到,我们正在直接从按钮使用数组集合的 reverse 方法运行 JavaScript。添加此按钮并再次运行页面。点击按钮后,你会看到数组集合中的所有项现在都已反转。再次点击它,每次点击都会反转项。如果你将前面的截图与下面的截图进行比较,你可以看到数组集合中的项已反转:

处理数组集合

虽然这一切都很不错,但动态网站是需要添加和删除数据的网站。我们将在本章中立即开始学习这方面的知识。我们首先要做的是创建一个添加员工的按钮。将以下高亮代码添加到页面的标记部分:

<button onclick="myVM.employee.reverse()">Reverse Sort Staff</button>
&nbsp;
<button onclick="addStaff()">Add Staff</button>

处理数组集合

你可以看到,我们需要将添加员工到我们页面上的addStaff函数附加到按钮的onClick事件处理器。我们需要使用我们绑定的数组集合的push方法来添加这个函数。将以下函数添加到代码的script部分:

function addStaff(){
    myVM.employee.push({name:"Charlie Targus", spouse: {name:"Patti Targus"}});
}

现在,当我们运行代码时,它应该会按照预期添加新员工。push方法始终会将项目添加到集合的末尾,如下面的截图所示:

处理数组集合

让我们刷新页面,并按照以下步骤验证这个假设:

  1. 刷新页面。

  2. 点击反转排序员工按钮。

  3. 点击添加员工按钮。

  4. 再次点击反转排序员工按钮。

这里就是你会得到的输出图像:

处理数组集合

如果我们只想将项目放在列表顶部,那就太麻烦了。当我们开始处理更大和更大的数据集时,这一点将变得更加明显。为了在数组集合的开始处插入一个项目,我们将使用unshift方法代替push方法。

注意

现在除了我之外,还有谁认为unshift()这个术语很奇怪吗?也许这能帮助我们更容易地记住它!

移除最后一个项目

我们看到了push是如何将项目添加到数据末尾的;现在我们可以看看如何从数组集合中取出最后一个项目。如果你使用pop方法,它不会是添加到数据末尾的最后一个项目,而是集合中的最后一个项目。这将是页面上员工列表底部显示的最后一个项目;在我们的例子中,我们将使用以下代码行:

<br/>
<button onclick="myVM.employee.pop()">Remove Staff</button>

现在通过添加员工按钮添加一个员工,你会得到以下结果:

移除最后一个项目

我们看到一切就像之前一样工作。现在有四名员工。我们现在将使用按钮内的内联 JavaScript 代码移除最后一个项目。这与向数组集合末尾添加项目的push函数相反。它会移除数组集合中的最后一个项目。

点击移除员工按钮后,你应该看到的视图是:

移除最后一个项目

尝试先点击反转排序员工按钮,然后点击移除员工按钮。这次,你应该看到John Jones从列表底部消失,列表应该看起来像这样:

移除最后一个项目

排序时间

显示数据是网页的一个非常常见的用例。也许除了搜索之外,人们最常对数据进行的功能就是排序。我们将探讨如何根据特定的数据字段对数据进行排序。这次,我们将首先创建逻辑。将以下代码输入到 script 标签中:

function doSort() {
    myVM.employee.sort(function (left, right) {
        return left.name == right.name ? 0 : (left.name < right.name ? -1 : 1);
    });
}

我们将分解那些不熟悉这种 JavaScript 级别的逻辑的人的逻辑:

  1. sort 函数传入两个结构。每个结构匹配正在排序的项。变量名可以是任何名称;我们选择 leftright 是因为它们有助于程序员记住哪个变量是哪个。当然,你可以使用你选择的任何变量命名。每个变量包含传入项的整个结构。

  2. sort 的基本返回值需要是 true 或 false。这告诉程序是否应该交换这两个项。这就是为什么它们使用三元符号返回值的原因。这些符号可能不是你在高中数学中使用的符号,所以对我们大多数人来说可能是陌生的。以下是一个解释:

    • 首先,有一个逻辑比较,后面跟着 ? 符号;? 前面的值告诉我们结果应该基于真还是假。

    • 第一个值是如果结果为真应该返回的值。

    • 然后有一个冒号来表示如果逻辑评估为假应该返回的结果。

    注意

    你应该在这里看到,如果需要的话,逻辑可以堆叠以执行第二次逻辑评估。这可以在真或假的位置进行。

  3. 第二个值将是如果值为假时返回的值。同样,在这个例子中,我们观察到当结果为假时运行的嵌套逻辑。

    这里是相同逻辑的 if 语句,以便进行比较:

    function doSort() {
        myVM.employee.sort(function (left, right) {
            if (left.name == right.name) {
                return 0;
            } else {
                if (left.name < right.name) {
                    return -1;
                } else {
                    return 1;
                }
            }
        });
    }
    
  4. 你应该注意到的一件事是,使用三元运算符方法确实有更少的代码。这并不意味着使用 if 方法是错误的;只是需要更多的正确代码,而当然,我们输入的代码越多,我们可能花费在调试上的时间就越多。如果你在工作中没有标准并且更喜欢使用 if 逻辑,请随意这样做。我的主要目标是向你展示许多经验丰富的开发者所做的事情,这样你就可以理解代码。

  5. 现在,让我们在标记中添加调用代码的按钮:

    <br/>
    <button onclick="myVM.employee.pop()">Remove Staff</button>
    &nbsp;
    <button onclick="doSort()">Sort Staff</button>
    
    

    在点击排序按钮之前,你应该看到以下内容:

    排序时间

    注意,它根据姓氏按字母顺序排序,因为整个姓名存储为单个字段。这正是我们预期的,如果它看起来与以下图像相同,则表示它工作正确:

    排序时间

  6. 现在,让我们添加一个员工成员以查看它是否与添加的项正确排序:排序时间

  7. 点击 排序员工 按钮以查看我们添加的项是否正确出现在员工列表中:排序时间

简单网格插件

就像 jQuery 允许自定义插件一样,使用 Knockout 也可以使用插件。在这里,我们将使用一个简单的插件来向我们的页面添加网格功能。此插件还智能地添加了分页。我们还将做一些简单的 CSS 来设置网格,以便在表格单元格之间放松空间。

提示

我们使用 jQuery、KnockoutJS、Bootstrap 和其他DRY(不要重复自己)库进行编码的原因是它们打包了我们的工作。当这样做时,我们不必重新思考、重新编码或重复我们的工作。使用 jQuery 和 KnockoutJS 等解决方案,我们可以通过添加自己的库扩展来 DRY 我们的代码。简单网格插件就是这样一个例子。

注意,这本书将涵盖足够的细节,到结束时你应该能够修改此插件或构建自己的。然而,你必须足够理解 JavaScript、CSS、HTML 和其他主题,以便获得你寻求的工作以 DRY(不要重复自己)的方式。关于 KnockoutJS,你将从这本书中获得所需的知识。(simpleGrid代码是从 KnockoutJS 主站获得的。它不是核心部分,但同样是一段很好的学习代码。)

使用_base.htm副本,在ko_2do文件夹中创建一个名为paged.htm的新页面,用于此练习。我们将进行的第一次修改是在代码底部附近的include标签之后添加高亮显示的行,用于 Knockout.js:

<script src="img/knockout.js"></script>
<script src="img/simplegrid.js"></script>
<script>
</script>

这添加了我们为简单网格插件所需的逻辑。现在我们可以创建我们练习的标记。在标记部分,添加以下标记:

<div data-bind="simpleGrid: orderVM"></div>
<button data-bind="click: addOrder">Add Order</button>
<button data-bind="click: orderPageOne, enable: orderVM.currentPageIndex">First Page</button>

仔细观察div标签的data-bind属性。你应该注意到一个新的命令属性,称为simpleGrid。这不是 Knockout 的默认部分。它是由我们的插件添加的。随后的orderVM属性是我们 ViewModel 中的根结构。这段优雅的代码教你如何打包你的代码。它要简单得多,更容易重用你的工作,甚至利用他人的工作。

让我们通过这段代码。首先,我们将在script标签部分添加一些用于网格的数据。这段代码将是一个标准的数组集合。请看以下代码:

var initOrders = [
    { name: "John Jones", item: "Apples", qty: 12 },
    { name: "Bill Williams", item: "Pears", qty: 24 },
    { name: "Sandy Rivers", item: "Bananas", qty: 44 },
    { name: "Patti Targus", item: "Peaches", qty: 12 }
];

接下来,我们将添加我们的 ViewModel 以使用 Knockout 进行绑定。在页面的script部分添加以下代码:

var PagedOrderModel = function (orders) {
    this.orders = ko.observableArray(orders);
    this.orderVM = new ko.simpleGrid.viewModel({
        data: this.orders
    });
};
ko.applyBindings(new PagedOrderModel(initOrders));

运行代码

这是你将看到的 Knockout 代码的常见方式。让我们运行代码并做一些观察:

运行代码

我们看到我们的数据显示与预期一致,集合项字段的名称被列在列名中。它还自动在底部添加了一个分页项。这是运行simpleGrid插件代码时的默认配置。默认每页 5 条记录。请注意,所有列都挤在一起。让我们首先将以下代码行添加到我们页面上的 CSS 样式:

table { width: 400px;}

运行代码

经过这个简单的修改后,看起来要好得多。如果你使用这个网格并且实际上需要更多对样式的控制,你可以使用 CSS 中的级联逻辑,并将表格嵌套在第二个div标签内以创建一个 ID 或类来管理该类中包含的表格。

接下来,我们想要通过设置配置来修改列和默认显示的列数。将以下高亮代码添加到页面的script部分:

this.orderVM = new ko.simpleGrid.viewModel({
      data: this.orders,
 columns: [
 { headerText: "Customer", rowText: "name"},
 { headerText: "Item", rowText: "item"},
 { headerText: "Count", rowText: "qty"}
 ],
 pageSize: 3
});

在声明了列的详细信息并设置了页面大小之后,我们看到使用 DRY(Don't Repeat Yourself)代码的更大价值。这使我们能够从我们的脚本代码中管理 ViewModel。这是我们在这个阶段的代码结果:

运行代码

现在,你可以点击到第 2 页,看看这个网格是如何工作的。我们还看到,我们的列使用自定义列名后看起来要好得多。

是时候考虑使用 Knockout 进行编码的最佳实践方法了。打开你的浏览器开发者工具并转到控制台。从控制台,我们想要与 ViewModel 交互。尝试在命令提示符中键入以下内容:

> PagedOrderModel.orders()

你将得到一个类似于 Chrome 给出的未定义响应。模型在那里并且正在工作,但你突然无法访问模型。这让我第一次遇到这个问题时感到困惑。最大的困惑是我从在线示例中获取的代码,但我遗漏了一个关键点。在线示例没有与命令提示符中的代码交互,也没有与 ViewModel 外部的 JavaScript 代码交互。

问题在于当我们在一个 JavaScript 函数中声明一个new对象时,该对象将不会在代码调用之外可用。我们可以通过在方法调用之外创建一个变量来保存对象,然后传递在方法参数中创建的对象来解决此问题。让我们通过以下更改来更改applyBindings方法。首先添加声明变量的行,然后更新applyBindings方法,如下所示:

pagedOrderModel = new PagedOrderModel(initOrders);
ko.applyBindings(pagedOrderModel);

现在,我们可以回到浏览器开发者工具并再次运行页面。然后输入新的命令,注意第一个 P 现在是小写的。JavaScript 区分大小写,所以请确保你的大小写正确:

> pagedOrderModel.orders()

现在你应该能看到一个集合项的数组。我展开第一个,这样我们就能在这里看到 Chrome 的结果:

运行代码

如果你使用 JavaScript 并且它没有连接到你的 ViewModel,那么你可能想要确保你没有犯之前解释过的编码错误。这仅在你需要从 ViewModel 外部连接到 ViewModel 时才是一个问题。

现在是时候在我们的网格下方添加一些按钮,以便进行更多自定义控制了。我们将添加两个按钮:添加订单第一页。让我们首先添加添加订单按钮。这是我们应该熟悉的代码,因为我们已经在本章的早期使用过它。我们将再次使用它来展示,即使在完全不同的用户界面UI)展示中,当我们更新 ViewModel 中的数据时,视图会自动更新。将以下代码添加到页面的script部分:

this.addOrder = function(){
       this.orders.push({name:"Gerry Markus", item: "Plums", qty:20});
};

当然,我们还需要按钮的标记:

<button data-bind="click: addOrder">Add Order</button>

添加按钮后,刷新浏览器。添加订单并点击到数据的第二页,它应该看起来像这样:

运行代码

我们看到Gerry Markus的记录已经被添加。我们可以使用本章中列出的数组方法中的任何代码来做像反转或删除记录这样的事情。

现在,我们还需要添加一个按钮,这将允许我们从 ViewModel 控制网格页面。首先,添加以下标记:

<button data-bind="click: orderPageOne">First Page</button>

我们看到点击事件连接到了 ViewModel 方法以跳转到第一页。我们还看到了一个命令来启用页面索引。这将确保我们的索引得到更新。

在这里列出了可以调用simpleGrid的一些其他方法。你会注意到其中一些是变量,而其他的是方法调用:

  • pagedOrderModel.orderVM.columns

  • pagedOrderModel.orderVM.data()

  • pagedOrderModel.orderVM.currentPageIndex()

  • pagedOrderModel.orderVM.itemsOnCurrentPage()

  • pagedOrderModel.orderVM.maxPageIndex()

  • pagedOrderModel.orderVM.pageSize

记住,如果你使用这些绑定到 Knockout data-bind属性的项,你并不总是需要括号。这个主题在第一章中有所涉及,KnockoutJS 入门

最终的script部分应该看起来像这样:

var PagedOrderModel = function(orders) {
    this.orders = ko.observableArray(orders);
    this.addOrder = function(){
          this.orders.push({name:"Gerry Markus", item: "Plums", qty:20});
};
    this.orderPageOne = function(){
          this.orderVM.currentPageIndex(0);
};
    this.orderVM = new ko.simpleGrid.viewModel({
          data: this.orders,
          columns: [
                { headerText: "Customer", rowText: "name"},
                { headerText: "Item", rowText: "item"},
                { headerText: "Count", rowText: "qty"}
          ],
          pageSize: 3
    });
};

最终排序

恭喜你使用 Knockout 创建了一个相当功能性的代码片段!然而,如果没有排序能力,数据交互将是什么样子呢?我们将看到如何将我们之前所做的排序修改得比之前的示例更动态,只需进行一些简单的修改。你可以继续修改当前页面,或者你可以将页面复制为sorting.htm,这就是我们处理它的方式。请注意,这完全是一个选择问题,因为它只会让你的do文件夹与我们的done文件夹相匹配。

将以下两个按钮添加到你的页面:

<button data-bind="click: sort('name')">Sort By Name</button>
<button data-bind="click: sort('item')">Sort By Item</button>

在我们之前的排序示例中,我们在方法调用期间没有传递任何值。在这里,我们传递了数据中的一个字段名称,它告诉我们我们将使用什么进行排序。这遵循了 DRY 编码的主题,因为我们能够为这两个按钮或任何未来的匹配代码使用相同的命令。以下是我们需要添加到实际进行排序的script代码:

this.sort = function(by){
    this.orders.sort(function(left,right){
          return left[by] < right[by] ? -1 : 1;
    });
};

虽然看起来很简单,但这正是创建一个可重用代码片段所需的所有代码,使我们能够在需要时进行排序。以下是基于名称排序的截图:

最终排序

这里是基于项目排序的另一个截图:

最终排序

为什么不创建一个按钮来根据数量进行排序作为练习,看看你是否已经搞明白了。以下截图显示了如果你做对了,表格应该看起来像什么。不过,我们还会做一件事。我们需要确保我们的排序是逆序的,所以先点击按名称排序按钮。然后,点击按数量排序按钮应该给出以下结果:

最终排序

现在,我们应该修改表格,以便我们可以进行逆序排序。基本上,我们只需要在我们的代码中将比较排序逻辑传递的左右项的“大于”符号翻转。当然,我们还需要在我们的方法中添加另一个参数,因为我们想重用我们的代码。由于排序方向并不总是传递,我们还将包括一行来设置升序排序作为默认选择,如果没有声明。修改后的script代码应该如下所示,变化部分已突出显示:

this.sort = function (by, direction) {
 direction = typeof direction === 'undefined' ?
 'asc' : direction
 if (direction === 'asc') {
        this.orders.sort(function (left, right) {
            return left[by] < right[by] ? -1 : 1;
        });
 } else {
 this.orders.sort(function (left, right) {
 return left[by] > right[by] ? -1 : 1;
 });
 }
};

当然,我们还需要为逆序排序设置一组新的按钮:

<button data-bind="click: sort('name','desc')">Reverse By Name</button>
<button data-bind="click: sort('item','desc')">Reverse By Item</button>
<button data-bind="click: sort('qty','desc')">Reverse By Quantity</button>

这是带有逆序排序的计数列的视图:

最终排序

摘要

如果你曾经使用 JavaScript 或 jQuery 构建过如此多的 JavaScript 页面与数据交互,那么你就会知道为什么这个库值得“Knockout”这个称号。我们仅用两章就获得了使用这个库所获得的强大功能,这是令人信服的。通过本章,你应该已经获得了进行条件绑定、嵌套绑定、可观察数组集合、以数组风格管理添加和删除记录以及排序可观察数组的能力。你甚至快速浏览了 Knockout 插件。

在下一章中,我们将学习如何使用 Knockout 的强大功能来驱动 Web 表单。它们的工作方式不同;对于一些人来说,这是第一个反应。这是一个有趣的第二次反应,紧接着几乎瞬间出现的反应是它更好。事件绑定也是我们将在下一章深入探讨的主题,特别关注 Knockout 在处理网格表单时给予我们的超凡表现。

第三章. 给表单添加 Knockout 触摸感

互联网上最古老的使用之一是分享和收集信息。这种交换包括表单和非表单元素的使用。在本章中,我们将学习如何通过以下重点领域来简化编码和用户交互:

  • 事件绑定

  • 文本绑定

  • 文本输入绑定

  • Web 3.0

  • 单选框和复选框绑定

  • 选择绑定

  • 网格表单

事件绑定

让我先说,我没有测试 Knockout 可以绑定的每种类型的事件,但 HTML(或者更确切地说,DOM)中有大量的事件可以测试。似乎随着 HTML5 友好浏览器的增加,我们可用的浏览器数量也在增加。我建议你确保测试以验证是否有任何新事件实际上在目标平台上可用。

当我们绑定一个事件时,它需要一个处理程序。这些处理程序要么是函数,要么是对象上的方法。函数可以是 ViewModel 的一部分,也可以在 ViewModel 的作用域之外。提醒一下,当函数是 ViewModel 的一部分时,我们可以不使用( )来分配函数方法。如果我们传递参数,当然我们会使用它们,即使它们是 ViewModel 的一部分。

在视图中我们可能会寻找的常见事件包括clickkeypressmouseovermouseout。还有很多其他事件,但这个列表已经足够让你了解事件是什么,如果你对这个概念是新手的话。

绑定标记

我们将首先创建我们页面的 HTML。为这个示例创建一个名为enable.html的文件:

<div>
  <div data-bind="event: { mouseover: oneLeft }">
    Move One Left
  </div>
  <div data-bind="event: { mouseover: oneRight }">
    Move One Right
  </div>
  <div style="border:solid 1px black;">
    <div data-bind="css: oneClass">
      ( One )
    </div>
    <div data-bind="css: twoClass">
      ( Two )
    </div>
    <div style="clear:both;"></div>
  </div>
  <button data-bind="event: {mousedown: twoLeft }">Move Two Left</button>
  <button data-bind="event: {mouseup: twoRight }">Move Two Right</button>
</div>

我们看到在这段代码中data-bind属性有一个事件声明。我们将把前两个元素绑定到mouseover事件上。oneLeft是一个当鼠标悬停在元素上时绑定会调用的函数。我们再次看到( )是不需要的,因为我们将会在 ViewModel 的一部分创建这个函数。

这里中间的两个div标签具有元素的类,CSS 属性,由 Knockout 的data-bind属性设置。这可能是我们创建额外类的好时机,在我们进行代码方面的工作之前。以下是 CSS 代码:

<style type="text/css">
.putLeft { float:left; }
.putRight { float:right; }
</style>

这些是两个非常简单的浮动类。在 ViewModel 中的前两个声明将是观察者,它们持有分配给这些元素的类的值。注意,我们声明这些为类,所以不要以点开始类名。CSS 假设这是这种情况。

<script>
    var vm = {
        oneClass: ko.observable('putLeft'),
        twoClass: ko.observable('putLeft'),
        /* mode code coming */
    };
    </script>

如果我们回顾我们的 HTML 标记代码,我们看到事件不是 mouseover,而是 mousedown 和 mouseup。这些 ViewModel 事件处理程序,也称为函数,将调用方法来管理浏览器页面上的框中的(Two)项。让我们添加这两组处理程序的代码:

oneLeft: function() {
     this.oneClass('putLeft');
},
oneRight: function() {
    this.oneClass('putRight');
},
twoLeft: function() {
    this.twoClass('putLeft');
},
twoRight: function() {
    this.twoClass('putRight');
}

我们所做的只是更改 ViewModel 元素的文本,因为这会更改绑定的 CSS 以匹配。当然,这将使分配的元素在页面上浮动到左边或右边。以下是这个示例的完整代码:

< script >
var vm = {
    oneClass: ko.observable('putLeft'),
    twoClass: ko.observable('putLeft'),
    oneLeft: function () {
        this.oneClass('putLeft');
    },
    oneRight: function () {
        this.oneClass('putRight');
    },
    twoLeft: function () {
        this.twoClass('putLeft');
    },
    twoRight: function () {
        this.twoClass('putRight');
    }
};
ko.applyBindings(vm); < /script>

现在,在浏览器中运行代码,你应该会看到当鼠标悬停在盒子上面的项目上时,(One) 元素会左右移动。你将需要实际点击盒子下面的项目来使它们执行动作。

绑定标记

如果我们能拿到这本书来展示结果给您看那就太好了。也许在未来的电子书中这将变得可能,但就目前而言,我们将保持传统方式,实际上输入代码并测试它。

绑定可见性的复选框

在这个例子中,我们将创建一个名为 event.html 的文件,并将 _base.html 复制过来以节省时间。我们将查看参数,并以一种非常优雅的方式完成任务,这在 JavaScript 中是一个过于繁琐的任务。jQuery 让 JavaScript 编码变得更好,但 Knockout 带来了我们从一开始就需要的绑定简单性。

接下来,我们将为我们的示例创建标记。这次我们看到 data-bind 与复选框的值变化相关联。第二个输入框与同一个 ViewModel 项目 bringingSpouse 相关联。第二个输入框存储输入到 ViewModel 项目 spouseName 中的值。当首次加载页面时,它不会存储任何内容,因为输入框将被禁用。

<p>
  Will you be bringing your spouse?
  <input type="checkbox" name="bringspouse" data-bind="checked: bringingSpouse" />
</p>
<p>
  Your Spouse's Name:
  <input type="text" data-bind="value: spouseName, enable: bringingSpouse" />
</p>

绑定可见性的复选框

这是驱动自动化逻辑的代码。它只需要两个可观察对象;一个用于保存配偶的名字的值,另一个用于处理真或假,当用户有配偶时切换输入框。

<script>
var vm = {
  bringingSpouse: ko.observable(false),
  spouseName: ko.observable("")
};
ko.applyBindings(vm);
</script>

现在,我们可以测试我们的代码,看看它是如何工作的。这就像点击复选框一样简单,之后输入框应该变得可使用,并准备好让您输入配偶的名字,当然,如果适用的话:

绑定可见性的复选框

修饰键

我们将重新访问我们的 enable.html 文件,并学习如何处理一些修改过的事件。在我们的例子中,我们将关注当按下 Shift 键时检测事件。如果按下 Shift 键,我们将以一种方式处理事件,如果没有按下,我们将以另一种方式处理。首先,让我们使用以下 HTML 代码在屏幕上添加一个重置按钮:

<br/>
<button data-bind="event: {mouseover: reset }">RESET</button>

现在,我们将添加页面 script 部分中高亮的代码:

var vm = {
    oneClass: ko.observable('putLeft'),
    twoClass: ko.observable('putLeft'),
    oneLeft: function() {
        this.oneClass('putLeft');
    },
    oneRight: function() {
        this.oneClass('putRight');
    },
    twoLeft: function() {
        this.twoClass('putLeft');
    },
    twoRight: function() {
        this.twoClass('putRight');
    },
 reset: function(data, event){
 if(event.shiftKey){
 alert("Don't hold the shift key!");
 } else {
 this.oneClass('putLeft');
 this.twoClass('putLeft');
 }
 }
};

当我们按下 Shift 键并将鼠标悬停在新的按钮上时,它将弹出一个类似于这样的警告框:

修饰键

默认动作

默认情况下,Knockout 阻止内置的标准浏览器事件发生。如果你想让这些事件运行,只需绑定一个返回 true 的处理程序到该事件。

阻止冒泡

你可能还想防止事件冒泡。冒泡意味着网页上的一个元素,也称为 DOM 元素,与我们所说的更高级元素之间存在链式关系。在项目有机会处理事件之后,事件随后会沿着链向上传递,以便允许更高层级的元素有机会对事件做出响应。

在我们的鼠标悬停示例中,页面上可能有一个包含按钮的鼠标悬停处理器的区域。如果有,在按钮处理事件后,事件会被传递给更高层元素,以便它也能响应该事件。

控制实际上非常简单。我们只需要通过告诉我们的按钮处理程序返回 false 来阻止事件。当然,在不同的场景中,对于你不希望超出的事件操作,你会在其处理程序中设置返回 false 值,就像我们在这里讨论的使用案例中的按钮一样。

textInput 绑定

我们显然已经对文本框做了很多绑定。现在,我们将对它们做一些新的操作。我们将在页面上放置一个文本输入和一个文本区域。以下是我们的 HTML 标记,我们将在这里放入一个名为text.html的新文件:

<p>Title: <input data-bind="textInput: title" /></p>
<p>Post: <textarea data-bind="textInput: post" /></textarea></p>
<p>
<h2 data-bind="text: title"></h2>
<div data-bind="text: post"></div>
</p>

如果我们使用值绑定,我们只有在输入元素失去焦点时才会得到更新。使用textInput绑定,我们可以逐个字符得到反馈。让我们添加我们的代码,以便我们可以尝试它:

<script>
function Blog() {
  this.title = ko.observable();
  this.post = ko.observable();
};
blog = new Blog();
ko.applyBindings( blog );
</script>

The textInput binding

我们可以看到,当我们输入时,页面内容逐个字符更新,但存在一个问题。Post中的文本是以文本形式进入页面的。有一个简单的方法可以解决这个问题。我们只需将内容目标从text更改为html。哦,并且确保你使用小写字母,否则你会遇到问题。现在,再次运行它,它应该看起来像这样:

The textInput binding

动态焦点

在进行动态 AJAX 风格网站时,有许多机会可以增强用户体验。多年前,与桌面应用程序相比,网站是一个死气沉沉的体验。今天,更动态的应用主要取决于每个平台的设计师和开发者的创造力。将用户界面改为更响应式,在用户和你的页面之间建立了一种虚拟关系。利用以下步骤使你的页面动态化:

  1. 我们首先将更改文本输入的data-bind属性。通过添加逗号,我们可以在一个元素上添加多个数据绑定。我们将向输入添加hasFocus绑定器,并将事件目标指向名为lookAtTitleBox的处理程序。现在,我们的输入框在代码中应该看起来像这样:

    <input data-bind="textInput: title, hasFocus: lookAtTitleBox" />
    
  2. 接下来,我们将在这个输入框后面添加一个按钮,以展示从代码中动态控制焦点的功能。这意味着我们需要将事件处理器,我们的函数,绑定到按钮的点击事件上。然后,我们将让代码负责将控制权交还给标题输入框。在标题输入框后添加以下标记:

    <button data-bind="click: focusTitle">Focus On Title</button>
    
  3. 现在,我们需要添加输出文本,以便在标题输入获得焦点时在视图中显示:

    <span data-bind="visible:lookAtTitleBox">( Enter A Good Title )</span>
    
  4. 我们最后需要做的是修改我们的 ViewModel,使我们的表单更具交互性。我们将添加两个项目:ViewModel 上的一个可观察属性和一个由视图元素调用的方法:

    this.lookAtTitleBox = ko.observable(false);
    this.focusTitle = function(){
      this.lookAtTitleBox(true);
    };
    

当页面重新加载时,我们的标题字段默认具有焦点。点击帖子以从标题元素中移除焦点。你应该在按钮可见后和标题元素具有焦点时看到焦点文本。现在,当标题元素没有焦点时,点击聚焦于标题按钮。通过 Knockout,你会看到我们能够通过在 ViewModel 中切换变量来指定元素的焦点。这是 ViewModel 在 MVVM 导向的应用程序中强大功能的另一个例子。以下是页面重新加载时我们得到的输出:

动态焦点

我们为此逻辑的完整代码如下所示:

<!-- Here is the markup code-->
<br/>
<p>Title: <input data-bind="textInput: title, hasFocus: lookAtTitleBox" />
<button data-bind="click: focusTitle">Focus On Title</button>
<span data-bind="visible:lookAtTitleBox">
( Enter A Good Title )
</span>
</p>
<p>Post: <textarea data-bind="textInput: post" /></textarea></p>
<p>
<h2 data-bind="text: title"></h2>
<div data-bind="html: post"></div>
</p>
// Here is the script code
<script>
function Blog() {
  this.title = ko.observable();
  this.post = ko.observable();
  this.lookAtTitleBox = ko.observable(false);
  this.focusTitle = function(){
    this.lookAtTitleBox(true);
  };
};
blog = new Blog();
ko.applyBindings( blog );
</script>

抽空看看这段代码的优雅简洁。这就是为什么许多 Knockout 程序员感觉 Knockout 对 JavaScript 所做的,就像 jQuery 对 JavaScript 所做的,Knockout 在增强设计和开发体验方面也做了同样多的工作。

嗯,这还不够好。如果目标是提升用户体验,为什么我们不复制这个文件,text.html,作为text3.html并创建一个 Web 3.0 级别的体验呢?我们将一次性展示所有代码,并对其进行讲解。我们将在以下地方创建点击编辑体验:

<p>
  <span data-bind="click:editTitle">Title</span>:
  <input data-bind="visible: showTitleEditor, textInput: title, hasFocus:showTitleEditor" />
  <span data-bind="visible: !showTitleEditor(), html:title, click: editTitle"></span>
</p>
<p>
  <span data-bind="click:editPost">Post</span>:
  <textarea data-bind="visible: showPostEditor, textInput: post, hasFocus:showPostEditor" /></textarea>
  <span data-bind="visible: !showPostEditor(), html:post, click: editPost"></span>
</p>

我们所做的是将标题的输入元素与一个显示标题值内容的 span 元素放在一起。你会注意到它们两者都有相同的项开始data-bind属性。就代码稳定性而言,顺序并不重要;这仅仅是我们的编码顺序。文本中的可见项有括号,因为!(非)符号意味着我们已经在属性值中输入了 JavaScript 代码。每次我们这样做时,都需要输入括号以使其正确运行。

我们还向标签和标题内容添加了点击事件处理器。我们添加标签的原因是,有时你可能遇到页面加载而没有预先填充标题的情况。这只是一个示例,以表明你仍然可以通过点击标题来访问编辑框。页面加载时,它最初看起来是这样的(记得在之前完成 ViewModel,以便它能正确工作):

动态焦点

我们希望帖子框也有相同的功能,这样你就可以看到我们的代码在标记中的相同之处,只是它是一个textArea字段,用于帖子而不是标题。现在,让我们看看我们的 ViewModel 代码:

<script>
function Blog() {
  this.title = ko.observable('Web 3.0');
  this.showTitleEditor = ko.observable(false);
  this.editTitle = function(){
    this.showTitleEditor(true);
  };

  this.post = ko.observable('Here is my <strong>Web 3.0</strong> content!');
  this.showPostEditor = ko.observable(false);
  this.editPost = function(){
    this.showPostEditor(true);
  };
};
blog = new Blog();
ko.applyBindings( blog );
</script>

我们看到标题和标题编辑器的可见性值只是 ViewModel 上的简单可观察属性。我们只需要让editTitle事件处理程序切换标题输入框的可见状态为 true。Knockout 会为我们正确设置视图和编辑元素的可见性,几乎不需要写代码。我们为帖子元素也做了同样的事情。现在,当我们点击标题内容或标题标签时,我们会看到标题的编辑框如下显示:

动态焦点

无线电和复选框绑定

在表单中处理单选按钮和复选框可能会很麻烦。这是 Knockout 使事情变得简单化的另一个领域。我们的例子将首先创建一个名为radio.html的文件。让我们从复选框的标记开始:

<h2>Checkbox</h2>
<p>
  Colors (<span data-bind="text: colors"></span>)<br/>
  <input type="checkbox" value="red" data-bind="checked: colors" /> Red<br/>
  <input type="checkbox" value="green" data-bind="checked: colors" /> Green<br/>
  <input type="checkbox" value="blue" data-bind="checked: colors" /> Blue<br/>
  <input type="checkbox" value="yellow" data-bind="checked: colors" /> Yellow<br/>
  <input type="checkbox" value="purple" data-bind="checked: colors" /> Purple<br/>
</p>

无线电和复选框绑定

现在,在页面的底部添加一个script标签,就像我们在其他页面上做的那样。我们需要一个数组来保存选中项的内容,这就是我们在 ViewModel 中实现此功能所需的所有内容:

function VM() {
  this.colors = ko.observableArray([]);
};
vm = new VM();
ko.applyBindings( vm );

通过在data-bind属性中绑定选中处理程序,我们会看到colors属性会自动填充。span标签中的colors属性将显示所有当前选中的复选框元素。你甚至可以尝试点击前面的项目来打开和关闭,你会发现它总是将最后选中的项目放在列表的末尾:

无线电和复选框绑定

单选按钮的标记非常相似。我们将以相同的方式创建我们的代码,以展示单选按钮集合和复选框集合元素在功能上的差异,如下所示:

<h2>Radio</h2>
<p>
  Shapes (<span data-bind="text: shapes"></span>)<br/>
  <input type="radio" value="square" data-bind="checked: shapes" />Square<br/>
  <input type="radio" value="round" data-bind="checked: shapes" />Round<br/>
  <input type="radio" value="triangle" data-bind="checked: shapes" />Triangle<br/>
  <input type="radio" value="rectangle" data-bind="checked: shapes" />Rectangle<br/>
  <input type="radio" value="oval" data-bind="checked: shapes" />Oval<br/>
</p>

无线电和复选框绑定

我们只需要在function VM()中添加这一行代码:

this.shapes = ko.observableArray([]);

无线电和复选框绑定

对于单选按钮,点击多少个项目并不重要,因为单选按钮总是限制自己只选择一个项目。在常见的 HTML 中,我们需要给每个单选按钮都加上一个名称,以确保 DOM 知道如何实现这种功能。在这个例子中,我们可以清楚地看到 Knockout 正在为我们处理这些事情。

增强的事件集成

我们之前解释过的单选按钮和复选框绑定非常有用,许多经验丰富的开发者可能认为 HTML 编码过于繁琐,需要手动在标记或文档的视图部分输入所有颜色或形状的详细信息。这种类型的事情最好是放在某种数据集合中。这正是我们将要做的,我们将使用另一组复选框,我们将它们标记为食物

在这个例子中,我们将向我们的 ViewModel 添加一个名为foodItems的属性。我们还将更进一步,在名为foods的变量中设置一些预选值。您会注意到我们的foodItems属性有一个结构化的集合,包含两个嵌套项:itemitemDisplay。确保您的预选项使用item值。如果您输入Milk而不是milk,您可能会困惑为什么它看起来没有正确工作。项目必须有一个 100%的匹配,所以大小写在这里都至关重要。在function VM()中添加以下代码:

  this.foodItems = ko.observableArray([
    { item: 'bread', itemDisplay: 'Bread' },
    { item: 'milk', itemDisplay: 'Milk' },
    { item: 'eggs', itemDisplay: 'Eggs' }
  ]);
  this.foods = ko.observableArray(['bread', 'eggs'"]);

现在,我们可以在代码的视图部分添加另一组用于食物的复选框。我们也将在这个集合中使用 foreach 绑定。在这里,我们可以传递一个类似 JSON 的结构,以便我们可以为内部的$data项创建“食物”的别名。foreach内部的每个项目都被指定为$data。添加as键允许我们为$data设置别名“食物”。我们在这里展示了两种使用方法,通过使用$data.item和通过使用food.itemDisplay(在更动态的示例中)。以下是添加到标记中的代码:

<h2>Checkbox</h2>
<p>
  Foods (<span data-bind="text: foods"></span>)<br/>
<div data-bind="foreach: {data:foodItems, as: 'food'}">
  <input type="checkbox" data-bind="checkedValue: $data.item, checked: $root.foods" />
  <span data-bind="text: food.itemDisplay"></span><br/>
</div>
</p>

增强事件集成

这是我们完整的代码,以防它有助于您在一个地方看到所有内容。Knockout 再次展示了它工作方式上的简单和强大优势。

function VM() {
  this.colors = ko.observableArray([]);
  this.shapes = ko.observableArray([]);
  this.foodItems = ko.observableArray([
    { item: 'bread', itemDisplay: 'Bread' },
    { item: 'milk', itemDisplay: 'Milk' },
    { item: 'eggs', itemDisplay: 'Eggs' }
  ]);
  this.foods = ko.observableArray(["bread","eggs"]);
};
vm = new VM();
ko.applyBindings( vm );

选择绑定

我们使用 Knockout 与select元素的第一例将是用于单项选择。这是我们将在其中放入colors的标记:

<p>
  Colors: ( <span data-bind="text: colors"></span> )
  <br/>
  <select data-bind="options: colorOptions, 
              value: colors,
              optionsCaption: 'Choose a color'"></select>
</p>

在我们的代码中,我们现在将做一件特别的事情。在我们创建 ViewModel 之后,我们将修改其一个属性,并使用 JavaScript 中常见的push函数向colorOptions数组添加另一个颜色。这意味着 JavaScript 的一些部分已经非常出色,我们应该继续使用它们。以下是script代码:

<script>
function MyModel(){
  this.colorOptions = ko.observableArray(['Red','Green','Blue','Yellow','Green']);
  this.colors = ko.observableArray();
};
myModel = new MyModel();
ko.applyBindings( myModel );
myModel.colorOptions.push('Orange');
</script>

选择绑定

如果我们选择最后添加的项目,橙子,我们将看到选择器和显示 span 都显示与前面截图中的值相同。使用这段代码,您可以设置 ViewModel 的color属性值,它将自动将选择框设置为匹配的值。您应该在浏览器开发者工具控制台中尝试它。别忘了匹配值的大小写。

现在,我们将使用多选元素。在这种情况下,我们不需要选项标题。我们需要做的是更改data-bind处理程序的值到selectedOptions处理程序。这允许我们捕获多个项目。只需记住,一个值是单数,而所选选项是复数。我并不是说我们总是必须考虑单数和复数,但在这个情况下我们必须这样做。将以下代码添加到标记中:

<p>
  Shapes: ( <span data-bind="text: shapes"></span> )
  <br/>
  <select size="3" multiple="true"
    data-bind="options: shapeOptions, 
      selectedOptions: shapes"></select>
</p>

现在,是时候添加一些代码来处理新的选择元素了。在 ViewModel 中,单选和复选元素的数据编码在实用上没有差异。

this.shapeOptions = ko.observableArray(['Square','Circle','Triangle','Rectange', 'Oval']);
this.shapes = ko.observableArray();

如您在以下屏幕截图中所见,这通过允许我们选择多个元素而工作得非常好:

绑定选择

使用对象集合选择元素

有时候我们的数据会以值和表示值的显示项目作为两个不同的项目。以下是一个示例,说明在这种情况下如何进行编码:

<p>
  Budget:
  <br/>
  <select data-bind="options: budgets,
        optionsText: 'budgetName',
        value: budget,
        optionsCaption: 'Pick...'"></select>
</p>
<div>
  You have chosen a 
  '<span data-bind="text: budget() ? budget().budgetType : 'undeclared'"></span>'
  budget type.
</div>

script标签内,在MyModel()中,添加以下代码行:

this.budget = ko.observable();

注意optionsText不是一个变量,而是集合项目中的结构元素的值。在这里我们使用单个结果,因此我们使用值处理器来保存结果。如果是多个选择,我们将使用selectedOptions绑定。以下是添加我们的代码后初始显示的样子。注意类型被设置为undeclared

使用对象集合选择元素

也要注意我们文本绑定中的逻辑。如果没有选择任何项目,预算项目将是一个空项目。在 JavaScript 中,这会返回一个假的结果。然后它会显示冒号后面的项目内容。否则,它将返回冒号前面的项目结果。在这种情况下,我们将从预算项目返回结构元素,用于类型,您将在我们的脚本中看到;我们将其编码为budgetType。现在让我们看看script代码:

var Budget = function(name, type){
  this.budgetName = name;
  this.budgetType = type;
};

上述代码段将在我们声明 ViewModel 的结构之前出现。我们将使用它来声明预算集合中的项目,如下所示。我们使用简单的可观察对象而不是可观察数组,因为我们只返回单个项目。这是一个具有嵌套属性的结构化项目,但在那个级别上它是一个单一的项目,因此这是正确的逻辑:

this.budgets = ko.observableArray([
  new Budget('Electric','expense'),
  new Budget('Bob Pay','income'),
  new Budget('Betty Pay','income'),
  new Budget('Taxes','expense'),
  new Budget('Gas','expense'),
  new Budget('Rental House','income'),
  new Budget('House Payment','expense')
]);
this.budget = ko.observable();

现在,让我们看看选择一个项目后的结果。如果我们选择出租房屋项目,我们会看到预算类型变为收入。这使得与我们的自动化系统进行动态交互变得非常方便,而且手动编码的工作量大大减少。

列出管理 Knockout 风格

现在,我们可以将我们的预算系统进一步扩展。我们将创建一个列表编辑器,允许我们添加、删除和排序我们的列表。虽然我们迄今为止所做的大部分工作都非常简单,但这个例子将会不同。这个例子将会简化。它做了更多的事情,所以需要更多的时间。然而,通过 Knockout 的力量,我们将看到简洁性仍然保持不变:

<p>
  <form data-bind="submit:addBudget">
  Budget Editor:
  <hr/>
  Budget:
  <input data-bind="value: newName, valueUpdate: 'afterkeydown'" /><br/>
  Type:
  <select data-bind="options: budgetTypes,
              value: budgetType"></select><br/>
  <button type="submit" data-bind="enable: newName().length">Add Budget</button>
  </form>
</p>

在我们的第一段代码中,我们使用了一些之前没有使用过的东西。我们使用了一个form标签。通常在使用 Knockout 进行编码时不会使用表单。那么为什么在这个例子中会有所改变?改变的原因是我们将使用表单的submit函数作为我们的触发器,以将新的预算项目添加到我们的预算中。这是通过我们的代码中的addBudget处理器完成的。以下是我们的script代码:

this.newName = ko.observable("");
this.budgetTypes = ko.observableArray(['income','expense']);
this.budgetType = ko.observable();
this.selectedBudgets = ko.observableArray([]);
this.addBudget = function(){
  var myBudget = new Budget(this.newName(),this.budgetType());
  if((this.newName() != "") 
      && (this.budgets.indexOf(myBudget) < 0) ) {
    this.budgets.push(myBudget);
  }
  this.newName("");
};
this.dropBudget = function(){
  this.budgets.removeAll(this.selectedBudgets());
};
this.sortBudgets = function(){
  this.budgets.sort(bCompare);
};

大部分代码看起来都很熟悉。可能对一些人来说,唯一可能新的部分是预算的排序。我们使用标准的 JavaScript 样式排序和我们在 ViewModel 外部创建的自定义函数来完成这个操作。自定义函数是 bCompare 函数。以下是该函数的代码:

var bCompare = function(left,right){
  if(left.budgetName < right.budgetName) {
    return -1;
}
  if(left.budgetName > right.budgetName) {
    return 1;
}
  return 0;
};

这是一个简单的比较函数,JavaScript 用它来与自然流程交互,以确保所有项目都是有序的。再次,我们看到这个编辑表单背后的代码足够简单。让我们看看我们正在构建的列表编辑器。我们没有让它变得花哨,但我们确实在它里面构建了一些很棒的功能:

以 Knockout 风格列出管理

在代码的视图部分有一些特殊的标记,以确保正确绑定,如下所示:

<p>	
  <select size="5" multiple="true"
      data-bind="options: budgets,
              optionsText: 'budgetName',
              selectedOptions: selectedBudgets"></select>
</p>
<p>
  <button data-bind="click: dropBudget">Drop Budget</button>
  <button data-bind="click: sortBudgets">Sort Budgets</button>
</p>

现在,如果你正在查看代码,你可能想知道代码有什么特别之处。这只是 Knockout 使事情变得简单和强大。这就是完成这项工作所需的一切。哦,你发现我对 Knockout 印象深刻了吗?我是。Knockout 是使其特殊的地方,因为它为我们做了很多事情,这样我们就可以专注于编程目标,而不是如何实现它们。以下是我们在添加娱乐作为支出、排序列表和从列表中删除汽油后的截图:

以 Knockout 风格列出管理

唯一名称绑定

IE6 不允许在没有 name 属性的情况下检查单选按钮。大多数时候这无关紧要,因为你的单选按钮元素将具有 name 属性,以便将它们放入互斥组中。然而,如果你没有添加 name 属性,因为在你情况下它是多余的,Knockout 将内部使用 uniqueName 在这些元素上以确保它们可以被检查。希望我们中没有人需要使用 IE6,但以防万一,将 uniqueName 处理程序添加到 data-bind 属性中,如果需要的话,可以像这样:

<input data-bind="value: newName, uniqueName: true" />

网格表单

现在我们将把我们的预算表单再向前推进一步。我们将把到目前为止我们已经学习到的多个功能组合成一个可编辑的表格。这次我们也将表格包裹在表单中,但你在代码中会看到如何使用表单的双向选项。为这个示例创建一个名为 grid.html 的文件。在标记中输入以下内容:

<form action='/serverTargetHandlerHere'>
  <p>You have asked for <span data-bind='text: budget().length'>&nbsp;</span> budget item(s)</p>
  <table data-bind='visible: budget().length > 0'>
    <thead>
      <tr>
        <th>Budget</th>
        <th>Amount</th>
        <th/>
      </tr>
    </thead>
    <tbody data-bind='foreach: budget'>
      <tr>
        <td><input data-bind='value: name, uniqueName: true' /></td>
        <td><input data-bind='value: amount, uniqueName: true' /></td>
        <td><a href='#' data-bind='click: $root.removeBudget'>Delete</a></td>
      </tr>
    </tbody>
  </table>
  <button data-bind='click: addBudget'>Add Budget</button>
  <button data-bind='enable: budget().length > 0,click: save' type''='submit'>Submit</button>
</form>

如果你只想将 Knockout 用作自动化的编辑器,并且仍然提交表单,你可以在表单的 action 属性中填写详细信息,并仍然以传统的方式提交它。如果你正在与较旧的系统合作,并且你的业务逻辑仍然是基于表单提交的,这可能很有用。这将还允许你逐步进入 Knockout 的世界。这也可能允许你在测试过程中测试功能,而无需完全重写你的系统。

注意,我们正在使用 foreach 标记绑定来管理网格中的每一行,对于数据模型中的每一行数据。ViewModel 智能地保持所有这些内容紧密相连并保持最新。我们还添加了 uniqueName 逻辑,这是需要在支持 IE6 的应用程序中添加的。现在让我们看看脚本代码:

var BudgetModel = function(budget) {
    var self = this;
    self.budget = ko.observableArray(budget);
    self.addBudget = function() {
        self.budget.push({
            name: "",
            amount: ""
        });
    };
    self.removeBudget = function(budget) {
        self.budget.remove(budget);
    };
    self.save = function(form) {
        alert("Could now transmit to server: " + ko.utils.stringifyJson(self.budget));
        // To actually transmit to server as a regular form post, write this: ko.utils.postJson($("form")[0], self.budget);
    };
};
var budget = new BudgetModel([
    { name: "Food", amount: "560.00"},
    { name: "Utilities", amount: "180.00"},
    { name: "Rent", amount: "620.00"},
    { name: "Insurance", amount: "80.00"}
]);
ko.applyBindings( budget );

在这个例子中,我们创建了一个名为 BudgetModel 的标准 ViewModel。再次强调,我们正在使用 JavaScript 数组的 push 方法在我们的数组集合中堆叠另一个预算项。我们还有移除预算项的处理程序。如果我们回顾我们的标记,您会看到它使用基于作用域的变量 $root.removeBudget。行是通过 foreach 功能添加的,因此 Knockout 再次为我们承担了繁重的工作,并且它会知道您正在尝试删除哪一行。不要因为试图弄清楚它而感到压力。把它想象成一部智能手机。您不需要知道它是如何工作的。您需要知道的是如何使用它。

保存方法处理程序被设置为在警告框中显示结果。您本可以使用 jQuery AJAX 将这些结果发送回服务器以进行持久数据存储。您还会看到,正如之前所述,您可以选择将其作为表单发送。选择权在您手中,您可以选择最适合您需求的方法。以下是之前代码的截图:

网格表单

现在尝试这个例子。添加一些行。删除一些行。带有数据的点击 提交 按钮,您将在警告框中看到结果。如果没有行而点击它,您会看到它足够智能,可以忽略试图提交空内容的用户。

摘要

当处理表单和网格,并保持数据更新时,无论何时何地触及它,这正是 Knockout 从一开始就关注的内容。我希望您正在享受使用这项技术提供的简单但强大的进步来编写网页表单。它使得关注业务逻辑变得更加容易,代码更少。

在下一章中,我们将学习如何使用 JSON 和映射在 Knockout 中集成数据管理。

第四章:编码 – AJAX、属性绑定、映射和实用工具

Knockout 有一个方面可以消除重复的任务。还有这样一个现实,我们从未期望它完成 100%的工作。它也很有道理,我们不希望 Knockout 做这么多,因为它在核心优势方面并不擅长。在这里,我们将学习如何扩展 Knockout 的覆盖范围,以及如何连接到 Knockout 之外的世界并做更多的事情。本章将重点介绍:

  • 与 JSON 一起工作

  • 映射与手动 ViewModel

  • 与 AJAX 请求一起工作

  • 解除数据映射

  • 管理映射

  • 实用函数

在本章中,我们将学习关于 Knockout 的一些常用方面——与 JSON 一起工作以及映射插件。这两个都是简单而强大的枢纽,我们将通过 Knockout 扩展快速应用开发。

JSON done Knockout style

JSON 是著名的数据打包标准,似乎已经占据了整个互联网。曾经有 XML;RoR 有自己的打包标准;甚至 Adobe 也有可能性能最好的方式来打包和传输客户端与服务器之间的数据,称为动作消息格式AMF)。总体赢家似乎还是 JSON,因为它简单,并且基于所有开发平台中最常见的:JavaScript。如果你想了解更多关于 JSON 的信息,你可以访问json.org

为了达到我们的目的,让我们创建一个名为json.html的文件。我们将要做的第一件事是将 ViewModel 转换为 JSON。大多数现代浏览器都有一个名为JSON.stringify的函数,但在 Knockout 中我们并不是这样做的。Knockout 中包含两种方法:

  • ko.toJS

  • ko.toJSON

第一种方法,ko.toJS,将克隆 Knockout 数据到一个不包含任何 Knockout 相关提示或信息的普通副本。第二种方法,ko.toJSON,将执行ko.toJS操作,然后根据 JSON 标准将其转换为序列化的 JSON 字符串。如果你使用的是较旧的浏览器,如 IE7 或更早版本,你需要获取json2.js文件的副本,该文件可在github.com/douglascrockford/JSON-js/blob/master/json2.js找到。

现在将以下代码输入到你的文件中:

<script>
function VM() {
  this.colors = ko.observableArray([]);
  this.shapes = ko.observableArray([]);
  this.foodItems = ko.observableArray([
    { item: 'bread', itemDisplay: 'Bread' },
    { item: 'milk', itemDisplay: 'Milk' },
    { item: 'eggs', itemDisplay: 'Eggs' }
  ]);
  this.foods = ko.observableArray(["bread","eggs"]);
};
vm = new VM();
ko.applyBindings( vm );
</script>

现在我们将查看存储在 ViewModel 中的结构。我们将使用 Chrome 作为我们的浏览器,但你可以使用任何带有支持控制台命令的开发工具的浏览器。以下是我们的 ViewModel 转储得到的结果。在控制台中输入console.log(vm)以获取存储在vm中的结果:

JSON done Knockout style

显然,虽然我们的 ViewModel 完全暴露在函数中,但你将习惯于在将内容倒入控制台时忽略undefined,。如果你得到预期的结果,那么不要被该项分散注意力。

这里我们使用了两个命令,即ko.toJSko.toJSON。让我们首先使用结构将 ViewModel 输出到控制台。在控制台中输入console.log( ko.toJS(vm) )

以 Knockout 风格完成的 JSON

这里,我们看到console.logdump。这很好,但如果我们想要将其发送到外部源或使用现代浏览器存储技术存储它,我们通常仍然希望将其作为 JSON 字符串打包。这可以通过使用辅助函数ko.toJSON方法来实现,这正是我们接下来要做的。您会看到这次我们的数据被放置在特殊字符的中间。这是 JSON 格式化,如下面的截图所示:

以 Knockout 风格完成的 JSON

现在,我们将创建json2.html来将我们的 JSON 数据拉入我们的应用。我们需要将 JSON 复制并粘贴到我们的应用中,但我们将从与 HTML 文件相同的目录中的另一个文件中执行此操作。在这个例子中,将该文件命名为json2.txt。当然,在实时网站上,不要将敏感数据存储在文本文件中。在json2.txt文件中,复制您在前一个示例中获得的控制台输出。这是您的 JSON 数据,它应该看起来像这样:

{"colors":[],"shapes":[],"foodItems":[{"item":"bread","itemDisplay":"Bread"},{"item":"milk","itemDisplay":"Milk"},{"item":"eggs","itemDisplay":"Eggs"}],"foods":["bread","eggs"]} 

虽然这可能看起来不错,但我们有一个问题。JSON 不能独立存在。它需要在一个变量中才能被管理。我们将按以下方式更改它。我们将使用变量myJSON。我们还需要在文本周围加上引号。由于 JSON 通常在内部使用双引号,处理这种情况的传统方法是在数据字符串之前使用单引号,并在末尾使用单引号,如下面的代码所示:

myJSON = '{"colors":[],"shapes":[],"foodItems":[{"item":"bread", "itemDisplay":"Bread"},{"item":"milk","itemDisplay":"Milk"}, {"item":"eggs","itemDisplay":"Eggs"}],"foods":["bread","eggs"]}';

以下代码用于我们的json2.html文件。我们将使用script命令将外部数据拉入页面。人们通常也会将这样的静态文件命名为json2.json。我们使用.text扩展名是为了强调这里的 JSON 只是一个文本文件:

<script src="img/json2.txt"></script>
<script>
function VM() {
  this.colors = ko.observableArray([]);
  this.shapes = ko.observableArray([]);
  this.foodItems = ko.observableArray([]);
  this.foods = ko.observableArray([]);
};
vm = new VM();
ko.applyBindings( vm );
myData = JSON.parse(myJSON);
vm.foodItems(myData.foodItems);
vm.foods(myData.foods);
</script>

这次,我们以结构开始我们的 ViewModel,但没有任何数据。在将 ViewModel 绑定到我们的数据模型后,我们将我们的 JSON 数据字符串转换为标准的 JavaScript 结构。然后,我们使用标准的 JavaScript 将数组传递到 ViewModel 的foodItemsfoods属性。虽然这样做很整洁,但感觉我们实际上并没有做任何有价值的事情。让我们在script部分上方添加一些 View 代码,如下所示。这是我们在上一章中使用过的相同代码:

Foods (<span data-bind="text: foods"></span>)<br/>
<div data-bind="foreach: {data:foodItems, as: 'food'}">
  <input type="checkbox" data-bind="checkedValue:$data.item, checked: $root.foods" />
  <span data-bind="text: food.itemDisplay"></span><br/>
</div>

我们看到的是一个网页,它拉取外部数据,并自动根据该数据填充视图:

以 Knockout 风格完成的 JSON

映射 - 首次查看

到目前为止,我们一直在本书中手动创建 ViewModel 的映射。对于较小的数据集,这可能是实用且高效的。随着数据集的增大以及更新数据的需求,这将变成一项繁琐的工作,有一个替代方案,许多 Knockout 开发者都喜欢。这是通过映射插件完成的。Knockout 是一个基于 jQuery 的库,在多个方面都是如此。对于 Knockout,也有可能构建和使用称为插件的库。最著名的库是映射库。我已经在本书的下载文件中包含了一个副本。我们将再次查看我们示例中的先前代码,但这次我们将使用映射插件来移动它。

我们这次将为我们的代码创建一个名为mapping.html的文件。我们将在 Knockout 调用之后立即包含一个额外的 JavaScript 文件,用于我们的映射插件。你可以在 ZIP 下载的工作示例文件夹中找到这些文件。在我们的情况下,它应该看起来像这样:

<script src="img/json2.txt"></script>
<script src="img/data.js"></script>

如果你想创建这两个文件,我们建议你只从本章的done文件夹中复制它们,并确保它们与mapping.html文件在同一个文件夹中。

通过 AJAX 远程连接

使用script标签来加载数据显然不是非常复杂。我们将通过再次创建示例来改进这一点,但这次我们将使用 jQuery 来执行我们的 AJAX 请求。我们将把这个代码放在一个名为ajax.html的文件中。我们将使用之前相同的 HTML 代码,但我们将添加一个按钮到表单中,使用以下代码行:

Foods (<span data-bind="text: foods"></span>)
<button data-bind="click: pullData">Pull Data</button><br/>
<div data-bind="foreach: {data:foodItems, as: 'food'}">
  <input type="checkbox" data-bind="checkedValue:$data.item, checked: $root.foods" />
  <span data-bind="text: food.itemDisplay"></span><br/>
</div>

pullData请求将是我们添加到 ViewModel 中的方法/函数。目前,它将用于从服务器获取数据并更新我们浏览器中的视图。以下是本例中我们将使用的script代码段:

<script>
function VM() {
  var self = this;
  self.foodItems = ko.observableArray([]);
  self.foods = ko.observableArray([]);
  self.pullData = function(){
    var reqAJAX = "data.json";
    jQuery.getJSON(reqAJAX).done(function(data){
      self.foodItems(data.foodItems);
      self.foods(data.foods);
    });
  };
};
vm = new VM();
ko.applyBindings( vm );
</script>

请注意,我将尝试让你养成使用——这是一个我编造的短语,并且我喜欢——“自私的编码”的习惯。因为我们的代码与具有this值的交互存在风险,调试this问题并不有趣。我们已经学会了使用self别名来确保这不会成为一个问题。.done()函数是 jQuery 中的一个链式命令,用于处理对服务器的良好请求的完成。请参阅 jQuery 文档以获取更多处理程序;你可以使用这些文档使你的代码更加完全响应。

我们创建了一个名为pullData的函数/方法。在数据内部,我们将使用jQuery.getJSON请求从服务器拉回我们的数据。我们这次将 JSON 结构复制到一个名为data.json的文件中。确保不要将其分配给变量;你只想得到如下的 JSON 结构:

{"colors":[],"shapes":[],"foodItems":[{"item":"bread","itemDisplay":"Bread"},{"item":"milk","itemDisplay":"Milk"},{"item":"eggs","itemDisplay":"Eggs"}],"foods":["bread","eggs"]}

现在,让我们看看我们加载页面时的初始屏幕。你应该得到以下内容:

通过 AJAX 远程连接

当我们点击拉取数据按钮时,我们会看到结果自动更新到以下视图:

通过 AJAX 远程连接

这对于许多开发者来说已经比纯 JavaScript 体验好多了。然而,如果这个表单在页面上有 40 个元素呢?这将是一个很大的代码块来设置 ViewModel 以进行那么多绑定。我们刚刚学习了映射;那么,如果我们包含映射插件并重新编写我们的代码会怎样呢?看看以下代码片段:

<script>
var reqAJAX = "data.json";
vm = {};
jQuery.getJSON(reqAJAX).done(function(data){
  vm = ko.mapping.fromJS(data);
  vm.pullData = function(){
    reqAJAX = "data2.json";
    jQuery.getJSON(reqAJAX).done(function(data){
      ko.mapping.fromJS(data,vm);
    });
  };
  ko.applyBindings( vm );
});
</script>

我们可以看到我们的方法中的一些变化。我们本可以直接将文件名放入getJSON请求中,但我们只是按照我们喜欢的方式将其作为变量传递。我们还创建了vm变量来保存我们的 ViewModel。

AJAX 已经被移出 ViewModel,而 ViewModel 的声明已经被移入 AJAX。与上一个例子相比,它是反过来的。区别在于我们看到数据在页面加载时立即填充到视图中。我们还改变了pullData函数的功能。现在,它将被用来对服务器进行第二次调用。通常,我们不会重置这个源文件,因为它将是一个典型的 AJAX 请求,以查看是否有任何更新。由于我们不是为动态服务器编写代码,所以我们在这里通过更改 AJAX 请求的源名称来展示这个场景的模仿。

现在,当我们进行映射时,我们必须在使用applyBindings方法之前声明映射功能。这给我们带来了与手动创建每个单独绑定相同的结果。再次强调,对于这样一个简单的表单,这种收益并不明显。当我们到达更大、更复杂的页面时,收益是惊人的。哦,还要注意,当我们对服务器进行额外的调用时,我们将更新数据。我们需要在映射数据后传递 ViewModel 变量。

现在,我们将再次查看通过运行带有更多渐进式 AJAX 的代码我们能得到什么。第一次加载看起来就像是从我们上一个例子中拉取的数据:

通过 AJAX 远程连接

我们创建了一个额外的项目;因此,当我们加载更新时,它将变得明显。这个项目是冰淇淋。我们还预先选择了这个项目。以下是第二次加载的 JSON 数据结构:

{"colors":[],"shapes":[],"foodItems":[{"item":"bread","itemDisplay":"Bread"},{"item":"milk","itemDisplay":"Milk"},{"item":"eggs","itemDisplay":"Eggs"},{"item":"icecream","itemDisplay":"Ice Cream"}],"foods":["bread","eggs"]}

现在我们按下按钮来拉取更新,我们应该看到以下内容:

通过 AJAX 远程连接

解除数据映射

这进展得很好,但我们需要为大多数基于 AJAX 的 Web 应用程序做一些事情。我们需要将数据存储在服务器上。仅仅将数据拉到浏览器是不够的。我们还需要能够将数据推回服务器。我们再次将使用 jQuery 来完成这个功能。当然,我们将展示如何做到这一点的代码,但我们将以不同的方式来处理,因为不同的读者将使用不同的后端,如 ASP.NET、ColdFusion、Node.js、PHP、Python、Ruby 和其他。

这次,除非你想要创建一个新文件,否则只需修改AJAX.html文件中的代码。我们将向我们的视图添加另一个按钮,这次连接一个推送数据方法:

<button data-bind="click:pushData">Push Data</button>

我们还需要在我们的视图代码的末尾添加一个文本框,以便查看从我们的 ViewModel 中提取的数据。创建一个textarea字段来保存结果:

<textarea id="unmapped"></textarea>

我们现在需要向我们的 ViewModel 添加另一个方法:

<script>
var reqAJAX = "data.json";
vm = {};
jQuery.getJSON(reqAJAX).done(function(data){
  vm = ko.mapping.fromJS(data);
  vm.pullData = function(){
    reqAJAX = "data2.json";
    jQuery.getJSON(reqAJAX).done(function(data){
      ko.mapping.fromJS(data,vm);
    });
  }; 
  vm.pushData = function(){
    // This next line is just to show unmapped data
    var myData = ko.toJSON(vm);
    jQuery('#unmapped').text(myData);
    /*
    jQuery.post(reqAJAX, myData ).done(function(data){
      // code here to reflect good request
      alert("Your food changes have been stored.");
    });
    */
  };
  ko.applyBindings( vm );
});
</script>

我们看到了用于向服务器发送请求的代码。在这个例子中,我们假设我们的服务器能够响应该reqAJAX变量中的 URL 并处理进入服务器的数据。如果不能,你只需要将该变量设置为可以接收数据的目标。在这种情况下,数据是以POST形式发送的。这和表单使用POST方法是一样的。

你应该看到我们只是添加了将结果拉回作为名为myData的变量的能力。如果我们再次运行代码,提取数据,并将选择设置为仅鸡蛋和冰淇淋,我们就能测试被推回服务器的数据。注意我们有了将myData结果推送到textarea字段的 jQuery 命令。以下是我们看到的结果:

取消映射你的数据

我们似乎遇到了一个与额外数据相关的问题。我们不必管理它,但修复起来并不难。这个问题发生是因为当数据被映射时,它会在内部创建额外的细节。这些细节在取消映射数据时会被带出来。如果你选择的话,可以保留这些细节,或者你可以使用以下代码来修复它。我们将编写一个console.log命令,将结构输出到控制台。我们还需要将myData更改为返回 JavaScript 结构。

如果我们查看浏览器开发者工具的控制台,我们将看到以下结构用于我们的输出。然后我们将使用JSON.stringify()使其准备好 AJAX:

var myData = ko.toJS(vm);
console.log(myData);
delete myData.__ko_mapping__;
myData = JSON.stringify(myData);
jQuery('#unmapped').text(myData);

我们看到myData变量和用于管理输出的控制台日志的变化。控制台日志中的额外细节是__ko_mapping__数据结构的一部分。使用 JavaScript 的删除命令,我们可以直接从我们的结果中移除它。一些函数显示出来了,但当我们使用stringify函数时,它不会拉取它们。以下是我们现在得到的内容:

{"colors":[],"shapes":[],"foodItems":[{"item":"bread","itemDisplay":"Bread"},{"item":"milk","itemDisplay":"Milk"},{"item":"eggs","itemDisplay":"Eggs"},{"item":"icecream","itemDisplay":"Ice Cream"}],"foods":["eggs","icecream"]}

现在我们正在进行有意义的 AJAX 交互。你可能希望向服务器发送比这里更少的结构。有选项可以进一步裁剪数据,以使事物尽可能紧凑。

哦,对于实时应用,记住要移除控制台日志和textarea字段。别说我教你把那些留在真实世界的网页上!

合并映射数据

有时候你可能想要从多个来源将数据拉入你的 ViewModel。当你这样做时,你可以为每个来源创建一个映射。如果源变量具有相同的名称,它们将覆盖现有的变量。只要基本变量具有不同的名称,它就会将它们合并到 ViewModel 中。如下这样做:

myViewModel = ko.mapping.fromJS(firstData, firstMap);
ko.mapping.fromJS(nextData, nextMap, myViewModel);

你得到的是 firstData JavaScript 结构与 firstMap 映射的组合,再加上 nextData JavaScript 结构和 nextMap 映射。如果 nextData 中有任何重复的基本结构,它们将覆盖现有 firstData JavaScript 结构中的相同结构。

映射选项

有时候,当你将数据加载到不需要更改的页面应用程序中时,这只是一个静态数据,将其转换为可观察数据会额外消耗处理器时间和内存资源,而没有任何收益。在将数据传递到映射处理程序时,你可以设置哪些项目被映射为可观察项目,使用以下代码行:

var data = {
  a: "a",
  b: [{ b1: "v1" }, { b2: "v2" }],
  c: true
};
var result = ko.mapping.fromJS(data, { observe: "a" });
var result2 = ko.mapping.fromJS(data, { observe: "a", copy: "b" }); //will be faster to map.

resultresult2 变量得到的结果将相同。为什么?这是因为当我们声明 observe 项目时,其他项目被认为是复制项目。如果传递单个项目,我们可以在数组外声明它,就像我们用 a 做的那样。如果传递多个项目,我们将在数组中声明它们,例如 ["a","c"]。这将使 ac 都成为可观察项目。

如果我们只想声明一个要复制的项目,我们可以传递复制的内容,这将直接复制的唯一项目。当然,我们还有能力在映射过程中忽略要复制的项目,使用 ignore

工具函数

ko.utils 中有许多函数。让我们首先看看标准 Knockout 中的特殊数组方法。

ko.utils.arrayFilter()

ko.utils.arrayFilter 函数允许我们在数组中过滤项目。我们将直接作为代码示例运行这些。我们将创建一个示例 JSON 文件并通过 AJAX 加载它,以保持对学习方法的关注,而不是浪费时间创建示例代码集。我们将创建一个名为 utility.html 的页面来运行这些代码,并从那里运行过滤代码。我们的示例标记如下:

<h3>arrayFilter() : staff under 35</h3>
<ul data-bind="foreach: youngStaff">
  <li><span data-bind="text: age() + ' ' + firstName()"></span></li>
</ul>

我们的 script 代码如下。随着我们继续添加每个示例,我们将添加更多内容,但这里是对工具示例的基本介绍:

<script>
var vm = {};
jQuery.getJSON('utility.json').done(function(data){
  vm = ko.mapping.fromJS( data );
  vm.youngStaff = ko.computed(function(){
    return ko.utils.arrayFilter(vm.arr(), function(item){
      if(item.age() < 35) { return true; }
      return false;
    });
  });
  ko.applyBindings(vm);
});
</script>

我们使用数据设置映射,然后开始添加我们的自定义功能。这些可以是自定义函数或计算值。最后,在将所有自定义修改应用到映射后,我们应用绑定。

在我们的代码示例中,我们向您展示了如何使用 arrayFilterarrayFilter 函数将逐个传递项目,并将包含在结果集中,其中我们发送一个 true 值来告知它应该被包含。

这里是我们将用于示例的 JSON。我们将在这里展示整个数据集。请注意,如果不想手动输入,JSON 也在我们的 done 文件夹下的 utility.json 文件中。通常,我会建议手动输入这些示例,以加强你在该主题上的技能:

{"arr":[{"firstName":"James","lastName":"Donald","age":23,"phone":[]},{"firstName":"Adam","lastName":"Thomas","age":46,"phone":[]},{"firstName":"Michelle","lastName":"Ingram","age":32,"phone":[]},{"firstName":"Edward","lastName":"Adams","age":63,"phone":[]},{"firstName":"Veronica","lastName":"Wesson","age":54,"phone":[]},{"firstName":"Greg","lastName":"Simons","age":46,"phone":[]}],"color_1":["red","green","blue","orange"],"color_2":["red","blue","orange","purple"]}

此外,我们使用了以下数据集来创建我们的 JSON。我们只是对数据集调用了 JSON.stringify 方法。以下是代码:

{
  arr : [
    { "firstName": "James", "lastName": "Donald", "age": 23, "phone": [] },
    { "firstName": "Adam", "lastName": "Thomas", "age": 46, "phone": [] },
    { "firstName": "Michelle", "lastName": "Ingram", "age": 32, "phone": [] },
    { "firstName": "Edward", "lastName": "Adams", "age": 63, "phone": [] },
    { "firstName": "Veronica", "lastName": "Wesson", "age": 54, "phone": [] },
    { "firstName": "Greg", "lastName": "Simons", "age": 46, "phone": [] }
  ],
  color_1 : [ "red","green","blue","orange" ],
  color_2 : [ "red","blue","orange","purple" ]
}

我们仍然需要包含我们的 jQuery、Knockout 和 Knockout 映射 JavaScript 文件。当你将所有这些代码组合在一起时,你应在浏览器中看到以下内容:

ko.utils.arrayFilter()

我们看到我们的过滤结果只返回了六个项目中的两个。这是因为其中只有两个小于 35。更有趣的是,这些数据是动态连接到 View 模型的。如果数组通过添加或删除项目而发生变化,屏幕将自动更新。这并不是连接到 jQuery,所以这是一个很好的补充。此外,如果数组项中的值发生变化,比如年龄之一,那么这里的过滤器将自动知道要添加、删除或保留项目在视图列表中。

ko.utils.arrayFirst()

ko.utils.arrayFirst 方法会将项目传递给要搜索的函数,直到找到匹配项或声明。它只会从数组中返回一个项目。以下是添加到这个示例中的 View 代码:

<h3>arrayFirst() : first found over 45</h3>
( <span data-bind="text: firstRetire().age()"></span> )

这里是添加到理解我们的 arrayFirst 工具命令的逻辑。在脚本中绑定设置之前添加它:

vm.firstRetire = ko.computed(function(){
  return ko.utils.arrayFirst(vm.arr(), function(item){
    if(item.age() > 45) return true;
    return false;
  });
}); // arrayFirst

与上一个实用方法一样,结果是由返回的 true 值触发的。这个例子中的不同之处在于,第一个 true 值将是唯一返回的值。以下是我们的结果截图:

ko.utils.arrayFirst()

ko.utils.arrayMap()

ko.utils.arrayMap 方法允许创建一个扁平化的数组。这意味着有时我们有一个结构数组,只想获取整个结构中的特定项,将其拉回到一个简单的数组中。以下是标记代码:

<h3>arrayMap() : till retirment</h3>
<ul data-bind="foreach: tillRetire">
  <li><span data-bind="text: $data"></span></li>
</ul>

这里是需要添加到这个示例中的脚本代码。这次我们不会返回 true 或 false 类型的结果。我们将返回一个值,在这种情况下,将创建一个简单的值数组:

vm.tillRetire = ko.computed(function(){
  return ko.utils.arrayMap(vm.arr(),function(item){
    return 65 - item.age() + ' years till retirement.';
  })
}); // arrayMap()

这是您将看到的屏幕结果,使用我们的数据集:

ko.utils.arrayMap()

通过修改项目,实际上可以修改传递给过程的结构。这是因为通常,结构和数组是通过在内存中设置对源结构的引用来传递的。这意味着尽管传入的是名称项,但项目指向 ViewModel 中的原始结构。以下是如果我们想修改 ViewModel 中的原始结构可以使用的另一个方法:

vm.tillRetire2 = ko.computed(function(){
  ko.utils.arrayMap(vm.arr(),function(item){
    item.yearsLeft = 65 - item.age() + ' years till retirement.';
  })
}); // arrayMap()

结果将是一个嵌套在 ViewModel 数组集合中的值,但该值不会被观察或计算。这意味着虽然它可能工作,但它不会与 Knockout 的所有功能一起工作。因此,在尝试之前应该仔细测试。如果有疑问,为了避免意外功能,请避免这种方法。哦,你也可以删除项目字段,所以请注意,你正在编辑原始结构,如果你手动更改这些内容,任何与之相关的内容都处于风险之中。

ko.utils.arrayGetDistinctValues ()

ko.utils.arrayGetDistinctValues 方法允许您从一个数组中移除重复值,只留下唯一的项目。这次我们将处理一个结果集,它是一个数组,因此我们再次在视图的 data-bind 属性上使用 foreach 方法:

<h3>arrayMap() : All ages sorted</h3>
<ul data-bind="foreach: allYears">
  <li><span data-bind="text: $data"></span></li>
</ul>
<h3>arrayGetDistinctValues() : Unique ages</h3>
<ul data-bind="foreach: uniqueYears">
  <li><span data-bind="text: $data"></span></li>
</ul>

您可能已经注意到这次我们插入了两个段。这是因为 arrayGetDistinctValues 修改了另一个数组。我们需要它将要修改的数组。我们将要做的是使用我们的 arrayMap 方法创建一个包含所有年龄的数组,然后创建另一个只包含其中唯一值的数组。在我们的脚本代码中,我们再次需要在 applyBindings 命令之前设置 arrayGetDistinctValues

vm.allYears = ko.computed(function(){
  return allYears = ko.utils.arrayMap(vm.arr(),function(item){
    return item.age();
  });
}); // arrayMap()
vm.uniqueYears = ko.computed(function(){
  return ko.utils.arrayGetDistinctValues(vm.allYears().sort(),vm);
}); // arrayGetDistinctValues()

这里是两组结果截图:

ko.utils.arrayGetDistinctValues ()

第一组在调用唯一值集时进行了排序。这是需要注意的一点,因为当发生这种情况时可能会令人困惑。我们将其包括在这里,以便提供一些思考的食物,并再次说明需要意识到您可能正在对原始数据集执行命令。如果您遵循最佳实践,您可能会对唯一的年份结果进行排序,如果这是您的目标,而不是在方法中对输入数据进行排序。这也是一个外部变量即使在方法参数中发生排序调用时也能可见的例子。

ko.utils.arrayForEach()

ko.utils.arrayForEach 方法将允许您遍历一个数组。这对于执行总计或其他从总结中提取的逻辑非常有用。例如,您可以返回所有有孩子的工人。虽然最理想的地方可能是从数据库中这样做,但这并不意味着它只能在那里完成。审查我们在哪里以及为什么做事情是良好的实践,以确保我们的应用程序可以按需进行性能和扩展。

我们这次的视图代码将使用非数组值,因此我们不会使用 foreach 方法。我们再次提到这一点的原因是,如果您使用错误的方法,您的页面将无法正确运行。通常,问题可能只是尝试在 ViewModel 的非数组属性上调用数组方法。这是任何开发者都可能面临的常规问题。以下是视图代码:

<h3>arrayForEach() : total ages</h3>
Total Age: <span data-bind="text: totalAge"></span>

就怕我在这本书的某个地方没有说过,让我明确一点。在 Knockout 中,一点点的代码就有很大的力量。以下是我们的总年龄处理代码:

  vm.totalAge = ko.computed(function(){
    var years = 0;
    ko.utils.arrayForEach(vm.arr(), function(item){
      years += item.age();
    });
    return years;
  }); // arrayForEach();

当然,我们在截图中也得到了总年龄如下:

ko.utils.arrayForEach()

ko.utils.compareArrays()

这允许您比较数组并返回一个集合,显示来自两个数组的所有项目。如果项目在第二个集合中不存在,则将其显示为已删除。如果项目在第二个集合中但不在第一个集合中,则将其显示为已添加。返回的集合还将显示项目存在的索引。

这里是compareArrays上最后一个数组示例的视图代码:

<h3>compareArrays() : 2 Color Arrays</h3>
<ul data-bind="foreach: diff">
  <li><span data-bind="text: $data.value + ' was ' + $data.status"></span></li>
</ul>

我们的脚本代码再次很简单,希望通过手动处理每个这些代码,它们对你来说也变得更加自然。以下是script代码:

vm.diff = ko.computed(function(){
  return ko.utils.compareArrays(vm.color_1(),vm.color_2());
}); // compareArrays()

这里是代码运行的截图:

ko.utils.compareArrays()

我们想从浏览器开发者工具中包含一个控制台日志,因为我们认为 Chrome 显示了很好的结构:

ko.utils.compareArrays()

纯化我们的计算

现在我们将使用pureComputed而不是computed来重做代码的script部分。术语pureComputed是从纯函数作为编程风格的概念中获得的灵感。这并不是你需要理解才能使用的东西,所以不要被名称的语义所困扰,因为就学习这些纯计算如何对我们在这里有所帮助而言,并没有实际的收益。

当 Knockout 有东西在监视时,一个计算项被称为订阅者。因此,它被视为一个订阅者依赖项。如果我们使用pureComputed方法而不是computed方法,当没有订阅者时,Knockout 不会计算值。这当然通过减少不必要的计算和运行不必要的代码来增加我们的处理速度。这也是避免任何内存问题的另一种方式。

当没有订阅者时,纯计算可观察者被认为是睡眠状态。当有订阅者时,它被认为是监听状态。对我来说,术语“监听”有点奇怪,因为对我来说它应该是响应。尽管如此,这确实是有意义的,因为它是一种计算方法。所以它确实需要监听它用于计算的值。如果这些值中的任何一个发生变化,它需要重新计算其结果。

这里是更新后的脚本代码,移动到更好的pureComputed方法:

<script>
var vm = {};
jQuery.getJSON('utility.json').done(function(data){
  vm = ko.mapping.fromJS( data );
  vm.youngStaff = ko.pureComputed(function(){
    return ko.utils.arrayFilter(vm.arr(), function(item){
      if(item.age() < 35) { return true; }
      return false;
    });
  });
  vm.firstRetire = ko.pureComputed(function(){
    return ko.utils.arrayFirst(vm.arr(), function(item){
      if(item.age() > 45) return true;
      return false;
    });
  }); // arrayFirst
  vm.tillRetire = ko.pureComputed(function(){
    return ko.utils.arrayMap(vm.arr(),function(item){
      return 65 - item.age() + ' years till retirement.';
    })
  }); // arrayMap()
  vm.tillRetire2 = ko.pureComputed(function(){
    ko.utils.arrayMap(vm.arr(),function(item){
      item.yearsLeft = 65 –I tem.age() + ' years till retirement.';
    })
  }); // arrayMap()
  vm.allYears = ko.pureComputed(function(){
    return allYears = ko.utils.arrayMap(vm.arr(),function(item){
      return item.age();
    });
  }); // arrayMap()
  vm.uniqueYears = ko.pureComputed(function(){
    return ko.utils.arrayGetDistinctValues(vm.allYears().sort(),vm);
  }); // arrayGetDistinctValues()
  vm.totalAge = ko.pureComputed(function(){
    var years = 0;
    ko.utils.arrayForEach(vm.arr(), function(item){
      years += item.age();
    });
    return years;
  }); // arrayForEach();
  vm.diff = ko.pureComputed(function(){
    return ko.utils.compareArrays(vm.color_1(),vm.color_2());
  }); // compareArrays()
  ko.applyBindings(vm);
});
</script>

如果你想知道一个项有多少个依赖项,你可以查看getDependenciesCount方法。在你的浏览器开发者工具的控制台中输入以下内容:

vm.allYears.getDependenciesCount()

这将显示订阅allYears计算方法的项的数量。还有一个函数会告诉我们订阅allYears计算方法的项的数量,如下所示:

vm.allYears.getSubscriptionsCount()

为计算可观察者编写代码文档

我们在这里仅为了参考包含了以下关于可观察者的文档。虽然这些文档在 KnockoutJS 网站上可用,但将它们包含在这里似乎是个好主意,这样你就不必在网站和书籍之间跳来跳去了。

计算可观察者可以使用以下形式之一构建。

表格 1

ko.computed( evaluator [, targetObject, options] )形式支持创建计算可观察者的最常见情况。它有以下属性:

  • evaluator:这是一个用于评估计算可观察者当前值的函数。

  • targetObject: 如果提供,它定义了 Knockout 调用你的回调函数时this的值。

  • options: 这是一个对象,包含计算出的可观察对象的进一步属性。完整的列表见下文。

表格 2

ko.computed( options ) 参数是一个用于创建计算出的可观察对象的单参数形式,它接受一个具有以下任何属性的 JavaScript 对象:

  • read: 这是一个必需的函数,用于评估计算出的可观察对象的当前值。

  • write: 这是一个可选函数。如果提供,这个函数使得计算出的可观察对象可写。这个函数接收其他代码尝试写入你的计算出的可观察对象的值。你需要提供自定义逻辑来处理传入的值,通常是通过将值写入某些底层可观察对象。

  • owner: 这是一个可选函数,如果提供,它定义了 Knockout 调用你的readwrite回调时this的值。

  • pure: 这是一个可选参数。如果这个参数为真,计算出的可观察对象将被设置为纯计算出的可观察对象。这个选项是ko.pureComputed构造函数的替代。

  • deferEvaluation: 这是一个可选参数。如果这个选项为真,那么计算出的可观察对象的值将不会在创建时立即评估,直到实际尝试访问其值或手动订阅它。

  • disposeWhen: 这是一个可选函数。如果提供,这个函数在每次重新评估之前执行,以确定是否应该销毁计算出的可观察对象。如果结果为真,将触发计算出的可观察对象的销毁。

  • disposeWhenNodeIsRemoved: 这是一个可选函数。如果提供,当指定的 DOM 节点被 Knockout 移除时,将触发计算出的可观察对象的销毁。这个特性用于在模板和控制流绑定移除节点时销毁用于绑定的计算出的可观察对象。

表格 3

ko.pureComputed( evaluator [, targetObject] ) 形式使用给定的评估函数和可选对象来构建纯计算出的可观察对象。与ko.computed不同,这个方法不接受options参数。

表格 4

ko.pureComputed( options ) 形式使用一个options对象来构建纯计算出的可观察对象。它接受之前描述的readwriteowner选项。

使用计算出的可观察对象

计算出的可观察对象提供了以下函数:

  • dispose(): 这个函数手动销毁计算出的可观察对象,清除所有对依赖项的订阅。如果你想要停止计算出的可观察对象更新或想要清理依赖于不会清理的可观察对象的计算出的可观察对象的内存,这个函数很有用。

  • extend(extenders): 这会将给定的扩展器应用到计算出的可观察对象上。

  • getDependenciesCount(): 这个函数返回计算可观察对象的当前依赖项数量。

  • getSubscriptionsCount(): 这个函数返回计算可观察对象的当前订阅数量(无论是来自其他计算可观察对象还是手动订阅)。

  • isActive(): 这个函数返回计算可观察对象是否可能在将来更新。如果没有依赖项,计算可观察对象是无活动的。

  • peek(): 这个函数返回计算可观察对象的当前值,而不创建依赖项。

  • subscribe( callback [,callbackTarget, event] ): 这个函数注册了手动订阅,以便在计算可观察对象发生变化时接收通知。

使用计算上下文

在计算可观察对象的评估函数执行期间,你可以访问ko.computedContext来获取有关当前计算属性的详细信息。它提供了以下函数:

  • isInitial(): 这是一个函数,如果在当前计算可观察对象的第一次评估期间调用,则返回 true,否则返回 false。对于计算可观察对象,isInitial()始终是未定义的。

  • getDependenciesCount(): 这个函数返回当前评估期间检测到的计算可观察对象的依赖项数量。

    注意

    ko.computedContext.getDependenciesCount()函数等同于在计算可观察对象本身上调用getDependenciesCount()。它之所以也存在于ko.computedContext上,是为了提供一个在计算可观察对象尚未完成构建之前,第一次评估期间计算依赖项的方法。

摘要

本章为我们打开了大量功能和选项,使网络风格编码变得强大而优雅。对于一些人来说,学会让库为你处理事情是困难的,但这个观点是讽刺的,因为我们编程计算机是为了让其他人让我们的工作为他们管理事情。本章为我们知识和经验增添了几个有价值的技能和选项:

  • 我们学习了如何使用 JSON 作为传递数据进出的替代方式,将数据管理集成到 Knockout 中。

  • 我们已经看到了 Knockout 映射的强大功能,它使我们的代码变得更加优雅,提高了另一个数量级的优雅性。

  • 我们已经介绍了使用 Knockout 进行 AJAX 的基本知识。

  • 我们已经学习了如何使用实用函数来获取更高级的集合功能,并且代码行数更少。

  • 最后,我们学习了如何使用纯面向计算函数(附带 KnockoutJS 在线文档中的计算文档剪辑)来提高我们的 ViewModel 的性能。

在下一章中,我们将探讨另一种打包方式,以使我们的代码更短、更易于接近、更可持续。模板曾经是后端服务器的领域。下一章将向您展示为什么您需要在后端做更少的事情,而在前端做更多的事情,比以往任何时候都要多!

第五章:模板的乐趣

快速回顾历史,讲述了服务器端代码如何将模板变成魔法。数据与这些模板混合,并为客户端返回有意义的 HTML。这些模板动态适应,产生灵活且功能强大的自定义 HTML 页面。随着 AJAX 的到来,开发者失去了这种强大的编码方法。但是等等,模板又回来了!现在它们在客户端运行。本章将教你如何使用 Knockout 在客户端创建魔法。在本章中,我们将关注:

  • 原生模板

  • 改进的集合处理

  • 渲染事件

  • 第三方模板

  • 令人惊叹的模板选项

Knockout 不仅仅是关于双向绑定。如果做得正确,它关乎更优雅和可持续的代码。当我们学习如何利用 KnockoutJS 的模板功能时,我们将学习如何通过另一个层次使页面创建更加简单。

原生模板

模板是我们合并数据和数据适合放入的东西的模式。在计算机的早期,最流行的模板形式是我们称之为邮件合并的东西。像 Microsoft Word 这样的程序会使用邮件合并文档和数据文件,并将它们合并在一起。原始目的是用于打印。

随着时间的推移,我们转向使用这种相同类型的技术来制作网页。当用户访问一个网站并请求一个网页时,服务器开始变得更聪明。数据被混合到模板中,合并后的结果以 HTML 的形式返回给浏览器。这对于购物网站和其他基本页面相同的网站来说是一个惊人的变革。这就是为什么我们把这些代码片段称为模板。

一种看待这个问题的方法是可以想到人们装饰房子的时候。我们有东西叫做模板。虽然模板定义了结果形状的样式,但它并不控制你用什么颜色来画东西。如果你有一个苹果模板,你可以画一个红色的苹果,一个绿色的 Granny Smith 风格的苹果,一个黄色的 Golden Delicious 苹果,或者偏离标准,画一个紫色和黄色的条纹苹果。模板只控制内容将被放置在内的容器。

以同样的方式,网页模板定义了内容。嗯,大多数情况下是这样的。你看,这是一个程序,它比墙画模板要聪明一些;因此,我们可以添加条件逻辑,做一些像重复信息或条件性地选择是否显示任何信息的事情。这就是模板的力量。

在本章中,我们将拥有一个稍微复杂一些的数据集,因为我们想让自己接触更多现实生活中的网站页面,这些页面我们将在这本书之外使用。以下是本页面的代码部分。这里不会使用所有数据。随着我们通过本章的进展,我们将扩展数据,所以请只关注我们实际使用的内容:

var mySeminar = {
  guest1 : { name: "Pete", seating: "standard" },
  guest2 : { name: "Re-Pete", seating: "balcony" },
  conference : {
    name: "KnockOut 2K",
    byLine: "MVVM That Works"
  }, speakers : [
    { id: 1, name: "John Doe", bio: 'This is the bio for John.',
      skills: [ "jQuery","KnockoutJS","SammyJS","NodeJS" ] },
    { id: 2, name: "Mary Smith", bio: 'This is the bio for Mary.',
      skills: [ "jQuery","KnockoutJS","PHP" ] },
    { id: 3, name: "TBA", bio: 'This is the bio for ???.',
      skills: [ "HTML5","SQL","JavaScript" ] }
  ], sessions : [
    { name: "SPA Applications", speakerId: 1 },
    { name: "MVVM Best Practices", speakerId: 1 },
    { name: "Mapping Madness", speakerId: 2 },
    { name: "Custom Components", speakerId: 2 },
    { name: "Browser Database for Beginners", speakerId: 3 }
  ]
};
vm = ko.mapping.fromJS(mySeminar);
ko.applyBindings( vm );

你可能已经猜到了,看着我们的数据,我们将展示一个伪的,在这个情况下意味着假的,但“伪”听起来要顺耳得多,研讨会页面。你应该意识到的是,数据将以各种形式出现。你可能需要调整数据,使其最适合你的页面。不要认为这里有任何严格的模式你必须遵循。如果你需要重新排列你的数据,这是任何模板方法的一部分,你可以做到。

现在,我说这个部分将关于原生模板。这意味着你将按照 Knockout 创建模板的标准方式创建这些模板。这应该是你大多数时候应该做的事情。我们稍后会展示如何集成外部模板库;但让我一开始就明确说,在现实世界中,这是开发者如何在 Knockout 页面上使用模板的一种非常罕见的方法。

Knockout 遵循大多数浏览器端模板所采用的方法。实际的模板嵌套在一系列脚本标签内。如果你是新手,想知道为什么这样做时没有 JavaScript 错误,让我来解释一下。过去,微软的 Internet Explorer 使用一种名为 VBScript 的脚本。大多数其他浏览器使用一种名为 JavaScript 的脚本。今天,我们再次看到一些其他脚本出现在 Internet Explorer 上。TypeScript 是一个例子,但它似乎需要 Visual Studio 才能正确运行,并且不能作为原生脚本运行;因此,为了便携性,继续使用 JavaScript 似乎是正确的选择。这些标签创建了一种声明它们内部内容类型的方法。如果内容不被识别,它基本上会被忽略。

当脚本被忽略时,这意味着从标记代码中没有创建 DOM。尽管如此,JavaScript 仍然可以看到这段代码。因此,通过声明不同的类型属性,我们能够将嵌套 HTML 转换为可操作的代码,用于我们的模板。这也允许我们实际上使用熟悉的 HTML 脚本。许多人使用类似 jQuery 的东西来创建内容,但它远不如模板方法优雅。事实上,在一段时间内,jQuery 有自己的模板。它撤回的理由是模板不是其解决方案重点的一部分。

这里是我们将用于我们页面的第一个模板。我们需要在这里包含 Knockout 和映射库的代码示例。映射库不是做模板代码所必需的。我们只是用它来使我们的示例更简单。这使得对主题的关注更加集中,你在阅读本书时需要编写的代码也更少。

<script type="text/html" id="guest-template">
    <h3 data-bind="text: name"></h3>
    <p>Seating: <span data-bind="text: seating"></span></p>
</script>

你会注意到我们在模板代码段上放置了一个id属性。每个模板都需要一个唯一的 ID 才能正常工作。在我们的模板中,这看起来与我们在整本书中看到的 Knockout 代码非常相似。这是 Knockout 模板的一个优点。它建立在您已经知道并使用的知识上,几乎没有例外。在制作模板时,一个重要的考虑因素是模板应该在绑定 ViewModel 的实际调用之前。以下是我们将包含的两个外部库,以使这个页面工作。script标签可以在这之前或之后。它只需要在绑定调用之前,以防止问题。

<script src="img/knockout.js"></script>
<script src="img/knockout.mapping.js"></script>

我们需要做的最后一件事,为了使用模板绑定,是在代码中调用模板。模板类似于墙壁模板。你可以选择在图案中放置什么。你可以选择合并到模板中的内容。这意味着你必须有与模板匹配的数据。在这个模板中,你需要传递一个包含姓名和座位结构项的 ViewModel 结构。以下是带有声明数据的模板调用:

<div data-bind="template: { name: 'guest-template', data: guest1 }"></div>

如果你有一个没有数据合并的模板想要使用,你可以在页面上重用它。我不确定你为什么要这样做,但这是可能的。注意,在将数据传递给模板时,我们使用的是经典的 JSON 格式方法来传递内容。让我们稍微修改一下前面的代码,并使用两个传递不同数据的模板,以及模板附近的一点点标记:

<h2>Guests</h2>
These are the guests:
<div data-bind="template: { name: 'guest-template', data: guest1 }"></div>
<div data-bind="template: { name: 'guest-template', data: guest2 }"></div>

注意,我们连续两次使用了我们的模板,唯一的区别是它们传递给模板的 ViewModel 结构不同。你也应该注意到,模板名称是我们脚本模板声明中id属性的值。以下是该模板的结果截图:

原生模板

由于姓名和座位是结构中的变量,模板合并时会自动理解作用域。如果你听到这个术语,将数据与模板渲染合并是一种常见的做法。

我们合并到模板中的实际数据,在插入到 ViewModel 之前,如下所示:

guest1 : { name: "Pete", seating: "standard" }

好吧,我们现在可以快速轻松地看到我们的数据与模板混合在一起。如果我们进入浏览器开发者工具中的控制台,我们可以输入以下命令。我们想要修改 ViewModel 内的数据,看看绑定是否会对我们的视图产生即时影响:

vm.guest1.name('Peter')

当我们这样做时,注意模板会智能且立即更新内容。因此,我们看到我们的数据绑定已经锁定在模板中,就像模板外的绑定一样。开源市场上有很多不同的模板。Knockout 之所以强大,是因为它将渲染的内容绑定到当连接的数据发生变化时重新生成。已经有一些框架的比较,Knockout 在这方面也表现优于其他框架。所以,除非你正在做非常庞大的事情,否则让这个运行实时不会对你的用户体验产生很大影响。话虽如此,别忘了测试性能,因为这始终是最佳实践。以下是更新的截图:

原生模板

好吧,现在皮特(Pete)或彼得(Peter)在哪里,还有 Re-Pete 的嘉宾们?这是我们接下来的代码块。我们已经有了数据,但我们将要使用的数据部分是这样的:

conference : {
  name: "KnockOut 2K",
  byLine: "MVVM That Works"
}

这里是显示和绑定到它的模板的代码:

<h2>Seminar</h2>
<div data-bind="template: { name: 'tmpl-Seminar', data: conference }"></div>

我们需要另一个模板来演示合并不同数据集。以下是该模板的代码:

<script type="text/html" id="tmpl-Seminar">
  <h3 data-bind="text:name"></h3>
  <p>Theme: <span data-bind="text:byLine"></span></p>
</script>

注意,我们的第一个模板使用了一个名为name的变量。在这个模板中,我们也在使用一个名为name的变量。每个模板在渲染时,只会查看传递给它的数据以合并内容。以下是本例中模板渲染到页面上的截图:

原生模板

在页面上使用模板的数量或可以合并的数据量没有基本限制。我们不会假设某个开发者或客户端请求可能超过极限。然而,浏览器和计算机如此之快,这些问题很少会成为关注的焦点。唯一可能成为问题的情况是,如果你正在使用 ViewModel 中的变量进行复杂操作,而这个变量基于复杂的逻辑计算值。但到目前为止,我没有在我的公司为我们的客户制作的工作中找到这样的案例。

增强型收集处理

在我们的第一个部分,原生模板中,我们关注单一数据集。在 JavaScript 中,我们经常将数据存储在数组中。我们在这里将包括一个示例,以展示如何使用存储在数组中的模板。

我们可以从上一个示例页面复制数据到这个页面。这次我们将使用更多的结构。我们将首先关注演讲者。这个数组包含每个演讲者的结构化项。我们的script标签的代码部分应该看起来像这样:

vm = ko.mapping.fromJS(mySeminar);
ko.applyBindings( vm );

如果你认为这看起来基本上和上一个代码示例相同,你是正确的。它很快就会改变,但我们在这里想要表达的是,数组和非数组在常规绑定功能上没有不同的编码。以下是我们将用于这部分练习的数据段:

speakers : [
  { id: 1, name: "John Doe", bio: 'This is the bio for John.',
    skills: [ "jQuery","KnockoutJS","SammyJS","NodeJS" ] },
  { id: 2, name: "Mary Smith", bio: 'This is the bio for Mary.',
    skills: [ "jQuery","KnockoutJS","PHP" ] },
  { id: 3, name: "TBA", bio: 'This is the bio for ???.',
    skills: [ "HTML5","SQL","JavaScript" ] }
],

我们将把这个数据与以下模板结构混合,并且随着我们的进行,它将会得到增强:

<script type="text/html" id="tmpl-Speakers">
  <h3 data-bind="text:name"></h3>
  <p>Bio: <span data-bind="text:bio"></span></p>
</script>

在这里,我们可以看到名称和简介将会混合到我们的页面中,从每个集合记录的数据中提取名称和简介。这是我们页面上将要使用的模板调用:

<h2>Speakers</h2>
<div data-bind="template: { name: 'tmpl-Speakers', foreach: speakers, as: 'speaker' }"></div>

这次我们在模板代码中使用 foreach 而不是 data。当使用 data 时,它将一次传递整个数据结构。使用 foreach 将一次传递数据结构作为一个集合项。模板实际上对数据是如何传递给它的没有意识。这是由我们调用模板的方式控制的。以下是运行代码时我们将看到的结果:

增强型集合处理

好吧,那是对集合的处理;但这一节被称为增强型集合处理。我们将深入探讨,尽管使用 Knockout,我们仍将保持事情简单,代码轻量。

如以下代码所示,在每一个演讲者记录中嵌套的还有另一个简单的演讲者技能集合。为了清晰起见,以下是集合中的第一个项目:

{ id: 1, name: "John Doe", bio: 'This is the bio for John.',
    skills: [ "jQuery","KnockoutJS","SammyJS","NodeJS" ] },

我们可以看到,John Doe 在 jQuery、KnockoutJS、SammyJS 和 NodeJS 方面有技能。这是一个 JavaScript 高手!他可能还有其他未列出的才能,但我们只能展示他给出的那些,当然。我们如何做到这一点?嗯,Knockout 模板的一个美妙功能是能够在模板中嵌套其他模板调用。以下是我们将添加以显示技能的新模板:

<script type="text/html" id="tmpl-SpeakerSkills">
    <li data-bind="text:skill"></li>
</script>

显然,我们需要修改主模板以使其工作。我们使用 HTML 列表来显示技能,因此我们需要在列表包装标签中嵌套列表。我们还想添加一个标题来明确这个列表代表的内容。以下是我们的修改后的模板,其中新代码被突出显示:

<script type="text/html" id="tmpl-Speakers">
  <h3 data-bind="text:name"></h3>
  <p>Bio: <span data-bind="text:bio"></span></p>
  <h4>Skills</h4>
 <ul data-bind="template: { name: 'tmpl-SpeakerSkills', foreach: $data.skills, as: 'skill' }"></ul>
</script>

注意,还有一个 foreach 模板绑定,因为传入的数据是一个集合。上一次它是一个结构集合,这次它是一个简单值集合。Knockout 对集合中存在什么类型的项并不关心。它需要是一个集合,或者更确切地说,当你使用foreach传递数据时,你指定的数组。

对演讲者模板的原始调用确实需要修改。现在我们的第一个演讲者记录看起来是这样的。当然,所有其他记录也都有添加的技能,但我们想节省空间;所以请确保你在浏览器中做所有这些示例,使用你亲手输入的代码,以最大限度地提高你的学习体验。

增强型集合处理

这已经相当不错了,但还不够好。我们承诺要介绍增强的集合模板技术,还有一件事我们想展示给你。嵌套是增强我们的集合模板的最简单形式。现在,我们想展示如何使用会话记录进行一些关系数据处理。以下是我们将混合的会话记录数据段:

sessions : [
  { name: "SPA Applications", speakerId: 1 },
  { name: "MVVM Best Practices", speakerId: 1 },
  { name: "Mapping Madness", speakerId: 2 },
  { name: "Custom Components", speakerId: 2 },
  { name: "Browser Database for Beginners", speakerId: 3 }
]

你应该已经注意到speakerId属性是数据收集的一部分。这就是我们将如何关联记录的方式。正确关联这些记录只需要少量代码,但这确实只是少量代码。我们需要添加以下高亮代码以使此功能正常工作。我们在映射和绑定之间添加了此代码:

vm = ko.mapping.fromJS(mySeminar);
vm.speakerSessions = function(speakerId){
 var mySessions = [];
 ko.utils.arrayMap(vm.sessions(),function(session){
 if(speakerId() == session.speakerId()){
 mySessions.push(session);
 }
 });
 return mySessions;
};

这次我们不是使用集合传递给模板;好吧,不是一个绑定数组集合。我们将通过触发一个函数来调用数据,该函数将过滤并返回一个数组集合。这意味着只要我们在调用时传递一个数组,模板调用就不关心。

我们还使用了 Knockout 工具的arrayMap方法来过滤数据与筛选。注意,我们通过指定将 ViewModel 作为筛选的第一个参数。然后,它将逐个传递每个会话项以查看该项是否应放置在筛选集合中。然后,它将返回结果。代码很简单。哦,是的,它在调用时确实接收了speakerId属性作为参数。这是使事情正常工作的关键。看看下面的script代码:

<script type="text/html" id="tmpl-SpeakerSessions">
  <h4>Sessions</h4>
  <ul data-bind="foreach:$root.speakerSessions(id)">
    <li data-bind="text:name() +' with '+ speaker.name()"></li>
  </ul>
</script>

注意,我们使用了$root指定来调用我们的方法。这是因为我们想要提醒你,由于这是一个嵌套模板,我们需要确保我们的方法调用处于正确的级别。$root指定允许你访问 ViewModel 的实际根级别。传入的 ID 与演讲者数据项匹配。我们添加的speakerSessions代码将然后与具有相同 ID 的记录匹配。如果有多个会话,它将显示它们。

为了使此功能正常工作,我们还需要在我们的主模板中包含对会话的嵌套调用。同样,我们不需要修改对演讲者的原始调用。以下是带有新部分高亮的代码:

<script type="text/html" id="tmpl-Speakers">
  <h3 data-bind="text:name"></h3>
  <p>Bio: <span data-bind="text:bio"></span></p>
 <div data-bind="template: { name: 'tmpl-SpeakerSessions', data: $data }"></div>
  <h4>Skills</h4>
  <ul data-bind="template: { name: 'tmpl-SpeakerSkills', foreach: $data.skills, as: 'skill' }"></ul>
</script>

抽空看看代码;发现我们调用额外嵌套模板和第一个嵌套模板之间的区别。你会看到在我们的额外方法中,我们没有使用foreach指定符传递数据。这次我们使用了数据指定符来传递我们的集合。这两种方法都是有效的,但我们想给你提供两种方法的示例,以供你参考。

这是结果的第一条记录(再次,为了节省空间,没有显示所有内容)。注意这次嵌套模板是如何使用 Knockout 的经典绑定来处理foreach指定符的:

增强集合处理

渲染事件处理

随着经验的增长,你越来越欣赏的是处理事件的能力。在我们构建的现场网站中,我还没有使用 Knockout 代码需要这个功能;但知道它在那里,如果需要的话,是非常棒的。

这次我们将使用以下代码。我们将为这个例子使用不同的数据集,以使我们的代码更简单,并专注于这一部分:

<script>
    var ViewModel = function(){
        seasons = ko.observableArray([
            { name: 'Spring', months: [ 'March', 'April', 'May' ] },
            { name: 'Summer', months: [ 'June', 'July', 'August' ] },
            { name: 'Autumn', months: [ 'September', 'October', 'November' ] },
            { name: 'Winter', months: [ 'December', 'January', 'February' ] }
        ]);
    showRendered = function(e){
      $(e).wrapInner("<em style='color:green'></em>");
    };
    };
  vm = new ViewModel();
    ko.applyBindings(vm);
</script>

这也告诉我们,我们并不总是认为映射是最好的方法。在页面的早期原型设计阶段,通常最好使用映射,因为它可以加快所需内容的创建速度。然后,如果需要,我们可以迁移到一个非映射的 ViewModel,以最大化生产力。如果你的经验有所不同,那么就跟随你的直觉,使用最适合你团队的方法。

到现在为止,数据和绑定都很明显。我们添加的函数将使用 jQuery 对每个指定的项目进行包装,这些项目由 Knockout 以斜体和绿色渲染。我想到的这种实用用途之一是单元测试日志,以确保输出符合预期。无论你用它做什么,它都会向你展示一些事情。

首先,我们在使用 Knockout 时并没有完全与 jQuery 断绝联系。有一种我喜欢向那些担心使用 Knockout 会消除 jQuery 使用的人描述的方法。是的,我记得在 jQuery 出现之前,JavaScript 对大多数人来说有多难。它们都有不同的优势。Knockout 在数据绑定方面表现出色,而 jQuery 在客户端和服务器之间的 AJAX 通信以及 DOM 操作方面表现出色。它们一起组成了一支伟大的团队,每个成员都有各自的优势和不同的挑战,最适合解决。

下面是我们将在本例中使用的模板。我们还将再次使用嵌套模板。这是根模板:

<script type="text/html" id="seasonTemplate">
    <li>
        <strong data-bind="text: "></strong>
        <strong data-bind="text: name"></strong>
        <ul data-bind="template: { name: 'monthTemplate', foreach: months, as: 'month' , afterRender: showRendered}"></ul>
    </li>
</script>

我们将月份称为嵌套模板调用。虽然我们可以只用常规标签来渲染所有这些内容,但这不会显示给我们事件处理程序是如何工作的。仔细观察模板调用中的嵌套数据绑定,你会看到一个afterRender的设置。你会看到它调用了我们添加到代码中的函数。由于它位于根模板中,因此它不需要使用$root指示符来正确调用函数方法以处理渲染事件。以下是它将调用来渲染月份项的嵌套模板:

<script type="text/html" id="monthTemplate">
    <li>
        <span data-bind="text: month"></span>
        is in
        <span data-bind="text: season.name"></span>
    </li>
</script>

我还想要你注意这里的一些特别之处。注意我们在模板中使用了变量season.name。我们能够从嵌套模板中访问父级的数据结构。编写 Knockout 库的那些人将许多这样的东西组合在一起,真是令人印象深刻。那么,季节是在哪里声明的呢?看看对根模板的实际调用:

<ul data-bind="template: { name: 'seasonTemplate', foreach: seasons, as: 'season' }"></ul>

你会看到一个 as 指示符,其中 season 字符串被声明为结构的别名。因此,我们可以在模板的第一级使用 $data,但在模板的第二级使用 $data 就不会正确工作。通过声明别名 season,我们可以在嵌套子项中使用它,并且它将正确地针对正确的数据集。太棒了!

现在,当我们查看我们的结果时,我们得到了正确处理和渲染的内容。我承认,虽然我从 jQuery 1.2 版本之前就开始使用了,但我的 Knockout 历史比这还要晚。如果你在使用早期版本时遇到问题,可能不支持我们在这里教授的每个功能,请加入在线小组。你将在 Knockout 网站上找到它。那里的社区在相互支持方面非常棒。

下面是这段代码的结果截图:

渲染事件处理

第三方模板选项

曾经有一段时间,jQuery 在探索创建自己的模板,但这项努力并没有成熟到完整发布的程度。因此,我们在这里不会介绍那个模板。结果是,与 Knockout 一起使用最流行的模板似乎是 Underscore。就我个人而言,在接触 Knockout 之前,我选择的模板是,嗯,是 Handlebars。然而,我非常喜欢 Knockout 模板,以至于它从未让我有动力将 Handlebars 与 Knockout 集成。

我只是玩了一下 Underscore,只是为了尝尝为什么其他人会用 Underscore 与 Knockout 一起使用;看看我是否遗漏了什么。这归结为一个开发者风格差异,一些开发者喜欢使用 Underscore 风格的模板编码,而其他人则更喜欢使用原生的 Knockout 模板。在本节中,我们将尊重 Underscore 粉丝的方法。

我们建议从示例代码的 done 目录中复制粘贴一些内容,以防止出现错误。如果你足够厉害,可以直接输入这些内容。再次提醒,你的示例应该放入每个章节的 do 文件夹中。

下面是这个示例的基础代码:

<script>
var viewModel = {
    people: ko.observableArray([
        { name: ko.observable('Rod'), age: ko.observable(123) },
        { name: ko.observable('Jane'), age: ko.observable(125) }, 
        { name: ko.observable('Mary'), age: ko.observable(25) }
    ])
};
ko.applyBindings(viewModel);
</script>

下面是我们调用模板的代码:

<h1>People</h1>
<ul data-bind="template: { name: 'peopleList' }"></ul>

现在,这是我们的模板。你应该注意到这里的模板风格不同。如果你习惯于 Underscore,这将工作得很好。如果你没有使用 Underscore,你不应该假设这个模板中的代码是原生 Knockout 模板的示例。

<script type="text/html" id="peopleList">
    <% _.each(people(), function(person) { %>
        <li>
            <b data-bind="text: person.name"></b> is <%= person.age() %> years old
        </li>
    <% }) %>
</script>

这张截图显示了我们将产生的以下结果。等等!有一块缺失,但我们确实想先展示一下结果。

第三方模板选项

对于最终用户来说,没有任何区别。使用第三方模板是为了开发者的利益。嗯,如果你签订的合同需要使用 Underscore,那么这种方法将带来另一个好处,因为它将允许你在允许的情况下使用 Underscore 和 Knockout 完成工作。提前验证这一点可能是个好主意,以确保一切顺利。

使用 Underscore 修改模板处理

让我再表达一次我的感激之情,并向 Google 群组中的 Knockout 社区表示感谢。除了几个在线网站外,这里是我学习如何处理模板的地方。这项技术使我们能够管理模板引擎,并将结果传递给 Underscore 引擎进行处理。关键代码如下所示:

/* ---- Begin integration of Underscore template engine with Knockout. Could go in a separate file of course. ---- */
    ko.underscoreTemplateEngine = function () { }
    ko.underscoreTemplateEngine.prototype = ko.utils.extend(new ko.templateEngine(), {
        renderTemplateSource: function (templateSource, bindingContext, options) {
            // Precompile and cache the templates for efficiency
 var precompiled = templateSource'data';
 if (!precompiled) {
 precompiled = _.template("<% with($data) { %> " + templateSource.text() + " <% } %>");
 templateSource'data';
 }
            // Run the template and parse its output into an array of DOM elements
 var renderedMarkup = precompiled(bindingContext).replace(/\s+/g, " ");
            return ko.utils.parseHtmlFragment(renderedMarkup);
        },
        createJavaScriptEvaluatorBlock: function(script) {
            return "<%= " + script + " %>";
        }
    });
    ko.setTemplateEngine(new ko.underscoreTemplateEngine());
/* ---- End integration of Underscore template engine with Knockout ---- */

第一个突出显示的部分将代码和数据传递给 Underscore 模板处理器以由 Underscore 渲染。Underscore 确实产生了一些需要清理才能正确工作的结果,因此第二个突出显示的部分用于清理渲染的代码。

Knockout 中的实时更新和订阅方法

我们将创建在屏幕上添加带有编辑表单的项的能力。以下是我们将为此功能添加的视图的标记:

<input data-bind="value: name" />
<input data-bind="value: age" />
<button data-bind="click: addItem">Add</button>

我们还需要添加 ViewModel 结构,它将包括以下项目:

name: ko.observable(),
age: ko.observable(),

addItem: function() {
var item = { name: ko.observable(viewModel.name()), age: ko.observable(viewModel.age()) };
   viewModel.people.push(item);
   viewModel.name("");
   viewModel.age("");
}

当添加或删除项时,Knockout 给我们监控这些变化的能力,这正是我们想要展示的。这是我们用来告诉 Knockout 我们想要订阅或监听事件的代码。我们将监听的事件将是 people 数组集合上的 arrayChange 事件。以下是相应的代码。注意,我们还将结果输出到控制台。我们还将把更改的结果推送到另一个用于人员更改的属性。

viewModel.people.subscribe(function(e){
  console.log(e);
}, viewModel, "arrayChange");
viewModel.people.subscribe(viewModel.peopleChange, viewModel, "arrayChange");

我们将传递给 peopleChange 的结果,将通过在视图上使用绑定将它们转换为 JSON,如下所示。每当 peopleChange 中的值更新时,结果将以 JSON 结构显示。

<pre data-bind="text: ko.toJSON(peopleChange, null, 2)"></pre>

现在,以防第一次浏览时感到困惑或你不想下载代码,让我在这里提供标记视图代码:

<h1>People</h1>
<ul data-bind="template: { name: 'peopleList' }"></ul>
<script type="text/html" id="peopleList">
    <% _.each(people(), function(person) { %>
        <li>
            <b data-bind="text: person.name"></b> is <%= person.age() %> years old
        </li>
    <% }) %>
</script>
<pre data-bind="text: ko.toJSON(peopleChange, null, 2)"></pre>
<input data-bind="value: name" />
<input data-bind="value: age" />
<button data-bind="click: addItem">Add</button>

当所有内容都在一个地方时,看起来要简单得多;嗯,对我来说是这样的。这次包含的脚本更多;以下是本例所需的脚本集合:

<script  src="img/jquery.mi n.js"></script>
<script  src="img/bootstrap.min.js "></script>
<script src="img/knockout.js"></script>
<script  src="img/underscore- min.js"></script>

是的,这些代码也可以与当前版本的 jQuery 一起使用,如果你有这个问题的话。现在,我们将展示本例页面的代码:

<script>
/* ---- Begin integration of Underscore template engine with Knockout. Could go in a separate file of course. ---- */
    ko.underscoreTemplateEngine = function () { }
    ko.underscoreTemplateEngine.prototype = ko.utils.extend(new ko.templateEngine(), {
        renderTemplateSource: function (templateSource, bindingContext, options) {
            // Precompile and cache the templates for efficiency
            var precompiled = templateSource'data';
            if (!precompiled) {
                precompiled = _.template("<% with($data) { %> " + templateSource.text() + " <% } %>");
                templateSource'data';
            }
            // Run the template and parse its output into an array of DOM elements
            var renderedMarkup = precompiled(bindingContext).replace(/\s+/g, " ");
            return ko.utils.parseHtmlFragment(renderedMarkup);
        },
        createJavaScriptEvaluatorBlock: function(script) {
            return "<%= " + script + " %>";
        }
    });
    ko.setTemplateEngine(new ko.underscoreTemplateEngine());
/* ---- End integration of Underscore template engine with Knockout ---- */
var viewModel = {
    people: ko.observableArray([
        { name: ko.observable('Rod'), age: ko.observable(123) },
        { name: ko.observable('Jane'), age: ko.observable(125) }, 
        { name: ko.observable('Mary'), age: ko.observable(25) }
    ]),
  peopleChange: ko.observable(),
    name: ko.observable(),
    age: ko.observable(),
    addItem: function() {
       var item = { name: ko.observable(viewModel.name()), age: ko.observable(viewModel.age()) };
       viewModel.people.push(item);
       viewModel.name("");
       viewModel.age("");
    },
    removeItem: function(item) {
       viewModel.people.remove(item);
    }
};
viewModel.people.subscribe(viewModel.peopleChange, viewModel, "arrayChange");
viewModel.people.subscribe(function(e){
  console.log(e);
}, viewModel, "arrayChange");
ko.applyBindings(viewModel);
</script>

现在,我们有代码可以显示第三方模板、订阅以及我们绑定中存储数据的动态修改。以下是屏幕将呈现的样子。嗯,在我的 done 版本的代码中,它被 Bootstrap 包裹起来,所以这就是它在 Bootstrap 中的样子:

Knockout 中的实时更新和订阅方法

我们将继续添加John到列表中,并让他40岁。这是你将在截图中所看到的,其中subscribe的结果以 JSON 结构显示。如果有一条记录被删除,它也会显示出来;这也是 Knockout 之所以如此灵活的原因之一。你也会在我们的代码中注意到,一旦我们通过绑定将项目添加到我们数据结构中,我们也清除了输入框,以保持用户界面的整洁。以下是结果截图:

Knockout 中的实时更新和 subscribe 方法

让我再重复一遍,大多数人并不使用第三方模板,因为原生模板已经非常强大。这并不意味着这样做是错误的,但这样做可能只是需要学习如何使用原生模板的能力。如果是这样的话,我们希望本章能够满足这一需求。

出色的模板选项

有些人可能还想用与 Knockout 纯原生风格不同的模板来做一些事情。这可能是因为你可能喜欢使用稍微不同一点的编码风格。在本章的最后部分,我们将为您提供更多选项。

我们回到了原生模板;我想我们应该提一下这一点,以防有人有疑问。以下是我们将在这个页面上使用的模板:

<script type="text/html" id="guest-template">
    <h3>{{name}}</h3>
    <p>Seating: {{seating}}</p>
</script>
<script type="text/html" id="guest-template-alt">
    <h3>Others</h3>
    <p>Seating: General Seating</p>
</script>
<script type="text/html" id="tmpl-Seminar">
  <h3>{{name}}</h3>
  <p>Theme: {{byLine}}</p>
</script>

我们将使用本章开头使用的相同数据来演示这个例子。除此之外,我们还将添加一个插件库到这个例子中。这显然是我最喜欢的 Knockout 库之一,即Punches库。毕竟,没有 Punches 的 Knockout 会是什么样子?因此,我们需要包含 jQuery、Knockout、Knockout-mapping 和 Knockout punches,这样现在就可以工作了。

这里是包含在本页上的传统模板的绑定数据:

<h2>Guests</h2>
These are the guests:
<div data-bind="template: { name: 'guest-template', data: guest1 }"></div>
<div data-bind="template: { name: 'guest-template', data: guest2 }"></div>

这里是我们从页面上的这一段代码中得到的输出。如果你花时间查看我们的模板,你可能已经注意到了一些不同之处。我们用来告诉模板我们想要合并哪些变量的编码风格现在被双大括号包围了。然而,我们的输出正如我们所预期的那样,无需深入挖掘并使用data-bind属性来传递结果:

出色的模板选项

现在,我们将看看 Punches 模板语法。我们包括标准绑定的代码,以确保没有人会假设这是属于script标签的代码。这是 HTML 代码,应该直接添加到我们页面上的视图代码中,如下所示:

<h2>Guests</h2>
These are the guests:
<div data-bind="template: { name: 'guest-template', data: guest1 }"></div>
<div data-bind="template: { name: 'guest-template', data: guest2 }"></div>
{{#template {name:'guest-template', data: guest1} /}}
{{#template {name:'guest-template', data: guest2} /}}

当您再次运行代码时,您将看到PeteRe-Pete两次输出相同的结果。有些人可能因为个人风格的原因,可能更喜欢 Punches 风格的编码。您可能还记得我提到过,我以前最喜欢的编码风格是 Handlebars。这与 Handlebars 编码语法相似,因此对我来说效果最好;如果它不是您的风格,那么就把它当作开发者个性的差异吧。以下是更新后的截图:

出色的模板选项

我现在还使用另一种方法在页面上放置模板。我正在使用由 Knockout 驱动的自定义标签。好吧,有点像。它们被一个我称之为"KOmponents"的库修改过。您将在下一章中了解更多关于 Knockout 自定义标签的信息,但在这里展示一个实际操作似乎很合适。我们将第三次在页面上放置输出。以下是修改后的视图标记:

<h2>Guests</h2>
These are the guests:
<div data-bind="template: { name: 'guest-template', data: guest1 }"></div>
<div data-bind="template: { name: 'guest-template', data: guest2 }"></div>
{{#template {name:'guest-template', data: guest1} /}}
{{#template {name:'guest-template', data: guest2} /}}
<kom-template data="guest1" template="'guest-template'"></kom- template>
<kom-template data="guest2" template="'guest-template'"></kom- template>

好了,运行代码后,您将看到页面上出现了第三组PeteRe-Pete。在我们的屏幕图像中,我们将跳过再次复制PeteRe-Pete。目标是向您展示它们都输出了相同的内容。

我们将通过在以下所有guest-template部分下方添加以下内容来完成此操作:

<hr>
{{#template 'guest-template-alt'/}}
<hr>
<h2>Seminar</h2>
<div data-bind="template: { name: 'tmpl-Seminar', data: conference }"></div>

这些将输出模板的内容,以下是一些结果。你看,我们选择在其中一个中使用 Punches 和 Handlebars 风格的组合。

出色的模板选项

摘要

本章为您提供了对 Knockout 模板的全面介绍。如果您以前从未使用过模板,我们希望您能欣赏模板的概念。如果您以前使用过,我们希望您喜欢 Knockout 模板提供的数据绑定自动更新能力的丰富性。

在本章中,我们学习了模板的作用以及如何使用 Knockout 来操作它们。我们学习了如何将模板嵌套在集合和非集合结构中。我们还学习了如何使用事件触发器,并在模板结果中混合一点 jQuery 来修改。此外,我们还学习了如何混合第三方模板技术,订阅观察者的概念,以及将模板合并到页面上的几种额外方法。我们还学习了如何使用 Punches 库的替代绑定风格。

在下一章中,我们将深入探讨构建自定义 HTML 标签的奇妙之处。有没有什么时候你觉得自己使用的标准 HTML 标签有限?你实际上想要一些具有可编程交互功能的 HTML 风格标签?跳到下一章,你将在这两种情况下找到你想要的东西。

第六章.打包优雅

首先,有 HTML 和 JavaScript,然后是 CSS。接下来是 AJAX,它开启了 Web 2.0 时代,正如它所称呼的那样。之后,模板驱使我们进入了一个更加动态、富有创造性的平台。网络开发的下一个进步是自定义 HTML 组件。KnockoutJS 允许我们以一些改变游戏规则的风格直接进入,这对于设计师和开发者来说都是一种优雅的方式。在本章中,我们将关注:

  • 组件简介

  • 自带标签BYOT

  • 增强属性处理

  • 创建自己的库

  • 异步模块定义AMD)——按需资源加载

  • 基于组件的 单页应用程序SPA

整个章节都是关于打包你的代码以供重用。使用这些技术,你可以让你的代码更加易于接近和优雅。

组件简介

我们希望你在上一章中学习模板时感到愉快。也许对组件最好的解释就是一个带有隔离 viewModel 的打包模板。以下是我们在页面上声明 like 组件将使用的语法:

<div data-bind="component: "like"''"></div>

如果你没有通过组件传递任何参数,这是正确的语法。如果你希望传递参数,你可以使用以下 JSON 风格的结构:

<div data-bind="component:
{name: 'like-widget',params:{ approve: like} }"></div>

这将允许我们通过自定义组件传递命名参数。在这种情况下,我们传递了一个名为 approve 的参数。这意味着我们通过名为 like 的绑定 viewModel 变量。看看这将如何编码。创建一个名为 components.html 的页面,使用 _base.html 文件来加快速度,就像我们在所有其他章节中做的那样。在你的 script 部分,创建以下 ViewModel:

<script>
ViewModel = function(){
  self = this;
  self.like = ko.observable(true);
}
;
// insert custom component here
vm = new ViewModel();
ko.applyBindings(vm);
</script>

现在,我们将创建我们的自定义组件。以下是我们将用于此第一个组件的基本组件。将代码放在注释处,因为我们想确保它在 applyBindings 方法执行之前被添加:

ko.components.register('like-widget', {
  viewModel: function(params) {
    this.approve = params.approve;
    // Behaviors:
    this.toggle = function(){ 
      this.approve(!this.approve()); 
    }.bind(this);
  },
  template:
    '<div class="approve">\
      <button data-bind="click: toggle">\
        <span data-bind="visible: approve" class="glyphicon  glyphicon-thumbs-up"></span>\
        <span data-bind="visible:! approve()" class="glyphicon  glyphicon-thumbs-down"></span>\
      </button>\
    </div>'
});

我们的组件有两个部分:viewModeltemplate 部分。在前一章中,我们学习了如何使用 Knockout 模板。在本章中,我们将使用组件内部的这些细节。

标准的 Knockout 组件使用 params 结构将变量传递给组件。我们可以使用这个结构,或者如果你愿意,也可以选择使用 *self = this* 方法。除了设置变量结构外,还可以为我们的组件创建行为。如果我们查看模板代码,我们可以看到我们已将点击事件绑定到组件中的 approve 设置切换。然后,在按钮内部,通过绑定到 span 元素的可见特性,将向用户显示点赞或踩不点赞的图片。是的,我们在这里使用的是 Bootstrap 图标元素而不是图形。以下是初始状态的截图:

组件简介

当我们点击缩略图图像时,它将在点赞和踩图之间切换。由于我们还传递了一个绑定到页面 ViewModel 的外部变量,因此我们可以看到匹配的 span 文本中的值也会切换。以下是我们在代码的视图部分添加的标记,以产生这些结果:

<div data-bind="component: {name: 'like-widget', params:{ approve: like} }"></div>
<span data-bind="text: like"></span>

您也可以使用 jQuery 插件构建此类功能,但可能需要更多的代码来实现双向绑定并匹配我们在这里实现的紧密功能。这并不意味着 jQuery 插件不好,因为这同样是一种与 jQuery 相关的技术。这意味着我们有一些方法可以做得更好。这位作者认为,此类功能仍然可以很好地添加到核心 jQuery 库中。然而,我并不期待他们采纳类似 Knockout 的项目到他们目前拥有的精彩项目集合中,并且不觉得我们应该因此对他们有所偏见。专注于他们最擅长的事情是 Knockout 等库能够提供更广泛选项的原因之一。看起来,他们的决定正在为我们工作,即使它们采取了与我预期不同的方法。

动态组件选择

当我们选择组件时,我们应该已经注意到我们使用的是引号声明的。虽然一开始可能看起来更加限制,但请记住,这实际上是一个强大的功能。通过使用变量而不是硬编码的值,您可以动态选择要插入的组件。以下是标记代码:

<div data-bind="component: { name: widgetName, params: widgetParams }"></div>
<span data-bind="text:widgetParams.approve"></span>

注意,我们传递了widgetName以及widgetParams。由于我们以不同的方式绑定结构,因此我们还需要以不同的方式在我们的 span 中显示绑定的值。以下是需要添加到我们的viewModel代码中的script部分的代码:

  self.widgetName = ko.observable("like-widget");
  self.widgetParams = {
    approve: ko.observable(true)
  };

我们将得到相同的外观结果,但请注意,每个点赞按钮都是独立于其他按钮操作的。如果我们页面上放置多于一个相同的元素会发生什么?如果我们这样做,Knockout 组件将独立于其他组件操作。嗯,大多数时候它们是独立操作的。如果我们将它们绑定到相同的变量,它们将不会独立。在你的viewModel声明代码中,添加另一个名为like2的变量,如下所示:

self.like2 = ko.observable(false);

现在,我们将通过复制我们的第一个点赞视图代码来向页面添加另一个点赞按钮。这次,将值从like更改为like2,如下所示:

<like-widget params="approve: like2"></like-widget>
<span data-bind="text: like2"></span>

这次当页面加载时,其他点赞显示为点赞,但这个点赞将显示为踩图。文本还将显示绑定值中存储的false。任何点赞按钮都将独立操作,因为每个按钮都绑定到唯一的值。以下是第三个按钮的截图:

动态组件选择

带上您自己的标签(BYOT)

什么是元素?基本上,元素是一个使用标签语法访问的组件。这是目前官方文档中的表述方式,并且很可能会保持这种方式。它仍然是底层的组件。根据你所处的群体,这种区别可能重要也可能不重要。主要的是,要意识到这种区别,以防有人认为它很重要,这样你就可以在讨论中保持一致。自定义标签是即将到来的 HTML 特性 Web Components 的一部分。Knockout 允许你从今天开始使用它们。以下是视图代码:

<like-widget params="approve: like3"></like-widget>
<span data-bind="text: like3"></span>

你可能想要用单个标签而不是成对的标签(即开始和结束标签语法)来编写一些标签。然而,目前,将自定义元素标签声明为单个标签时,每个浏览器都会遇到挑战。这意味着自定义标签或元素目前需要声明为开始和结束标签。

我们还需要使用以下代码为viewModel创建like3绑定变量:

self.like3 = ko.observable(true);

运行代码会给我们带来与data-bind方法相同的功能,但现在我们正在创建自己的 HTML 标签。有没有什么时候你想要一个特殊但并不存在的 HTML 标签?现在,使用 Knockout 组件元素风格的编码,你有可能创建出这样的标签。

增强属性处理

现在,虽然自定义标签很棒,但将所有内容通过单个param属性传递确实有一种不同的感觉。原因在于这个过程与我们在使用data-bind编码方法时标签的工作方式相匹配。在下面的例子中,我们将通过单个属性传递内容。这并不是要作为一个data-bind方法来工作,但它完全集中在自定义标签元素组件上。

你首先想要做的是确保这个增强功能不会对正常元素造成任何问题。我们通过检查自定义元素的标准前缀来做到这一点。你不需要深入理解这段代码,因为它稍微有些复杂。最简单的方法是使用以下script标签包含我们的 Knockout 组件标签:

<script src="img/knockout.komponents.js"></script>

在这个标签中,我们有一个代码段,用于将以kom-开头的标签转换为使用单个属性而不是属性的 JSON 转换的标签。你可以自由地借用这段代码来创建自己的库。我们将在 GitHub 上创建一组标准的库来为这些组件标签服务。由于 HTML 标签是 Knockout 组件,我们将这些库称为"KOmponents"。资源可以在github.com/sosensible/komponents找到。

现在,包含了这个库之后,我们将使用我们的视图代码来连接到新的标签。以下是视图中的代码:

<kom-like approve="tagLike"></kom-like>
<span data-bind="text: tagLike"></span>

注意,在我们的 HTML 标记中,标签以库前缀开始。这也将要求viewModel有一个绑定来传递给这个标签,如下所示:

self.tagLike = ko.observable(true);

下面的代码是 Knockout 组件实际“属性感知版本”的代码。不要将此代码放入代码中,因为它已经包含在共享目录中的库中:

// <kom-like /> tag
ko.components.register('kom-like', {
  viewModel: function(params) {
    // Data: value must but true to approve
    this.approve = params.approve;
    // Behaviors:
    this.toggle = function(){ 
      this.approve(!this.approve()); 
    }.bind(this);
  },
  template:
    '<div class="approve">\
      <button data-bind="click: toggle">\
        <span data-bind="visible: approve" class="glyphicon glyphicon-thumbs-up"></span>\
        <span data-bind="visible:! approve()" class="glyphicon glyphicon-thumbs-down"></span>\
      </button>\
    </div>'
});

在视图中,随着我们通过命名属性传递信息而不是在参数属性内的 JSON 结构中传递信息,标签发生了变化。我们还确保通过使用前缀来管理这些标签。这样做的原因是我们不希望我们的花哨标签破坏与常规 Knockout 组件一起普遍使用的传递参数的标准方法。

如我们所见,我们再次有一个功能组件,它增加了能够以更熟悉 HTML 标签编码的方式传递值的优势。

构建自己的库

再次强调,我们称之为自定义组件 KOmponents。我们将随着时间的推移创建许多库解决方案,并欢迎其他人加入。标签不会为我们做所有事情,因为还有一些限制尚未克服。但这并不意味着我们等待所有功能都准备好后再去做我们现在能做的事情。在前一章中,我们展示了用于使用模板的KOmponent标签,这也包含在基本 KOmponent 库中。

在本章的这一部分,我们还将展示一些来自我们的 Bootstrap KOmponents库的标签。首先,我们需要包含 Bootstrap KOmponents库:

<script src="img/knockout.komponents.bs.js"></script>

在我们的脚本中viewModel之上,我们需要添加一个函数来使这段代码更简单。有时,在将项目传递给可观察对象时,我们可以使用这样的函数传递更丰富的绑定数据。再次,在脚本中viewModel声明之上创建这个函数,如下所示:

var listItem = function(display, students){
  this.display = ko.observable(display);
  this.students = ko.observable(students);
  this.type = ko.computed(function(){
    switch(Math.ceil(this.students()/5)){
      case 1:
      case 2:
        return 'danger';
        break;
      case 3:
        return 'warning';
        break;
      case 4:
        return 'info';
        break;
      default:
        return 'success';
    }
  },this);
};

现在,在viewModel内部,我们将声明一组数据,以便将其传递给 Bootstrap 样式的listGroup,如下所示:

self.listData = ko.observableArray([
  new listItem("HTML5",12),
  new listItem("CSS",8),
  new listItem("JavaScript",19),
  new listItem("jQuery",48),
  new listItem("Knockout",33)
]);

我们数组中的每个项目都将包含显示、学生和类型变量。在这里我们使用了 Bootstrap 的许多功能,但将它们全部打包在我们的 Bootstrap 智能标签中。这个标签开始超越最基本的功能。它仍然非常易于实现,但我们不想一次性给你太多内容去吸收,所以我们将不会深入讲解这个标签的详细代码。我们想要展示的是,可以将多少功能封装到自定义 Knockout 标签中。以下是我们将用来调用这个标签并绑定viewModel的正确部分的标记:

<kom-listgroup data="listData" badgeField="'students'" typeField="'type'"></kom-listgroup>

就这些。你应该注意几个特殊细节。数据作为绑定的 Knockout ViewModel 传递。徽章字段作为字符串名称传递,以在数据集合中声明徽章计数将被提取的字段。对于类型字段也使用了相同的字符串方法。这里的主题是,如果没有足够的学生来维持一个班级,那么在列表组自定义标签中会显示危险颜色。以下是代码在浏览器中运行时的样子:

构建自己的库

虽然这很酷,但让我们跳到我们的浏览器工具控制台,并更改其中一个项目的值。假设有一个名为 jQuery 的酷炫网络技术上的类。如果人们没有听说过它,不知道它是什么,而你真的很想上这个课,那会怎么样?嗯,鼓励其他人去看看会很好。你如何知道这个课程是否处于危险水平?嗯,我们可以简单地使用徽章和数字,但使用颜色编码提示有多酷?在控制台中输入以下代码,看看会发生什么变化:

vm.listData()[3].display()

由于 JavaScript 从零开始计数第一个项目,我们将得到以下结果:

构建自己的库

现在我们知道我们找到了正确的项目,所以让我们在浏览器控制台中使用以下代码将学生计数设置为九:

vm.listData()[3].students(9)

注意 jQuery 类的变化。徽章和类型值都已更新。此更新截图显示了我们可以用极少的手动编码发挥多大的力量:

构建自己的库

我们还应该花点时间看看如何管理类型。使用函数赋值,我们能够使用 Knockout 计算绑定来处理该值。以下是该部分的代码再次:

this.type = ko.computed(function(){
  switch(Math.ceil(this.students()/5)){
    case 1:
    case 2:
      return 'danger';
      break;
    case 3:
      return 'warning';
      break;
    case 4:
      return 'info';
      break;
    default:
      return 'success';
  }
},this);

虽然代码位于viewModel声明之外,但它仍然能够正确绑定,使我们的代码即使在用 Knockout 的组件绑定创建的自定义标签内也能运行。

Bootstrap 组件示例

这是另一个使用 Bootstrap 进行绑定的示例。使用模态显示框的一般最佳实践是将它们放置在代码的较高位置,可能在body标签下,以确保不会与代码的其他部分发生冲突。将此标签放置在以下代码的body标签下方,如所示:

<kom-modal id="'komModal'" title="komModal.title()" body="komModal.body()"></kom-modal>

再次强调,为了使这个功能正常工作,我们还需要在viewModel内部做一些声明。将以下代码输入到viewModel的声明中:

self.komModal = {
  title: ko.observable('Modal KOMponent'),
  body: ko.observable('This is the body of the <strong>modal KOMponent</strong>.')
};

我们还将在页面上创建一个按钮来调用我们的viewModel。该按钮将使用 Bootstrap 的一部分绑定。data-toggledata-target属性不是 Knockout 绑定功能。尽管如此,Knockout 与它们协同工作得非常好。另一个值得关注的点是标准的 ID 属性,它说明了像这样的按钮如何与模态框交互。这也是使用 KOmponents 或类似库可能有益的另一个原因。以下是标记代码:

<button type="button" data-toggle="modal" data- target="#komModal">Open Modal KOmponent</button>

当我们点击按钮时,我们看到的是这个请求者:

Bootstrap 组件示例

现在,为了理解 Knockout 与我们的请求者协同工作的强大功能,请回到你的浏览器工具控制台。在提示符中输入以下命令:

vm.komModal.body("Wow, live data binding!")

以下截图显示了变化:

Bootstrap 组件示例

谁知道我们可以使用这种技术构建出什么样的创意模块化盒子。这让我们更接近于创造我们所想象的东西。也许它将使我们更接近于构建客户所想象的那些疯狂事物。虽然这可能不是你使用 Knockout 的主要动机,但当我们想要发挥创意时,有几个障碍会更少会很好。同时,也很棒能够拥有这种将解决方案打包和跨网站重用的能力,而无需复制粘贴和搜索代码,当客户对代码进行更改以进行更新时。

再次提醒,你可以查看这些文件以了解我们如何使这些组件工作。一旦你掌握了使用 Knockout 及其组件的基本知识,它们并不复杂。如果你想要构建自己的组件,它们将帮助你了解如何在技能提升到下一个水平时内部实现。

理解 AMD 方法

我们将探讨构成 AMD 风格网站的概念。这种网站方法的目的是按需获取内容。内容,或如在此定义的模块,不需要按特定顺序加载。如果有部分依赖于其他部分,当然,这是可以管理的。我们将使用 RequireJS 库来管理我们代码的这一部分。

在这个例子中,我们将创建四个文件,如下所示:

  • amd.html

  • amd.config.js

  • pick.js

  • pick.html

在我们的 AMD 页面中,我们将为 RequireJS 功能创建一个配置文件。这就是上述列表中提到的 amd.config.js 文件。我们将从以下代码开始创建此文件:

// require.js settings
var require = {
    baseUrl: ".",
    paths: {
        "bootstrap":        "/share/js/bootstrap.min",
        "jquery":           "/share/js/jquery.min",
        "knockout":         "/share/js/knockout",
        "text":             "/share/js/text"
    },
    shim: {
        "bootstrap": { deps: ["jquery"] },
        "knockout": { deps: ["jquery"] },
    }
};

我们可以看到,我们正在创建一些别名并设置这些名称指向此页面的路径。当然,这个文件可以为多个页面工作,但在这个例子中,它专门为单个页面创建。在 RequireJS 的配置中,文件名不需要 .js 扩展名,正如你所注意到的。

现在,我们将查看我们的 amd.html 页面,在那里我们将整合所有内容。我们再次使用本课程中使用的标准页面,如果你预览代码示例中的 done 文件,你会注意到这一点。尽管如此,也有一些不同之处,因为并非所有的 JavaScript 文件都需要在开始时调用。RequireJS 为我们很好地处理了这一点。我们并不是说这是 AMD 的标准实践,但它是对这些概念的介绍。

在这个例子中,我们需要包含以下三个脚本文件:

<script src="img/knockout.js"></script>
<script src="img/amd.config.js"></script>
<script src="img/require.js"></script>

注意,在调用 require.js 库之前需要设置配置设置。设置好之后,我们可以创建代码来在页面上绑定 Knockout。这部分代码位于页面的底部 amd.html 脚本中:

<script>
ko.components.register('pick', {
  viewModel: { require: 'pick' },
  template: { require: 'text!pick.html' }
});
viewModel = function(){
  this.choice = ko.observable();
}
vm = new viewModel();
ko.applyBindings(vm);
</script>

这段代码的大部分应该看起来非常熟悉。区别在于外部文件被用来设置pick组件中viewModeltemplate的内容。require设置智能地知道要包含pick.js文件以供pick设置使用。当然,它需要作为字符串传递。当我们包含模板时,你将看到我们在包含的文件前使用了text!。在这种情况下,我们还在文件名上声明了扩展名。实际上,文本方法需要知道文本的来源,你将在我们的amd.config.js文件中看到我们为文本函数的包含创建了一个别名。

现在,我们将创建pick.js文件,并将其放置在与amd.html文件相同的目录中。它也可以放在另一个目录中,你只需在组件声明中设置该目录以及文件名即可。以下是我们的 AMD 组件这一部分的代码:

define(['knockout'], function(ko) {
    function LikeWidgetViewModel(params) {
        this.chosenValue = params.value;
        this.land = Math.round(Math.random()) ? 'heads' : 'tails';
    }
    LikeWidgetViewModel.prototype.heads = function() {
        this.chosenValue('heads');
    };
    LikeWidgetViewModel.prototype.tails = function() {
        this.chosenValue('tails');
    };
    return LikeWidgetViewModel;
});

注意到我们的代码以define方法开始。这是我们 AMD 功能的具体实现。这意味着在我们尝试执行这段代码之前,我们需要确保 Knockout 库已加载。这允许我们按需加载所需的代码。viewModel部分内部的代码与我们所查看的其他示例相同,只有一个例外。我们在前一段代码的末尾返回viewModel。在这个例子中,我们使用了简写代码来设置headstails的值。

现在,我们将查看我们的模板文件,pick.html。这个文件中的代码如下:

<div class="like-or-dislike" data-bind="visible: !chosenValue()">
<button data-bind="click: heads">Heads</button>
<button data-bind="click: tails">Tails</button>
</div>
<div class="result" data-bind="visible: chosenValue">
    You picked <strong data-bind="text: chosenValue"></strong>
    The correct value was <strong data-bind="text: land"></strong>
</div>

这个模板遵循我们在上一章中查看的概念。除了使这个例子工作所需的代码外,没有特别之处。目标是允许自定义标签在页面上提供 heads 或 tails 选项。我们还从viewModel传递了一个绑定变量。我们将将其传递到三个相同的标签中。

在这个例子中,标签实际上会立即加载内容。目标是熟悉代码的工作方式。我们将在本章末尾进行完整的实践。现在,我们将这段代码放在amd.html页面的视图部分:

<h2>One Choice</h2>
<pick params="value: choice"></pick><br>
<pick params="value: choice"></pick><br>
<pick params="value: choice"></pick>

注意我们包含了pick标签三次。虽然我们正在从viewModel传递绑定选择项,但每个标签将随机选择 heads 或 tails。当我们运行代码时,我们会看到以下内容:

理解 AMD 方法

由于我们将相同的绑定项传递给了三个标签,当我们点击任何headstails设置时,它将立即将该值传递到viewModel,然后viewModel将立即将值传递回其他两个标签集。它们都通过viewModel绑定相同的变量连接在一起。这是点击Tails时我们得到的结果:

理解 AMD 方法

嗯,这是我们当时得到的结果。实际上,每次刷新页面,结果都会有很大的变化。现在,我们准备通过结合我们的 AMD 方法和 Knockout 模块来做一些特别的事情。

基于组件的 SPA

现在,我们可以看看网络开发中增长最快的趋势之一,即单页应用的概念。这个名称比实际做法更花哨,因为它被笼罩在困惑之中,但我们将在这里帮助你克服这一点。

首先,将你的网站视为一台计算机或移动设备。你可以在系统上安装多个应用程序。这些应用程序可以协同工作或独立工作。虽然我们理解我们的计算系统,但当我们转向网络服务器时,我们似乎会感到困惑。一切并不都是一个应用程序,或者更确切地说,很少应该一切都是一个应用程序。在 Windows 出现之前,你的电子表格和文字处理器被作为一个应用程序一起发货。这非常受欢迎,但随着时间的推移,我们意识到将两个应用程序打包比试图将跨功能构建到一个应用程序中更明智。因此,我们创建了办公套件。

今天,我们正遭受大多数服务器平台所犯错误的影响。当我们的后端服务器语言被创建时,其中许多创建了围绕“应用”这一术语的包装方法。因此,当你用购物车、论坛和博客构建一个网站时,它们都在同一个应用范围内运行。事后看来,我们应该将这些网站范围和将购物“应用”、论坛“应用”和博客“应用”作为“网站”下的项目进行标记。

单页应用似乎也犯了一个同样的错误。其架构和命名约定往往会引起混淆。作为一个经验丰富的开发者,让我自大地给你一个时刻,允许你在这个问题上跳出思维定式。暂时忘记 SPA 这个名称,让我们退后一步,仅仅理解它做了什么。一旦你理解了它做了什么,你将能够明智地想出如何应用它。毕竟,良好的实现,或者它应该是,目标是。

最佳开发策略

与最佳开发策略相关的目标有很多。事实是,没有一种单一的最好策略。这类似于在技术圈中普遍分享的真理:这取决于。这意味着在你确定什么最适合之前,你需要知道目标需求。有些人陷入了认为尖端解决方案是最佳策略的误区。有时它们是,但并非普遍如此。这应该是一个提醒,要清醒地做出决定。事实是,除非我们在完成业务,否则技术不会给画面增添任何东西。

技术是强大的。技术是关于知识的。如果我们正确理解了挑战,那么我们可以更好地选择我们的技术、我们的方法和我们的解决方案。有时我们可能会选择 Knockout,有时则只坚持使用 jQuery。有时,我们会引入其他产品的混合,如 RequireJS 等。

Getting real

我们应该避免的另一件事是尝试应用我们从未实际在任何地方使用过的技术。这是一个高风险的赌注,这也是为什么我希望你在阅读这本书的过程中已经做了代码示例。

我们现在将创建一个解决方案,通俗地说,一个 SPA,使用 Knockout 及其组件来驱动视图。因为概念被称为 SPA,我们将把这些动态加载的视图称为“页面”。我们将基于 AMD 动态加载内容的概念,同时使用一些其他的 SPA 概念。这里的目的是不是要教你所有关于 SPA 的细节,但如果你之前从未尝试过,这将为你提供一个相当不错的起点。

我们的内容,如果你喜欢的话,代码,将被分成一个名为pages的目录,每个页面都有自己的模板和viewModel文件。之前,我们重复了页面组件的模板和 ViewModel。这次,我们将使用组件来处理页面内容,以及使用组件来声明主视图内容。为了清晰起见,我们为非页面组件创建了一个components文件夹;例如,导航栏将被放在components文件夹中。

使 SPA 适合基本单页描述的是,浏览器一次只加载一个基本页面。之后,所有内容都被拉入并注入到页面中。因此,从技术上讲,只有一个页面是正确的。页面上内容的显示是即时加载和更新的,但从技术上讲,它仍然是在原始页面上。这将是我们index.html文件。如果你查看你的练习根目录,你会看到一个名为spa的子目录,另一个名为myspa

如果你想看看一个完成的 SPA,你可以提前看看 SPA 版本。我的建议是逐步构建,并采取探索的方式。只有在你觉得自己犯了错误时,才将其与 SPA 版本进行比较。以下是我们的起点截图,使用myspa示例:

Getting real

这看起来相当简单,但实际上比你想象的要复杂。在架子上,强大的工具通常看起来不起眼。查看主页的代码是值得的。以下是页面的视图或模板文件:

<h2>Home</h2>
<p data-bind=: message'"html"></p>

通常,当我们查看 Knockout 以获取绑定数据时,我们正在寻找基本的viewModel数据。在这种情况下,我们正在查看viewModel组件中的数据。让我们看看组件的代码,并查找我们的消息变量来了解它从哪里来:

define(["knockout", "text!./view.html"], function(ko, homeTemplate) {
  function myViewModel(route) {
    this.message = ko.observable('This is dynamic content presented on the home page.');
  console.log(vmSPA);
  }
  return { viewModel: myViewModel, template: homeTemplate };
});

我们在这里再次看到了define方法,它告诉我们我们正在使用 RequireJS 正确地处理依赖关系。我们在代码的myViewModel部分看到了viewModel属性消息。请记住使用this类型的作用域,否则属性将无法正确绑定并在viewModel中可见。

我们刚刚开始接触 SPA 网站和页面的工具。虽然现在可能还感觉不到它的神奇,但随着你对这里工具的使用,它的美将逐渐显现。就像欣赏伟大艺术家的画笔一样。你无法预见到他们创作出的惊人画作。现在是时候教你如何使用创建强大 SPA 解决方案的工具,并提高你对 Web 开发的掌握能力。

编码时间

我们首先将查看我们的单页,其中展示了所有内容。以下是超级无敌一切页面的代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>SPA Guide</title>
  <link href="/share/css/bootstrap.min.css" rel="stylesheet">
  <link href="css/styles.css" rel="stylesheet">
  <script src="img/require.config.js"></script>
  <script data-main="app/startup" src="img/require.js"></script>
</head>
<body>
  <div id="page" class="container" data-bind="component: { name: route().page, params: route }"></div>
</body>
</html>

我们在这里看到了两个 JavaScript 文件。第一个是配置文件,第二个是 RequireJS 文件。这并不是所有需要的 JavaScript 文件,但 RequireJS 文件将负责引入使网站正常运行所需的核心库。这些库包含在配置文件中。让我们来看看这个文件:

// require.js looks for the following global when initializing
var require = {
    baseUrl: ".",
    paths: {
        "bootstrap":            "/share/js/bootstrap.min",
        "crossroads":           "/share/js/crossroads.min",
        "hasher":               "/share/js/hasher.min",
        "jquery":               "/share/js/jquery.min",
        "knockout":             "/share/js/knockout",
        "knockout-projections": "/share/js/knockout-projections.min",
        "signals":              "/share/js/signals.min",
        "text":                 "/share/js/text"
    },
    shim: {
        "bootstrap": { deps: ["jquery"] }
    }
};

这是我们之前在 AMD 示例中使用的相同逻辑。你会注意到我们在代码中使用了多个库。如果你更喜欢这样表达,RequireJS 处理了大部分按需加载或按需加载的工作。

你还会注意到在index.html页面的body部分有一个 Knockout 绑定的div元素,如下所示:

<div id="page" class="container" data-bind="component: { name: route().page, params: route }"></div>

这是我们如何提取页面内容并将其驱动到用户视图的方法。如果你是这种应用的新手,可能会觉得有些东西缺失。实际上有一个文件被调用,但并不立即明显。看看调用require.js到页面的script标签:

<script data-main="app/startup" src="img/require.js"></script>

注意data-main属性绑定。这告诉页面从app文件夹加载startup.js文件。让我们来看看这个文件:

define(['jquery', 'knockout', './router', 'bootstrap', 'knockout- projections'], function($, ko, router) {
  // General Components (can be packaged as AMD modules):
  // dynamic page components
  ko.components.register('home-page', { require: 'page/home/home' });
  // static page components
  // Start the application
  vmSPA = function() { 
    this.route = router.currentRoute;
  };
  vm = new vmSPA();
  ko.applyBindings(vm);});

我们看到了RequireJS define方法,这里不需要返回,因为这里的目的是确保与 Knockout 正确绑定。你还会看到它提前声明了它需要的所有库,基于我们require.config.js文件中的设置列表。

接下来,你会看到动态页面组件的注册。目前我们只有一个页面,所以如果你查看page/home文件夹,你会看到home.jsview.html作为那里找到的文件。我们很快就会查看这些文件,但它们是我们页面组件的viewModeltemplate项。

大多数单页应用(SPA)的共同特点之一是路由。其他 JavaScript 库处理这个问题,这就是通过route属性绑定到我们的全局绑定。所有这些共同工作,在之前的截图中被称作“不那么出色”的页面。router文件是我们构建 SPA 时需要处理的最后一个核心组件。让我们看看它的代码:

define(["knockout", "crossroads", "hasher"], function(ko, crossroads, hasher) {

  return new Router({
    routes: [
      { url: '', params: { page: 'home-page' } }
    ]
  });
  function Router(config) {
    var currentRoute = this.currentRoute = ko.observable({});
    ko.utils.arrayForEach(config.routes, function(route) {
      crossroads.addRoute(route.url, function(requestParams) {
      currentRoute(ko.utils.extend(requestParams, route.params));
    });
  });
  activateCrossroads();
  }
  function activateCrossroads() {
          function parseHash(newHash, oldHash) { 
                crossroads.parse(newHash); 
          }
    crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
    hasher.initialized.add(parseHash);
    hasher.changed.add(parseHash);
    hasher.init();
  }
});

再次,我们看到define方法在这里发挥作用,正确地管理了依赖关系。还有路由,包括路由,或者将包括路由。目前,只有一个路由,那就是我们的主页视图。这就是我们将处理更多页面视图的地方。这里的其他代码是路由功能所需的,但你不需要比理解 jQuery 核心代码更多地去理解它。所以,如果你想探索它,欢迎你这样做。否则,只需专注于你需要知道的内容,那就是这段代码中的路由数组部分。

添加导航

要添加导航,我们需要做两件事。导航组件已经创建好了,所以我们只是将其添加到我们的 SPA 解决方案中。首先,打开app文件夹中的startup.js文件。在通用组件注释下,添加以下代码行:

  ko.components.register('nav-bar', { require: 'components/nav-bar/nav-bar' });

我们需要在调用组件之前注册它们,这就是我们在 SPA 中做这件事的地方。现在,我们将在index.html文件的body部分顶部添加nav-bar标签,如下所示:

<nav-bar params="route: route"></nav-bar>

哦,虽然那覆盖了代码,但还需要一件事。为了显示我们的导航,我们需要将内容和页面视图向下移动足够的空间以显示导航。我们在元素上放置了一个页面 ID;所以,打开css/style.css文件并输入以下代码:

#page {
  margin-top: 80px;
}

好吧,保存并关闭这个文件,因为我们在这个练习中不再需要它了。我们现在有了 SPA 的基本导航栏。现在事情看起来是这样的:

添加导航

添加页面

让我们添加一个关于页面。关于页面将不同于我们的主页,因为它将只包含静态内容。这次我们不会绑定到viewModel。在startup.js文件中,在静态页面组件注释下,添加以下代码:

  ko.components.register('about-page', {
    template: { require: 'text!page/about/about.html' }
  });

注意我们只添加了一个模板,没有viewModel。我们还需要添加导航的链接。这里的代码基于 Bootstrap 的使用。这是我们在components/nav-bar/nav-bar.html文件中的代码之后添加的:

<li data-bind="css: { active: route().page === 'about-page' }">
<a href="#about">About</a>
</li>

我喜欢根据这个路由是否匹配这个页面来管理活动设置有多简单。如果你看看我们注册的组件和route().page的比较,你会看到当它们匹配时,值会使页面变得活跃。我们还需要做一件事才能让about页面工作——那就是about页面组件本身。在page文件夹中,将_base文件复制为about

创建新页面的最后一件事是将页面添加到我们的Router数组中,该数组位于app/router.js文件中:

{ url: 'about',     params: { page: 'about-page' } }

这是我们选择about导航项时页面的样子。(如果你遇到显示问题,请重新加载应用程序,一旦更改后它就会工作。)

添加页面

注意高亮显示工作得非常好,为用户提供反馈以验证他们实际查看的页面。本页面的静态内容和主页面的动态内容为我们理解如何构建 SPA 网站奠定了基础。然而,我们可以做得更好。我们将添加一个页面,允许我们更新用于查看网站的 Bootstrap 皮肤。

定制样式的时间

我们将从列出我们在向这种基于 Knockout 的 SPA 网站添加另一个页面时做的事情开始:

  1. 将页面组件注册添加到app文件夹中的startup.js文件。

  2. 如果需要,在nav-bar标签中添加一个菜单项。

  3. 将路由配置添加到app文件夹中的router.js文件。

  4. page文件夹中创建一个新页面,并添加适当的viewModeltemplate代码文件。

这是前三个步骤的代码。首先是startup.js文件的注册代码。在home-page组件注册之后立即添加此代码,如下所示:

ko.components.register('bootstrap-page', { require: 'page/bootstrap/bootstrap' });

然后,我们将添加以下代码到nav-bar组件中:

<li data-bind="css: { active: route().page === 'bootstrap-page' }">
<a href="#bootstrap">Bootstrap</a>
</li>

接下来,我们将向那个router.js文件添加以下代码,以便我们的系统知道如何处理新的路由项。

{ url: 'bootstrap', params: { page: 'bootstrap-page' } },

当然,最后一阶段将是创建实际的页面组件文件。再次,我们将复制_base文件夹。这次,将复制的文件夹重命名为boostrap。在文件夹内,将_base.js文件重命名为bootstrap.js,然后你就可以开始编码了。以下是开始时的template代码。将此代码放置在page/bootstrap文件夹下的view.html文件中,如下所示:

<h2>Bootstrap</h2>
<p data-bind='html: message'></p>

这将是放置在page/bootstrap文件夹下的boostrap.js文件中的viewModel代码:

define(["knockout", "text!./view.html"], function(ko, bootstrapTemplate) {
  function myViewModel(route) {
    this.message = ko.observable('Welcome to Bootstrap Page.');
  }
  return { viewModel: myViewModel, template: bootstrapTemplate };
});

现在,当我们运行此代码时,点击Bootstrap导航菜单后,我们得到以下视图。啊,等等!在保存截图之前,我注意到模板的标题不可见。我的 CSS 尚未保存,因此偏移没有正确工作。现在,经过修正,以下是正确的视图。你应该会发现其他视图现在也会正确显示。有多少人在看我的截图时注意到了这一点?在这里,我们再次将截图展示出来:

定制样式的时间

这确实看起来更好。它也开始成形,并呈现出一个有希望的网站。我们需要进入并修改我们的 ViewModel,使这个页面功能强大。在我们这样做之前,我们需要实际回到主index.html文件,并给页面样式标签添加一个id属性,如下所示:

<link id="page_style" href="/share/css/bootstrap.min.css" rel="stylesheet">

这将允许我们使用 jQuery 来定位和更新页面样式。是的,我们在这里使用 jQuery 而不是 Knockout。这种情况仍然会发生。现在,在boostrap.js文件中,将消息变量替换为以下代码:

var self = this;
self.message = ko.observable('Hello from the bootstrap component! \
On this page we will be letting you choose from different <a target="_blank" href="http://bootswatch.com/">Bootswatch</a> themes.');
  self.theme = ko.observable('none');
  self.themes = ko.observableArray([]);
  self.themesVisible = ko.observable(true);

提示

注意,我们使用斜杠作为换行符来在下一行继续编码。我们中的一些人熟悉这个快捷键,但其他人如果不知道这个技巧可能会感到高兴。

我们接着添加themethemesthemesVisible属性到我们的 ViewModel 中。我们还需要在页面上添加几个方法。这些将是loadThemeschangeTheme方法。

loadThemes方法将拉取一个基于 JSON 的数据文件,采用 AJAX 风格。我们还将使用 jQuery 来实现这个功能。以下是 JSON 文件的内容。如果你不是打字高手,我建议直接从匹配的 SPA 复制文件。这也会进入page/bootstrap文件夹:

{
  "theme": [
    { "name": "cerulean", "bg":"light"},
    { "name": "cosmo", "bg":"light"},
    { "name": "cyborg", "bg":"dark"},
    { "name": "darkly", "bg":"dark"},
    { "name": "flatly", "bg":"light"},
    { "name": "journal", "bg":"light"},
    { "name": "lumen", "bg":"light"},
    { "name": "paper", "bg":"light"},
    { "name": "readable", "bg":"light"},
    { "name": "sandstone", "bg":"light"},
    { "name": "simplex", "bg":"light"},
    { "name": "slate", "bg":"dark"	},
    { "name": "spacelab", "bg":"light"},
    { "name": "superhero", "bg":"dark"},
    { "name": "united", "bg":"light"},
    { "name": "yeti", "bg":"light" }
  ]
}

现在,我们需要将剩余的代码添加到page/bootstrap文件夹中的bootstrap.js文件中。首先,我们将添加loadThemes方法。以下是该方法的代码:

myViewModel.prototype.loadThemes = function() {
  jQuery.getJSON("page/bootstrap/data.json")
.done(function(data){
    self.themes(data.theme);
    self.themesVisible(false);
  }).fail(function(data){
    alert('fail data pull for Bootstrap page');
  });
};

这段代码会将数据拉取并直接推送到数据绑定的主题数组中。请注意,我们还包含了一些代码来展示异常处理的基本概念。现在,添加changeTheme函数的代码:

myViewModel.prototype.changeTheme = function(data,event) {
  var style = '/share/css/bootswatch/'+data.name+'/bootstrap.min.css';
  jQuery('#page_style').attr('href',style);
  vm.bg(data.bg);
};

我们将能够在viewModel中绑定这些方法来处理模板代码中的事件触发。不过,我们还需要处理这个功能的一个额外问题。注意方法末尾的vm.bg(data.bg)。我们正在设置传入项的值来设置一个全局变量。它告诉系统我们使用的背景是浅色还是深色背景。我们目前不会使用它,但现在我们正在创建它以便稍后使用。为了做到这一点,我们需要在app文件夹中的主站starup.js文件中添加以下代码段:

vmSPA = function(){ 
  this.route = router.currentRoute;
  this.bg = ko.obervable("light");
};

现在,我们可以在view.html文件中添加我们的模板代码,位于现有代码下方。请注意,按钮的可见性被管理以改善用户体验。当我们的数据加载完成后,我们将按钮设置为不可见。我们还将图像的点击方法连接到viewModel组件上的changeTheme方法。此时,组件没有基础变量别名,因此我们将使用$parent别名来定位此数据结构父级的方法。看看下面的代码:

<button data-bind="click: loadThemes, visible: themesVisible'">Load Themes</button>
<div class="row" data-bind="foreach: themes">
<div class="col-xs-6 col-md-3" style="text-align: center;">
  <img class="img-responsive img-thumbnail" data-bind="attr:{ 'src':'/share/css/bootswatch/'+$data.name+'/thumbnail.png'}, click: $parent.changeTheme"></img>
  <br/><br/>
</div>
</div>

好的,我们现在都已经连接好了。如果你有问题,可以将代码与SPA文件夹中的代码进行比较,因为那里的是正常工作的,并且是这次重构练习示例代码的来源。以下是重新加载并选择 Bootstrap 导航项时的初始截图:

定制样式的时间

哦,如果你不认识它们,这些皮肤样式是由bootswatch.com为我们提供的开源样式。那里也有商业解决方案。

现在,我们将点击按钮来从基于 JSON 的数据中加载我们的主题。这也可以是一个实时动态服务器位置 URL。点击按钮后,我们得到以下内容:

定制样式时间

如果我们向下滚动并点击超级英雄,然后移动鼠标,我们会看到以下内容:

定制样式时间

注意

我不确定为什么我们必须移动鼠标,但大多数情况下这会起作用;当然,如果这是一个实时网站,我们会找出即时更新的方法。

现在,我们可以根据需要选择性地更新样式。如果你切换到不同的页面,你会看到外观保持不变。如果这是一个实时登录网站,我们会使用 AJAX 将用户样式选择存储回服务器。我们也可以选择将它们存储在 cookie 变量中,但这样如果用户切换到另一台机器,他们的选择将不会随之移动。这就是为什么我们建议尽可能将它们存储在服务器上。

奖励项目

哦,这本书的这一部分有一个奖励。这本书基于 Knockout 3.2,但 Knockout 3.3 提出了一个很好的变更,这将允许我们使用组件别名来处理组件代码。更新我们的代码以尝试这个功能非常简单。首先,我们需要前往app文件夹中的require.config.js文件并做出一个更改。我们必须更改 Knockout 别名所指向的文件,如下所示:

"knockout": "/share/js/knockout.3_3_alpha:

然后,我们回到bootstrap文件夹中的view.html文件,将$parent变量更改为$component。接下来,我们再次运行我们的代码,它应该可以正常工作。这是一段更实用的代码,如果组件中的数据结构更复杂,这种实用的别名将使代码尽可能简单。

构建跨页面交互

当我们创建 Bootstrap 页面时,我们按需加载数据。这意味着它不会在用户点击并告诉主题加载之前加载。在这个页面上,数据将立即、自动加载到页面上。展示这两种方法似乎很有帮助,可以让你不必自己弄清楚这些选项。

这次我们将使用 jQuery 页面。我们选择它有几个原因。首先,我们恰好欣赏 jQuery 多年来为社区所做的一切。第二个原因是,我们注意到他们有深色和浅色标志样式。我们借用了他们的图形来展示它们在深色 Bootstrap 主题以及浅色 Bootstrap 主题上的样子。

这里是 jQuery 页面的代码。首先是startup.js文件的注册代码。在 Bootstrap 注册后添加此代码:

ko.components.register('jquery-page', 
{ require: 'page/jquery/jquery' });

然后我们将添加以下代码到nav-bar组件中:

<li data-bind="css: { active: route().page === 'jquery-page' }">
  <a href="#jquery">jQuery</a>
</li>

接下来,我们将把这段代码添加到router.js文件中,以便我们的系统知道如何处理新的路由项。到目前为止,这是完整的路由代码部分:

return new Router({
  routes: [
    { url: '',          params: {page: 'home-page' } },
    { url: 'about',     params: {page: 'about-page' } },
    { url: 'bootstrap', params: {page: 'bootstrap-page' } },
    { url: 'jquery',     params:{ page: 'jquery-page' } }
  ]
});

当然,最后一阶段将是创建实际的页面组件文件。再次,我们将复制_base文件夹。这次,将复制品重命名为jquery。在文件夹内,将_base.js文件重命名为jquery.js,然后你就可以开始编码了。我们还有从 SPA 的工作版本中拉取的文件。我们将拉取data.json文件和logos文件夹。

我们现在可以首先编辑我们的viewmodel文件。以下是jquery.js文件的代码:

define(['knockout', 'text!./view.html'], function(ko, templateMarkup) {
  function myViewModel(params) {
    var self = this;
    self.message = ko.observable('Hello from the jQuery component!');
    self.project = ko.observableArray();
  jQuery.getJSON('page/jquery/data.json').done(function(data){
      self.project(data.project);
    }).fail(function(){
      alert("AJAX error on jQuery Page View.");
    });
  }
  myViewModel.prototype.logo = function(data,event) {
    var url = 'page/jquery/logos/'+data.base+'-mark- '+vm.bg()+'.gif';
    return url;
  };
  // This runs when the component is torn down. Put here any logic necessary to clean up,
  // for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
  myViewModel.prototype.dispose = function() { };
  return { viewModel: myViewModel, template: templateMarkup };
});

我们再次有我们的defined方法,它接受两个项目的值并将它们作为参数传递给函数。声明很简单,只有一个区别。这次,我们将jQuery JSON请求的结果传递到self.project绑定中。我们再次有非常基本的异常处理,以防出现异常,因为它们是优秀编码的一部分。这次,我们在组件中创建的方法是设置标志名称的方法。以下是我们的view.html代码,这样我们可以查看它是如何集成的。注意,在方法内部,我们再次使用主要的 SPA viewModel绑定来拉取浅色或深色背景:

<h2>jQuery</h2>
<p data-bind="text: message'"></p>
<div class="row" data-bind="foreach: project">
  <div class="col-md-4">
    <img class="img-responsive img-thumbnail" data-bind="attr:{ 'src':$ component.logo($data)}">
  </div>
  <div class="col-md-8">
    <h3 data-bind="text:$ data.name"></h3>
    <span data-bind="html:$ data.description"></span>
  </div>
  <div class="clearfix"></div>
</div>

在这里,我们看到我们的模板将遍历从我们的 AJAX 调用返回的每个项目。请注意,我们正在使用$component项作为标志。如果你还没有使用 Knockout 3.3 alpha 代码,或者需要使用其他东西进行部署,你应该将其改回$parent以确保在没有该增强功能的情况下也能正确工作。否则,这是标准的模板代码。

在此基础上,以下是使用 jQuery 导航标签进行刷新后的 jQuery 页面外观:

构建跨页面交互

默认情况下,背景设置为浅色,显示的标志也是浅色标志。现在,我们将跳转到 Bootstrap 页面并选择较深的样式。以下是选择该样式后的外观:

构建跨页面交互

现在我们有一个非常漂亮的程序可以展示。虽然它可能无法做到你在任何网站上看到的所有事情,但它所做到的将会给许多客户或朋友留下深刻印象。

接下来是什么?

有许多插件和方式可以包装和提升你作为 Knockout 开发者的能力。我将在knockout.developers.zone上开放一个资源,我们将分享链接和技巧,并希望在未来一年内为 Knockout 开发者举办一两次聚会。

摘要

本章汇集了每个章节的概念,并展示了模板与 Knockout 组件中的 ViewModel 协同工作的强大功能。你现在应该有一个坚实的基础,可以以前所未有的方式做更多的事情。你应该知道如何将 jQuery 代码与 Knockout 代码并排混合。

回顾本章内容,我们学习了 Knockout 组件是什么。我们学习了如何使用这些组件来创建交互性强且功能强大的自定义 HTML 元素。我们还学习了如何增强自定义元素,以便使用更常见的属性方法来管理变量。此外,我们还学习了如何使用 AMD 风格的编码方式与 Knockout 结合。同时,我们也学习了如何使用 AJAX 进行一切操作,并将 jQuery 集成到基于 Knockout 的解决方案中。

许多人努力理解 SPA 或单页应用网站的概念。我们已经能够创建一个提供理解和视角的单页应用。实际上,这可能是你下一个网站的起点。

接下来是什么?这取决于你。有一点可以肯定,使用 Knockout 的可能性比之前更广泛。祝您编码愉快,并祝贺您完成 KnockoutJS 的学习!

posted @ 2025-10-08 11:37  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报