KnockoutJS-基础知识-一-

54 阅读28分钟

KnockoutJS 基础知识(一)

原文:zh.annas-archive.org/md5/2823CCFFDCBA26955DFD8A04E5A226C2

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

当我们构建用户界面时,解决的最困难的问题之一是同步开发人员在代码中管理的数据和向用户显示的数据。开发人员采取的第一步是将演示和逻辑分开。这种分离使开发人员能够更好地分别管理两侧。但这两个层之间的通信仍然很困难。那是因为 JavaScript 被认为是一种不重要的语言,我们过去只是用它进行验证。然后 jQuery 给了我们一个线索,说明这种语言有多强大。但是数据仍然在服务器上管理,我们只是显示静态演示。这使得用户体验差和缓慢。

在过去的几年中,一种新型的架构模式出现了。它被称为 MVVM 模式。使用这种模式的库和框架使开发人员能够轻松地同步视图和数据。其中一个库就是 Knockout,使用 Knockout 的框架名为 Durandal。

Knockout 是一个快速且跨浏览器兼容的库,可以帮助我们开发具有更好用户体验的客户端应用程序。

开发人员不再需要担心数据同步的问题。Knockout 将我们的代码绑定到 HTML 元素,实时向用户显示我们代码的状态。

这种动态绑定使我们忘记了编码同步,我们可以将精力集中在编写应用程序的重要功能上。

如今,管理这些框架对前端开发人员来说是必不可少的。在本书中,你将学习 Knockout 和 Durandal 的基础知识,并且我们将深入探讨 JavaScript 的最佳设计实践和模式。

如果你想改进应用程序的用户体验并创建完全操作的前端应用程序,Knockout 和 Durandal 应该是你的选择。

本书涵盖内容

第一章,使用 KnockoutJS 自动刷新 UI,教你关于 Knockout 库。你将创建可观察对象并使你的模板对变化具有反应性。

第二章,KnockoutJS 模板,展示了如何创建模板以减少 HTML 代码。模板将帮助您保持设计的可维护性,并且它们可以根据您的数据进行调整。

第三章,自定义绑定和组件,展示了如何扩展 Knockout 库以使您的代码更易维护和可移植。

第四章, 管理 KnockoutJS 事件,教你如何使用 jQuery 事件与隔离的模块和库进行通信。事件将帮助你在不同组件或模块之间发送消息。

第五章,从服务器获取数据,展示了如何使用 jQuery AJAX 调用从客户端与服务器通信。您还将学习如何使用模拟技术在没有服务器的情况下开发客户端。

第六章,模块模式 – RequireJS,教您如何使用模块模式和 AMD 模式编写良好形式的模块以管理库之间的依赖关系。

第七章,Durandal – The KnockoutJS Framework,教您最好的 Knockout 框架是如何工作的。您将了解框架的每个部分,从而能够用更少的代码制作大型应用程序。

第八章,Durandal – The Cart Project,将本书中构建的应用程序迁移到 Durandal。你将用几行代码开发同样的应用程序,并能够添加新功能。

本书所需内容

下面是在不同阶段需要的软件应用程序列表:

  • 要开始:

    • Twitter Bootstrap 3.2.0

    • jQuery 2.2.1

    • KnockoutJS 3.2.0

  • 为了管理高级模板:

    • Knockout 外部模板引擎 2.0.5
  • 用于从浏览器执行 AJAX 调用的服务器:

    • Mongoose 服务器 5.5
  • 为了模拟数据和服务器调用:

    • Mockjax 1.6.1

    • MockJSON

  • 要验证数据:

    • Knockout 验证 2.0.0
  • 使用浏览器进行调试:

    • Chrome Knockout 调试器扩展
  • 为了管理文件依赖关系:

    • RequireJS

    • Require 文本插件

    • Knockout 和 helpers

  • KnockoutJS 框架:

    • Durandal 2.1.0 Starter Kit
  • 其他:

    • iCheck 插件 1.0.2

本书适合谁

如果您是一名 JavaScript 开发人员,一直在使用 DOM 操作库(如 jQuery、MooTools 或 Scriptaculous),并且希望在现代 JavaScript 开发中进一步使用简单、轻量级和文档完善的库,那么这项技术和本书就适合您。

学习 Knockout 将是构建响应用户交互的 JavaScript 应用程序的下一个完美步骤。

约定

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

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄如下所示:“例如,background-color 会抛出错误,因此您应该写成 'background-color'。”

代码块如下所示:

var cart = ko.observableArray([]);
var showCartDetails = function () {
  if (cart().length > 0) {
    $("#cartContainer").removeClass("hidden");
  }
};
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

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

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
<button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, enable: cart().length  > 0">
  Show Cart Details
</button>

<button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, disable: cart().length  < 1">
  Show Cart Details
</button>

任何命令行输入或输出如下所示:

# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
 /etc/asterisk/cdr_mysql.conf

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:"一旦我们点击了确认订单按钮,订单应该显示给我们以审查,并确认我们是否同意。"

注意

警告或重要提示会以这样的框中出现。

提示

小贴士和技巧会出现在这样的格式中。

第一章:自动刷新 UI,使用 KnockoutJS

如果你正在阅读这本书,那是因为你已经发现管理 web 用户界面是相当复杂的。 DOM(Document Object Model 的缩写)仅使用本地 JavaScript 进行操作是非常困难的。这是因为每个浏览器都有自己的 JavaScript 实现。为了解决这个问题,过去几年中诞生了不同的 DOM 操作库。最常用于操作 DOM 的库是 jQuery。

越来越常见的是找到帮助开发人员在客户端管理越来越多功能的库。正如我们所说,开发人员已经获得了轻松操作 DOM 的可能性,因此可以管理模板和格式化数据。此外,这些库为开发人员提供了轻松的 API 来发送和接收来自服务器的数据。

然而,DOM 操作库并不为我们提供同步输入数据与代码中模型的机制。我们需要编写代码来捕捉用户操作并更新我们的模型。

当一个问题在大多数项目中经常出现时,在几乎所有情况下,它肯定可以以类似的方式解决。然后,开始出现了管理 HTML 文件与 JavaScript 代码之间连接的库。这些库实现的模式被命名为 MV*(Model-View-Whatever)。星号可以被更改为:

  • 控制器,MVC(例如,AngularJS)

  • ViewModel,MVVM(例如,KnockoutJS)

  • Presenter(MVP)(例如,ASP.NET)

在这本书中我们要使用的库是 Knockout。它使用视图模型将数据和 HTML 进行绑定,因此它使用 MVVM 模式来管理数据绑定问题。

在本章中,你将学习这个库的基本概念,并开始在一个真实项目中使用 Knockout 的任务。

KnockoutJS 和 MVVM 模式

KnockoutJS 是一个非常轻量级的库(仅 20 KB 经过压缩),它赋予对象成为视图和模型之间的纽带的能力。这意味着你可以使用清晰的底层数据模型创建丰富的界面。

为此,它使用声明性绑定来轻松将 DOM 元素与模型数据关联起来。数据与表示层(HTML)之间的这种链接允许 DOM 自动刷新显示的值。

Knockout 建立了模型数据之间的关系链,隐式地转换和组合它。Knockout 也是非常容易扩展的。可以将自定义行为实现为新的声明性绑定。这允许程序员在几行代码中重用它们。

使用 KnockoutJS 的优点有很多:

  • 它是免费且开源的。

  • 它是使用纯 JavaScript 构建的。

  • 它可以与其他框架一起使用。

  • 它没有依赖关系。

  • 它支持所有主流浏览器,甚至包括古老的 IE 6+、Firefox 3.5+、Chrome、Opera 和 Safari(桌面/移动)。

  • 它完全有 API 文档、实时示例和交互式教程。

Knockout 的功能很明确:连接视图和模型。它不管理 DOM 或处理 AJAX 请求。为了这些目的,我建议使用 jQuery。 Knockout 给了我们自由发展我们自己想要的代码。

KnockoutJS 和 MVVM 模式

MVVM 模式图

一个真实的应用程序—koCart

为了演示如何在实际应用中使用 Knockout,我们将构建一个名为 koCart 的简单购物车。

首先,我们将定义用户故事。我们只需要几句话来知道我们想要实现的目标,如下所示:

  • 用户应该能够查看目录

  • 我们应该有能力搜索目录

  • 用户可以点击按钮将物品添加到目录中

  • 应用程序将允许我们从目录中添加、更新和删除物品

  • 用户应该能够向购物车中添加、更新和删除物品

  • 我们将允许用户更新他的个人信息。

  • 应用程序应该能够计算购物车中的总金额

  • 用户应该能够完成订单

通过用户故事,我们可以看到我们的应用程序有以下三个部分:

  • 目录,包含和管理店内所有的商品。

  • 购物车负责计算每行的价格和订单总额。

  • 订单,用户可以在其中更新他的个人信息并确认订单。

安装组件

为了开发我们的真实项目,我们需要安装一些组件并设置我们的第一个布局。

这些都是你需要下载的组件:

由于我们在前几章只在客户端工作,我们可以在客户端模拟数据,现在不需要服务器端。 所以我们可以选择我们通常用于项目的任何地方来开始我们的项目。 我建议您使用您通常用来做项目的环境。

首先,我们创建一个名为ko-cart的文件夹,然后在其中创建三个文件夹和一个文件:

  1. css文件夹中,我们将放置所有的 css。

  2. js文件夹中,我们将放置所有的 JavaScript。

  3. fonts文件夹中,我们会放置 Twitter Bootstrap 框架所需的所有字体文件。

  4. 创建一个index.html文件。

现在你应该设置你的文件,就像以下截图所示:

安装组件

初始文件夹结构

然后我们应该设置index.html文件的内容。记得使用<script><link>标签设置所有我们需要的文件的链接:

<!DOCTYPE html>
<html>
<head>
  <title>KO Shopping Cart</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
</head>
<body>
  <script type="text/javascript" src="img/jquery.min.js">
  </script>
  <script type="text/javascript" src="img/bootstrap.min.js">
  </script>
  <script type="text/javascript" src="img/knockout.debug.js">
  </script>
</body>
</html>

有了这些行代码,我们就有了开始应用程序所需的一切。

视图-模型

视图模型是 UI 上的数据和操作的纯代码表示。它不是 UI 本身。它没有任何按钮或显示样式的概念。它也不是持久化的数据模型。它保存用户正在处理的未保存数据。视图模型是纯 JavaScript 对象,不了解 HTML。以这种方式将视图模型保持抽象,让它保持简单,这样您就可以管理更复杂的行为而不会迷失。

要创建一个视图模型,我们只需要定义一个简单的 JavaScript 对象:

var vm = {};

然后要激活 Knockout,我们将调用以下行:

ko.applyBindings(vm);

第一个参数指定我们要与视图一起使用的视图模型对象。可选地,我们可以传递第二个参数来定义我们想要搜索data-bind属性的文档的哪个部分。

ko.applyBindings(vm, document.getElementById('elementID'));

这将限制激活到具有elementID及其后代的元素,这在我们想要有多个视图模型并将每个视图模型与页面的不同区域关联时非常有用。

视图

视图是表示视图模型状态的可见、交互式 UI。它显示来自视图模型的信息,向视图模型发送命令(例如,当用户点击按钮时),并在视图模型状态更改时更新。在我们的项目中,视图由 HTML 标记表示。

为了定义我们的第一个视图,我们将构建一个 HTML 来显示一个产品。将这个新内容添加到容器中:

<div class="container-fluid">
  <div class="row">
    <div class="col-md-12">
      <!-- our app goes here →
      <h1>Product</h1>
      <div>
        <strong>ID:</strong>
        <span data-bind="text:product.id"></span><br/>
        <strong>Name:</strong>
        <span data-bind="text:product.name"></span><br/>
        <strong>Price:</strong>
        <span data-bind="text:product.price"></span><br/>
        <strong>Stock:</strong>
        <span data-bind="text:product.stock"></span>
      </div> 
    </div>
  </div>
</div>

查看data-bind属性。这被称为声明性绑定。尽管这个属性对 HTML 来说并不是本机的,但它是完全正确的。但由于浏览器不知道它的含义,您需要激活 Knockout(ko.applyBindings方法)才能使其生效。

要显示来自产品的数据,我们需要在视图模型内定义一个产品:

var vm = {
  product: {
    id:1,
    name:'T-Shirt',
    price:10,
    stock: 20
  }
};
ko.applyBindings(vm);//This how knockout is activated

在脚本标签的末尾添加视图模型:

<script type="text/javascript" src="img/viewmodel.js"></script>

提示

下载示例代码

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

这将是我们应用的结果:

视图

数据绑定的结果

模型

此数据表示业务域内的对象和操作(例如,产品)和任何 UI 无关。使用 Knockout 时,您通常会调用一些服务器端代码来读取和写入此存储的模型数据。

模型和视图模型应该彼此分离。为了定义我们的产品模型,我们将按照一些步骤进行:

  1. 在我们的js文件夹内创建一个文件夹。

  2. 将其命名为models

  3. models文件夹内,创建一个名为product.js的 JavaScript 文件。

product.js文件的代码如下:

var Product = function (id,name,price,stock) {
  "use strict";
  var
    _id = id,
    _name = name,
    _price = price,
    _stock = stock
  ;

  return {
    id:_id,
    name:_name,
    price:_price,
    stock:_stock
  };
};

此函数创建一个包含产品接口的简单 JavaScript 对象。使用这种模式定义对象,称为揭示模块模式,允许我们清晰地将公共元素与私有元素分开。

要了解更多关于揭示模块模式的信息,请访问链接 carldanley.com/js-revealing-module-pattern/

将此文件与您的index.html文件链接,并将其设置在所有脚本标签的底部。

<script type="text/javascript" src="img/product.js">
</script>

现在我们可以使用产品模型定义视图模型中的产品:

var vm = {
  product: Product(1,'T-Shirt',10,20);
};
ko.applyBindings(vm);

如果我们再次运行代码,将看到相同的结果,但我们的代码现在更易读了。视图模型用于存储和处理大量信息,因此视图模型通常被视为模块,并且在其上应用了揭示模块模式。此模式允许我们清晰地公开视图模型的 API(公共元素)并隐藏私有元素。

var vm = (function(){
  var product = Product(1,'T-Shirt', 10, 20);
  return {
    product: product
  };
})();

当我们的视图模型开始增长时使用此模式可以帮助我们清晰地看到哪些元素属于对象的公共部分,哪些是私有的。

可观察对象自动刷新 UI

最后一个示例向我们展示了 Knockout 如何绑定数据和用户界面,但它没有展示自动 UI 刷新的魔法。为了执行此任务,Knockout 使用可观察对象。

可观察对象是 Knockout 的主要概念。这些是特殊的 JavaScript 对象,可以通知订阅者有关更改,并且可以自动检测依赖关系。为了兼容性,ko.observable对象实际上是函数。

要读取可观察对象的当前值,只需调用可观察对象而不带参数。在这个例子中,product.price()将返回产品的价格,product.name()将返回产品的名称。

var product = Product(1,"T-Shirt", 10.00, 20);
product.price();//returns 10.00
product.name();//returns "T-Shirt"

要将新值写入可观察对象,请调用可观察对象并将新值作为参数传递。例如,调用product.name('Jeans')将把name值更改为'Jeans'

var product = Product(1,"T-Shirt", 10.00, 20);
product.name();//returns "T-Shirt"
product.name("Jeans");//sets name to "Jeans"
product.name();//returns "Jeans"

有关可观察对象的完整文档在官方 Knockout 网站上 knockoutjs.com/documentation/observables.html

为了展示可观察对象的工作原理,我们将在模板中添加一些输入数据。

在包含产品信息的div上添加这些 HTML 标签。

<div>
  <strong>ID:</strong>
  <input class="form-control" type="text" data-bind="value:product.id"/><br/>
  <strong>Name:</strong>
  <input class="form-control" type="text" data-bind="value:product.name"><br/>
  <strong>Price:</strong>
  <input class="form-control" type="text" data-bind="value:product.price"/><br/>
  <strong>Stock:</strong>
  <input class="form-control" type="text" data-bind="value:product.stock"><br/>
</div>

我们已经使用value属性将输入与视图模型链接起来。运行代码并尝试更改输入中的值。发生了什么?什么都没有。这是因为变量不是可观察对象。更新您的product.js文件,为每个变量添加ko.observable方法:

"use strict";
function Product(id, name, price, stock) {
  "use strict";
  var
    _id = ko.observable(id),
    _name = ko.observable(name),
    _price = ko.observable(price),
    _stock = ko.observable(stock)
  ;

  return {
    id:_id,
    name:_name,
    price:_price,
    stock:_stock
  };
}

请注意,当我们更新输入中的数据时,我们的产品值会自动更新。当您将name值更改为Jeans时,文本绑定将自动更新关联的 DOM 元素的文本内容。这就是视图模型的更改如何自动传播到视图的方式。

可观察对象自动刷新 UI

可观察模型会自动更新

使用 observables 管理集合

如果你想检测并响应一个对象的变化,你会使用 observables。如果你想检测并响应一组东西的变化,请使用observableArray。这在许多情况下都很有用,比如显示或编辑多个值,并且需要在添加和删除项时重复出现和消失 UI 的部分。

要在我们的应用程序中显示一组产品,我们将按照一些简单的步骤进行:

  1. 打开index.html文件,删除<body>标签内的代码,然后添加一个表格,我们将列出我们的目录:

    <h1>Catalog</h1>
    <table class="table">
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
          <th>Stock</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td></td>
          <td></td>
          <td></td>
        </tr>
      </tbody>
    </table>
    
  2. 在视图模型内定义一个产品数组:

    "use strict";
    var vm = (function () {
    
      var catalog = [
        Product(1, "T-Shirt", 10.00, 20),
        Product(2, "Trousers", 20.00, 10),
        Product(3, "Shirt", 15.00, 20),
        Product(4, "Shorts", 5.00, 10)
      ];
    
      return {
        catalog: catalog
      };
    })();
    ko.applyBindings(vm);
    
  3. Knockout 中有一个绑定,用于在集合中的每个元素上重复执行一段代码。更新表格中的tbody元素:

    <tbody data-bind="foreach:catalog">
      <tr>
        <td data-bind="text:name"></td>
        <td data-bind="text:price"></td>
        <td data-bind="text:stock"></td>
      </tr>
    </tbody>
    

我们使用foreach属性来指出该标记内的所有内容都应该针对集合中的每个项目进行重复。在该标记内部,我们处于每个元素的上下文中,所以你可以直接绑定属性。在浏览器中观察结果。

我们想知道目录中有多少个项目,所以在表格上方添加这行代码:

<strong>Items:</strong>
<span data-bind="text:catalog.length"></span>

在集合中插入元素

要向产品数组中插入元素,应该发生一个事件。在这种情况下,用户将点击一个按钮,这个动作将触发一个操作,将一个新产品插入集合中。

在未来的章节中,你将会了解更多关于事件的内容。现在我们只需要知道有一个名为click的绑定属性。它接收一个函数作为参数,当用户点击元素时,该函数会被触发。

要插入一个元素,我们需要一个表单来插入新产品的值。将此 HTML 代码写在<h1>标签的下方:

<form class="form-horizontal" role="form" data-bind="with:newProduct">
  <div class="form-group">
    <div class="col-sm-12">
      <input type="text" class="form-control" placeholder="Name" data-bind="textInput:name">
      </div>
    </div>
    <div class="form-group">
      <div class="col-sm-12">
      <input type="password" class="form-control" placeholder="Price" data-bind="textInput:price">
      </div>
    </div>
    <div class="form-group">
      <div class="col-sm-12">
      <input type="password" class="form-control" placeholder="Stock" data-bind="textInput:stock">
      </div>
    </div>
    <div class="form-group">
      <div class="col-sm-12">
      <button type="submit" class="btn btn-default" data-bind="{click:$parent.addProduct}">
        <i class="glyphicon glyphicon-plus-sign">
        </i> Add Product
      </button>
    </div>
  </div>
</form>

在这个模板中,我们找到了一些新的绑定:

  • with 绑定:它创建一个新的绑定上下文,以便后代元素在指定对象的上下文中绑定,本例中为newProduct

    knockoutjs.com/documentation/with-binding.html

  • textInput 绑定:textInput 绑定将文本框(<input>)或文本区域(<textarea>)与视图模型属性连接起来,提供视图模型属性和元素值之间的双向更新。与value绑定属性不同,textInput 提供了对于所有类型的用户输入,包括自动完成、拖放和剪贴板事件的 DOM 的即时更新。它从 Knockout 的 3.2 版本开始提供。

    knockoutjs.com/documentation/textinput-binding.html

  • click 绑定:click 绑定添加了一个事件处理程序,使得当关联的 DOM 元素被点击时,您选择的 JavaScript 函数被调用。在调用处理程序时,Knockout 将当前模型值作为第一个参数提供。这在为集合中的每个项目渲染 UI,并且您需要知道哪个项目的 UI 被点击时特别有用。

    knockoutjs.com/documentation/click-binding.html

  • $parent 对象:这是一个绑定上下文属性。我们用它来引用foreach循环外的数据。

欲了解有关绑定上下文属性的更多信息,请阅读 Knockout 文档:knockoutjs.com/documentation/binding-context.html

在集合中插入元素

使用 with 设置上下文和 parent 通过它们导航

现在是时候向我们的视图模型添加 newProduct 对象了。首先,我们应该定义一个带有空数据的新产品:

var newProduct = Product("","","","");

我们已经定义了一个字面对象,将包含我们要放入新产品的信息。此外,我们已经定义了一个清除或重置对象的方法,一旦插入完成就会进行。现在我们定义我们的addProduct 方法:

var addProduct = function (context) {
  var id = new Date().valueOf();//random id from time
  var newProduct = Product(
    id,
    context.name(),
    context.price(),
    context.stock()
  );
  catalog.push(newProduct);
  newProduct.clear();
};

此方法创建一个从点击事件接收到的数据的新产品。

点击事件始终将上下文作为第一个参数发送。还要注意,您可以在可观察数组中使用push等数组方法。请查看 Knockout 文档 (knockoutjs.com/documentation/observableArrays.html) 以查看数组中可用的所有方法。

我们应该实现一个私有方法,一旦将新产品添加到集合中,就会清除新产品的数据:

var clearNewProduct = function () {
  newProduct.name("");
  newProduct.price("");
  newProduct.stock("");
};

更新视图模型:

return {
    catalog: catalog,
    newProduct: newProduct,
    addProduct: addProduct
};

如果您运行代码,您将注意到当您尝试添加新产品时什么也不会发生。这是因为,尽管我们的产品具有可观察属性,但我们的数组不是一个可观察的数组。因此,Knockout 不会监听更改。我们应该将数组转换为observableArray可观察的数组。

var catalog = ko.observableArray([
  Product(1, "T-Shirt", 10.00, 20),
  Product(2, "Trousers", 20.00, 10),
  Product(3, "Shirt", 15.00, 20),
  Product(4, "Shorts", 5.00, 10)
]);

现在 Knockout 正在监听该数组的变化,但不会监听每个元素内部发生的事情。Knockout 只告诉我们在数组中插入或删除元素的情况,但不告诉我们修改元素的情况。如果您想知道元素内发生了什么,那么对象应具有可观察的属性。

observableArray 只会跟踪它所持有的对象,并在添加或删除对象时通知监听者。

在幕后,observableArray 实际上是一个值为数组的可观察属性。因此,您可以像调用任何其他可观察属性一样,以无参数的方式将observableArray可观察属性作为函数进行调用,从而获取底层的 JavaScript 数组。然后您可以从那个底层数组中读取信息。

<strong>Items:</strong>
<span data-bind="text:catalog().length"></span>

计算可观察属性

想要思考一下我们在界面中显示的某些值是否取决于 Knockout 已经观察到的其他值并不奇怪。例如,如果我们想要按名称搜索我们目录中的产品,显然我们在列表中显示的目录产品与我们在搜索框中输入的术语相关联。在这些情况下,Knockout 为我们提供了计算可观察对象

您可以在 Knockout 文档中详细了解计算可观察对象

要开发搜索功能,请定义一个文本框,我们可以在其中写入要搜索的术语。我们将把它绑定到searchTerm属性。要在编写时更新值,我们应该使用textInput绑定。如果我们使用值绑定,当元素失去焦点时,值将被更新。将此代码放在产品表上方:

<div class="input-group">
  <span class="input-group-addon">
    <i class="glyphicon glyphicon-search"></i> Search</span>
  <input type="text" class="form-control" data-bind="textInput: searchTerm">
</div>

要创建一个过滤目录,我们将检查所有项目,并测试searchTerm是否在项目的name属性中。

var searchTerm = ko.observable(''); 
var filteredCatalog = ko.computed(function () {
  //if catalog is empty return empty array
  if (!catalog()) {
    return [];
  }
  var filter = searchTerm().toLowerCase();
  //if filter is empty return all the catalog
  if (!filter) {
    return catalog();
  }
  //filter data
  var filtered = ko.utils.arrayFilter(catalog(), function (item) {
    var fields = ["name"]; //we can filter several properties
    var i = fields.length;
    while (i--) {
      var prop = fields[i];
      var strProp = ko.unwrap(item[prop]).toLocaleLowerCase();
      if (strProp.indexOf(filter) !== -1){
        return true;
      };
    }
    Return false;
  });
  return filtered;
});

ko.utils对象在 Knockout 中没有文档。它是库内部使用的对象。它具有公共访问权限,并具有一些可以帮助我们处理可观察对象的函数。互联网上有很多关于它的非官方示例。

它的一个有用函数是ko.utils.arrayFilter。如果您看一下第 13 行,我们已经使用了此方法来获取过滤后的数组。

此函数以数组作为第一个参数。请注意,我们调用catalog数组可观察对象以获取元素。我们不传递可观察对象本身,而是传递可观察对象的内容。

第二个参数是决定项目是否在过滤数组中的函数。如果项目符合过滤数组的条件,它将返回true。否则返回false

在此片段的第 14 行,我们可以找到一个名为fields的数组。此参数将包含应符合条件的字段。在这种情况下,我们只检查过滤值是否在name值中。如果我们非常确定只会检查name字段,我们可以简化过滤函数:

var filtered = ko.utils.arrayFilter(catalog(), function (item) {
  var strProp = ko.unwrap(item["name"]).toLocaleLowerCase();
  return (strProp.indexOf(filter) > -1);
});

ko.unwrap函数返回包含可观察对象的值。当我们不确定变量是否包含可观察对象时,我们使用ko.unwrap,例如:

var notObservable = 'hello';
console.log(notObservable()) //this will throw an error.
console.log(ko.unwrap(notObservable)) //this will display 'hello');

将过滤后的目录暴露到公共 API 中。请注意,现在我们需要使用过滤后的目录而不是原始产品目录。因为我们正在应用揭示模块模式,我们可以保持原始 API 接口,只需使用过滤后的目录更新目录的值即可。只要我们始终保持相同的公共接口,就不需要通知视图我们将使用不同的目录或其他元素:

return {
  searchTerm: searchTerm,
  catalog: filteredCatalog,
  newProduct: newProduct,
  addProduct: addProduct
};

现在,尝试在搜索框中键入一些字符,并在浏览器中查看目录如何自动更新数据。

太棒了!我们已经完成了我们的前三个用户故事:

  • 用户应能够查看目录

  • 用户应能够搜索目录

  • 用户应能够向目录添加项目

让我们看看最终结果:

计算观察对象

总结

在本章中,你学会了 Knockout 库的基础知识。我们创建了一个简单的表单来将产品添加到我们的目录中。你还学会了如何管理 observable 集合并将其显示在表中。最后,我们使用计算观察对象开发了搜索功能。

你已经学会了三个重要的 Knockout 概念:

  • 视图模型:这包含代表视图状态的数据。它是一个纯 JavaScript 对象。

  • 模型:这包含了来自业务领域的数据。

  • 视图:这显示了我们在视图模型中存储的数据在某一时刻的情况。

为构建响应式 UI,Knockout 库为我们提供了一些重要的方法:

  • ko.observable:用于管理变量。

  • ko.observableArray:用于管理数组。

  • ko.computed:它们对其内部的 observable 的更改作出响应。

要迭代数组的元素,我们使用foreach绑定。当我们使用foreach绑定时,我们会创建一个新的上下文。这个上下文是相对于每个项目的。如果我们想要访问超出此上下文的内容,我们应该使用$parent对象。

当我们想要为变量创建一个新的上下文时,我们可以将with绑定附加到任何 DOM 元素。

我们使用click绑定将点击事件附加到元素上。点击事件函数始终将上下文作为第一个参数。

要从我们不确定是否为 observable 的变量中获取值,我们可以使用ko.unwrap函数。

我们可以使用ko.utils.arrayFilter函数来筛选集合。

在下一章中,我们将使用模板来保持我们的代码易维护和干净。模板引擎帮助我们保持代码整洁,且方便我们以简单的方式更新视图。

本章开发的代码副本在此处:

github.com/jorgeferrando/knockout-cart/archive/chapter1.zip

第二章:KnockoutJS 模板

一旦我们建立了我们的目录,就是时候给我们的应用程序添加一个购物车了。当我们的代码开始增长时,将其拆分成几个部分以保持可维护性是必要的。当我们拆分 JavaScript 代码时,我们谈论的是模块、类、函数、库等。当我们谈论 HTML 时,我们称这些部分为模板。

KnockoutJS 有一个原生模板引擎,我们可以用它来管理我们的 HTML。它非常简单,但也有一个很大的不便之处:模板应该在当前 HTML 页面中加载。如果我们的应用程序很小,这不是问题,但如果我们的应用程序开始需要越来越多的模板,这可能会成为一个问题。

在本章中,我们将使用原生引擎设计我们的模板,然后我们将讨论可以用来改进 Knockout 模板引擎的机制和外部库。

准备项目

我们可以从我们在第一章中完成的项目开始,使用 KnockoutJS 自动刷新 UI。首先,我们将为页面添加一些样式。将一个名为style.css的文件添加到css文件夹中。在index.html文件中添加一个引用,就在bootstrap引用下面。以下是文件的内容:

.container-fluid {
  margin-top: 20px;
}
.row {
  margin-bottom: 20px;
}
.cart-unit {
  width: 80px;
}
.btn-xs {
  font-size:8px;
}
.list-group-item {
  overflow: hidden;
}
.list-group-item h4 {
  float:left;
  width: 100px;
}
.list-group-item .input-group-addon {
  padding: 0;
}
.btn-group-vertical > .btn-default {
  border-color: transparent;
}
.form-control[disabled], .form-control[readonly] {
  background-color: transparent !important;
}

现在从 body 标签中删除所有内容,除了脚本标签,然后粘贴下面这些行:

<div class="container-fluid">
  <div class="row" id="catalogContainer">
    <div class="col-xs-12" data-bind="template:{name:'header'}"></div>
    <div class="col-xs-6" data-bind="template:{name:'catalog'}"></div>
    <div id="cartContainer" class="col-xs-6 well hidden" data-bind="template:{name:'cart'}"></div>
  </div>
  <div class="row hidden" id="orderContainer" data-bind="template:{name:'order'}">
  </div>
  <div data-bind="template: {name:'add-to-catalog-modal'}"></div>
  <div data-bind="template: {name:'finish-order-modal'}"></div>
</div>

让我们来审查一下这段代码。

我们有两个 row 类。它们将是我们的容器。

第一个容器的名称为catalogContainer,它将包含目录视图和购物车。第二个引用为orderContainer的容器,我们将在那里设置我们的最终订单。

我们还有两个更多的<div>标签在底部,将包含模态对话框,显示向我们的目录中添加产品的表单(我们在第一章中构建的表单),另一个将包含一个模态消息,告诉用户我们的订单已经完成。

除了这段代码,你还可以看到data-bind属性中的一个模板绑定。这是 Knockout 用来将模板绑定到元素的绑定。它包含一个name参数,表示模板的 ID。

<div class="col-xs-12" data-bind="template:{name:'header'}"></div>

在这个例子中,这个<div>元素将包含位于 ID 为header<script>标签内的 HTML。

创建模板

模板元素通常在 body 底部声明,就在具有对我们外部库引用的<script>标签上面。我们将定义一些模板,然后我们将讨论每一个模板:

<!-- templates -->
<script type="text/html" id="header"></script>
<script type="text/html" id="catalog"></script>
<script type="text/html" id="add-to-catalog-modal"></script>
<script type="text/html" id="cart-widget"></script>
<script type="text/html" id="cart-item"></script>
<script type="text/html" id="cart"></script>
<script type="text/html" id="order"></script>
<script type="text/html" id="finish-order-modal"></script>

每个模板的名称本身就足够描述性了,所以很容易知道我们将在其中设置什么。

让我们看一个图表,展示我们在屏幕上放置每个模板的位置:

创建模板

请注意,cart-item模板将针对购物车集合中的每个项目重复出现。模态模板只会在显示模态对话框时出现。最后,order模板在我们点击确认订单之前是隐藏的。

header模板中,我们将有页面的标题和菜单。catalog模板将包含我们在第一章中编写的产品表格,使用 KnockoutJS 自动刷新 UIadd-to-catalog-modal模板将包含显示向我们的目录添加产品的表单的模态框。cart-widget模板将显示我们购物车的摘要。cart-item模板将包含购物车中每个项目的模板。cart模板将具有购物车的布局。order模板将显示我们想购买的最终产品列表和确认订单的按钮。

头部模板

让我们从应该包含header模板的 HTML 标记开始:

<script type="text/html" id="header">
  <h1>
    Catalog
  </h1>

  <button class="btn btn-primary btn-sm" data-toggle="modal" data-target="#addToCatalogModal">
    Add New Product
  </button>
  <button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, css:{ disabled: cart().length  < 1}">
    Show Cart Details
  </button>
  <hr/>
</script>

我们定义了一个<h1>标签和两个<button>标签。

第一个按钮标签附加到具有 ID#addToCatalogModal的模态框。由于我们使用的是 Bootstrap 作为 CSS 框架,我们可以使用data-target属性按 ID 附加模态,并使用data-toggle属性激活模态。

第二个按钮将显示完整的购物车视图,只有在购物车有商品时才可用。为了实现这一点,有许多不同的方法。

第一个方法是使用 Twitter Bootstrap 提供的 CSS-disabled 类。这是我们在示例中使用的方式。CSS 绑定允许我们根据附加到类的表达式的结果来激活或停用元素中的类。

另一种方法是使用enable绑定。如果表达式评估为true,此绑定将启用元素。我们可以使用相反的绑定,称为disable。Knockout 网站上有完整的文档knockoutjs.com/documentation/enable-binding.html

<button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, enable: cart().length  > 0"> 
  Show Cart Details
</button>

<button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, disable: cart().length  < 1"> 
  Show Cart Details
</button>

第一种方法使用 CSS 类来启用和禁用按钮。第二种方法使用 HTML 属性disabled

我们可以使用第三个选项,即使用计算可观察值。我们可以在视图模型中创建一个计算可观察变量,根据购物车的长度返回truefalse

//in the viewmodel. Remember to expose it
var cartHasProducts = ko.computed(function(){
  return (cart().length > 0);
});
//HTML
<button class="btn btn-primary btn-sm" data-bind="click: showCartDetails, enable: cartHasProducts"> 
  Show Cart Details
</button>

要显示购物车,我们将以与上一章中相同的方式使用click绑定。

现在我们应该转到我们的viewmodel.js文件,并添加所有我们需要使此模板工作的信息:

var cart = ko.observableArray([]);
var showCartDetails = function () {
  if (cart().length > 0) {
    $("#cartContainer").removeClass("hidden");
  }
};

并且你应该在视图模型中公开这两个对象:

  return {
  //first chapter
    searchTerm: searchTerm,
    catalog: filteredCatalog,
    newProduct: newProduct,
    totalItems:totalItems,
    addProduct: addProduct,
  //second chapter
    cart: cart,
    showCartDetails: showCartDetails,
  };

目录模板

下一步是在header模板下方定义catalog模板:

<script type="text/html" id="catalog">
  <div class="input-group">
    <span class="input-group-addon">
      <i class="glyphicon glyphicon-search"></i> Search
    </span>
    <input type="text" class="form-control" data-bind="textInput: searchTerm">
  </div>
  <table class="table">
    <thead>
    <tr>
      <th>Name</th>
      <th>Price</th>
      <th>Stock</th>
      <th></th>
    </tr>
    </thead>
    <tbody data-bind="foreach:catalog">
    <tr data-bind="style:color:stock() < 5?'red':'black'">
      <td data-bind="text:name"></td>
      <td data-bind="text:price"></td>
      <td data-bind="text:stock"></td>
      <td>
        <button class="btn btn-primary" data-bind="click:$parent.addToCart">
          <i class="glyphicon glyphicon-plus-sign"></i> Add
        </button>
      </td>
    </tr>
    </tbody>
    <tfoot>
    <tr>
      <td colspan="3">
        <strong>Items:</strong><span data-bind="text:catalog().length"></span>
      </td>
      <td colspan="1">
        <span data-bind="template:{name:'cart-widget'}"></span>
      </td>
    </tr>
    </tfoot>
  </table>
</script>

这是我们在上一章中构建的相同表格。我们只是添加了一些新东西:

<tr data-bind="style:{color: stock() < 5?'red':'black'}">...</tr>

现在,每行使用 style 绑定来提醒用户,当他们购物时,库存达到最大限制。style 绑定与 CSS 绑定类似。它允许我们根据表达式的值添加样式属性。在这种情况下,如果库存高于五,行中的文本颜色必须是黑色,如果库存是四或更少,则为红色。我们可以使用其他 CSS 属性,所以随时尝试其他行为。例如,如果元素在购物车内部,将目录的行设置为绿色。我们应记住,如果属性有连字符,你应该用单引号括起来。例如,background-color 会抛出错误,所以你应该写成 'background-color'

当我们使用根据视图模型的值激活的绑定时,最好使用计算观察值。因此,我们可以在我们的产品模型中创建一个计算值,该值返回应显示的颜色值:

//In the Product.js
var _lineColor = ko.computed(function(){
  return (_stock() < 5)? 'red' : 'black';
});
return {
  lineColor:_lineColor
};
//In the template
<tr data-bind="style:lineColor"> ... </tr>

如果我们在 style.css 文件中创建一个名为 stock-alert 的类,并使用 CSS 绑定,效果会更好。

//In the style file
.stock-alert {
  color: #f00;
}
//In the Product.js
var _hasStock = ko.computed(function(){
  return (_stock() < 5);   
});
return {
  hasStock: _hasStock
};
//In the template
<tr data-bind="css: hasStock"> ... </tr>

现在,看一下 <tfoot> 标签内部。

<td colspan="1">
  <span data-bind="template:{name:'cart-widget'}"></span>
</td>

正如你所见,我们可以有嵌套模板。在这种情况下,我们在 catalog 模板内部有一个 cart-widget 模板。这使我们可以拥有非常复杂的模板,将它们分割成非常小的片段,并组合它们,以保持我们的代码整洁和可维护性。

最后,看一下每行的最后一个单元格:

<td>
  <button class="btn btn-primary" data-bind="click:$parent.addToCart">
    <i class="glyphicon glyphicon-plus-sign"></i> Add
  </button>
</td>

看看我们如何使用魔术变量 $parent 调用 addToCart 方法。Knockout 给了我们一些魔术词来浏览我们应用程序中的不同上下文。在这种情况下,我们在 catalog 上下文中,想要调用一个位于一级上的方法。我们可以使用名为 $parent 的魔术变量。

在 Knockout 上下文中,还有其他变量可供使用。Knockout 网站上有完整的文档 knockoutjs.com/documentation/binding-context.html

在这个项目中,我们不会使用所有这些绑定上下文变量。但我们会快速解释这些绑定上下文变量,只是为了更好地理解它们。

如果我们不知道我们有多少级别深入,我们可以使用魔术词 $root 导航到视图模型的顶部。

当我们有许多父级时,我们可以获得魔术数组 $parents 并使用索引访问每个父级,例如 $parents[0]$parents[1]。想象一下,你有一个类别列表,每个类别包含一个产品列表。这些产品是一个 ID 列表,而类别有一个获取其产品名称的方法。我们可以使用 $parents 数组来获取对类别的引用:

<ul data-bind="foreach: {data: categories}">
  <li data-bind="text: $data.name"></li>
  <ul data-bind="foreach: {data: $data.products, as: 'prod'}>
    <li data-bind="text: $parents[0].getProductName(prod.ID)"></li>
  </ul>
</ul>

看看foreach绑定内部的as属性有多有用。它使代码更易读。但是,如果你在foreach循环内部,你也可以使用$data魔术变量访问每个项目,并且可以使用$index魔术变量访问集合中每个元素的位置索引。例如,如果我们有一个产品列表,我们可以这样做:

<ul data-bind="foreach: cart">
  <li><span data-bind="text:$index">
    </span> - <span data-bind="text:$data.name"></span>
</ul>

这应该显示:

0 – 产品 1

1 – 产品 2

2 – 产品 3

...

目录模板

KnockoutJS 魔术变量用于导航上下文

现在我们更多地了解了绑定变量是什么,让我们回到我们的代码。我们现在将编写addToCart方法。

我们将在我们的js/models文件夹中定义购物车项目。创建一个名为CartProduct.js的文件,并插入以下代码:

//js/models/CartProduct.js
var CartProduct = function (product, units) {
  "use strict";

  var _product = product,
    _units = ko.observable(units);

  var subtotal = ko.computed(function(){
    return _product.price() * _units();
  });

  var addUnit = function () {
    var u = _units();
    var _stock = _product.stock();
    if (_stock === 0) {
      return;
    }
  _units(u+1);
    _product.stock(--_stock);
  };

  var removeUnit = function () {
    var u = _units();
    var _stock = _product.stock();
    if (u === 0) {
      return;
    }
    _units(u-1);
    _product.stock(++_stock);
  };

  return {
    product: _product,
    units: _units,
    subtotal: subtotal,
    addUnit : addUnit,
    removeUnit: removeUnit,
  };
};

每个购物车产品由产品本身和我们想购买的产品的单位组成。我们还将有一个计算字段,其中包含该行的小计。我们应该让对象负责管理其单位和产品的库存。因此,我们已经添加了addUnitremoveUnit方法。如果调用了这些方法,它们将增加一个产品单位或删除一个产品单位。

我们应该在我们的index.html文件中与其他<script>标签一起引用这个 JavaScript 文件。

在视图模型中,我们应该创建一个购物车数组,并在返回语句中公开它,就像我们之前做的那样:

var cart = ko.observableArray([]);

是时候编写addToCart方法了:

var addToCart = function(data) {
  var item = null;
  var tmpCart = cart();
  var n = tmpCart.length;
  while(n--) {
    if (tmpCart[n].product.id() === data.id()) {
      item = tmpCart[n];
    }
  }
  if (item) {
    item.addUnit();
  } else {
    item = new CartProduct(data,0);
    item.addUnit();
    tmpCart.push(item);        
  }
  cart(tmpCart);
};

此方法在购物车中搜索产品。如果存在,则更新其单位,如果不存在,则创建一个新的。由于购物车是一个可观察数组,我们需要获取它,操作它,并覆盖它,因为我们需要访问产品对象以了解产品是否在购物车中。请记住,可观察数组不会观察它们包含的对象,只会观察数组属性。

添加到购物车模态框模板

这是一个非常简单的模板。我们只需将我们在第一章中创建的代码包装在一起,使用 KnockoutJS 自动刷新 UI,以将产品添加到 Bootstrap 模态框中:

<script type="text/html" id="add-to-catalog-modal">
  <div class="modal fade" id="addToCatalogModal">
    <div class="modal-dialog">
      <div class="modal-content">
        <form class="form-horizontal" role="form" data-bind="with:newProduct">
          <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal">
              <span aria-hidden="true">&times;</span>
              <span class="sr-only">Close</span>
            </button><h3>Add New Product to the Catalog</h3>
          </div>
          <div class="modal-body">
            <div class="form-group">
              <div class="col-sm-12">
                <input type="text" class="form-control" placeholder="Name" data-bind="textInput:name">
              </div>
            </div>
            <div class="form-group">
              <div class="col-sm-12">
                <input type="text" class="form-control" placeholder="Price" data-bind="textInput:price">
              </div>
            </div>
            <div class="form-group">
              <div class="col-sm-12">
                <input type="text" class="form-control" placeholder="Stock" data-bind="textInput:stock">
              </div>
            </div>
          </div>
          <div class="modal-footer">
            <div class="form-group">
              <div class="col-sm-12">
                <button type="submit" class="btn btn-default" data-bind="{click:$parent.addProduct}">
                  <i class="glyphicon glyphicon-plus-sign">
                  </i> Add Product
                </button>
              </div>
            </div>
          </div>
        </form>
      </div><!-- /.modal-content -->
    </div><!-- /.modal-dialog -->
  </div><!-- /.modal -->
</script>

购物车小部件模板

此模板可以快速向用户提供有关购物车中有多少件商品以及它们的总成本的信息:

<script type="text/html" id="cart-widget">
  Total Items: <span data-bind="text:totalItems"></span>
  Price: <span data-bind="text:grandTotal"></span>
</script>

我们应该在我们的视图模型中定义totalItemsgrandTotal

var totalItems = ko.computed(function(){
  var tmpCart = cart();
  var total = 0;
  tmpCart.forEach(function(item){
    total += parseInt(item.units(),10);
  });
  return total;
});
var grandTotal = ko.computed(function(){
  var tmpCart = cart();
  var total = 0;
  tmpCart.forEach(function(item){
    total += (item.units() * item.product.price());
  });
  return total;
});

现在你应该像我们一直做的那样在返回语句中公开它们。现在不要担心格式,你将在未来学习如何格式化货币或任何类型的数据。现在你必须专注于学习如何管理信息以及如何向用户显示信息。

购物车项目模板

cart-item模板显示购物车中的每一行:

<script type="text/html" id="cart-item">
  <div class="list-group-item" style="overflow: hidden">
    <button type="button" class="close pull-right" data-bind="click:$root.removeFromCart"><span>&times;</span></button>
    <h4 class="" data-bind="text:product.name"></h4>
    <div class="input-group cart-unit">
      <input type="text" class="form-control" data-bind="textInput:units" readonly/>
        <span class="input-group-addon">
          <div class="btn-group-vertical">
            <button class="btn btn-default btn-xs" data-bind="click:addUnit">
              <i class="glyphicon glyphicon-chevron-up"></i>
            </button>
            <button class="btn btn-default btn-xs" data-bind="click:removeUnit">
              <i class="glyphicon glyphicon-chevron-down"></i>
            </button>
          </div>
        </span>
    </div>
  </div>
</script>

我们在每条线的右上角设置了一个x按钮,方便从购物车中移除一条线。正如您所见,我们使用了$root魔术变量来导航到顶级上下文,因为我们将在foreach循环中使用此模板,这意味着该模板将处于循环上下文中。如果我们把这个模板视为一个独立的元素,我们无法确定我们在上下文导航中有多深。为了确保,我们要到正确的上下文中调用removeFormCart方法。在这种情况下最好使用$root而不是$parent

removeFromCart的代码应该在 view-model 上下文中,代码应该如下所示:

var removeFromCart = function (data) {
  var units = data.units();
  var stock = data.product.stock();
  data.product.stock(units+stock);
  cart.remove(data);
};

注意,在addToCart方法中,我们获得了 observable 内部的数组。我们这样做是因为我们需要导航到数组的元素内部。在这种情况下,Knockout 可观察数组有一个叫做remove的方法,允许我们移除作为参数传递的对象。如果对象在数组中,则会被移除。

记住,数据环境始终作为我们在单击事件中使用的函数的第一个参数传递。

购物车模板

cart模板应显示购物车的布局:

<script type="text/html" id="cart">
  <button type="button" class="close pull-right" data-bind="click:hideCartDetails">
    <span>&times;</span>
  </button>
  <h1>Cart</h1>
  <div data-bind="template: {name: 'cart-item', foreach:cart}" class="list-group"></div>
  <div data-bind="template:{name:'cart-widget'}"></div>
  <button class="btn btn-primary btn-sm" data-bind="click:showOrder">
    Confirm Order
  </button>
</script>

重要的是,您注意到我们**

购物车

**下面正好绑定了模板。我们使用foreach参数将模板与数组绑定。通过这种绑定,Knockout 会为购物车中的每个元素渲染cart-item模板。这样可以大大减少我们在每个模板中编写的代码,而且使它们更易读。

我们再次使用cart-widget模板显示总商品数量和总金额。这是模板的一个很好的特点,我们可以反复使用内容。

请注意,我们在购物车的右上方放置了一个按钮,当我们不需要查看购物车的详细信息时,可以关闭购物车,并且另一个按钮是在完成时确认订单。我们的 view-model 中的代码应该如下:

var hideCartDetails = function () {
  $("#cartContainer").addClass("hidden");
};
var showOrder = function () {
  $("#catalogContainer").addClass("hidden");
  $("#orderContainer").removeClass("hidden");
};

正如您所见,我们使用 jQuery 和 Bootstrap 框架的 CSS 类来显示和隐藏元素。隐藏类只是给元素添加了display: none样式。我们只需要切换这个类来在视图中显示或隐藏元素。将这两个方法暴露在您的 view-model 的return语句中。

当需要显示order模板时我们将回来。

这就是我们有了我们的目录和购物车后的结果:

购物车模板

订单模板

一旦我们单击确认订单按钮,订单应该显示给我们,以便审查和确认我们是否同意。

<script type="text/html" id="order">
  <div class="col-xs-12">
    <button class="btn btn-sm btn-primary" data-bind="click:showCatalog">
      Back to catalog
    </button>
    <button class="btn btn-sm btn-primary" data-bind="click:finishOrder">
      Buy & finish
    </button>
  </div>
  <div class="col-xs-6">
    <table class="table">
      <thead>
      <tr>
        <th>Name</th>
        <th>Price</th>
        <th>Units</th>
        <th>Subtotal</th>
      </tr>
      </thead>
      <tbody data-bind="foreach:cart">
      <tr>
        <td data-bind="text:product.name"></td>
        <td data-bind="text:product.price"></td>
        <td data-bind="text:units"></td>
        <td data-bind="text:subtotal"></td>
      </tr>
      </tbody>
      <tfoot>
      <tr>
        <td colspan="3"></td>
        <td>Total:<span data-bind="text:grandTotal"></span></td>
      </tr>
      </tfoot>
    </table>
  </div>
</script>

这里有一个只读表格,显示所有购物车条目和两个按钮。其中一个是确认按钮,将显示模态对话框,显示订单完成,另一个让我们有选择返回目录继续购物。有些代码需要添加到我们的 view-model 中并向用户公开:

var showCatalog = function () {
  $("#catalogContainer").removeClass("hidden");
  $("#orderContainer").addClass("hidden");
};
var finishOrder = function() {
  cart([]);
  hideCartDetails();
  showCatalog();
  $("#finishOrderModal").modal('show');
};

正如我们在先前的方法中所做的,我们给想要显示和隐藏的元素添加和删除隐藏类。finishOrder方法移除购物车中的所有商品,因为我们的订单已完成;隐藏购物车并显示目录。它还显示一个模态框,向用户确认订单已完成。

订单模板

订单详情模板

finish-order-modal模板

最后一个模板是告诉用户订单已完成的模态框:

<script type="text/html" id="finish-order-modal">
  <div class="modal fade" id="finishOrderModal">
    <div class="modal-dialog">
            <div class="modal-content">
        <div class="modal-body">
        <h2>Your order has been completed!</h2>
        </div>
        <div class="modal-footer">
          <div class="form-group">
            <div class="col-sm-12">
              <button type="submit" class="btn btn-success" data-dismiss="modal">Continue Shopping
              </button>
            </div>
          </div>
        </div>
      </div><!-- /.modal-content -->
    </div><!-- /.modal-dialog -->
  </div><!-- /.modal -->
</script>

以下截图显示了输出:

完成订单模板

用 if 和 ifnot 绑定处理模板

你已经学会如何使用 jQuery 和 Bootstrap 的强大功能来显示和隐藏模板。这非常好,因为你可以在任何你想要的框架中使用这个技术。这种类型的代码的问题在于,由于 jQuery 是一个 DOM 操作库,你需要引用要操作的元素。这意味着你需要知道想要应用操作的元素。Knockout 给我们一些绑定来根据我们视图模型的值来隐藏和显示元素。让我们更新showhide方法以及模板。

将两个控制变量添加到你的视图模型中,并在return语句中公开它们。

var visibleCatalog = ko.observable(true);
var visibleCart = ko.observable(false);

现在更新showhide方法:

var showCartDetails = function () {
  if (cart().length > 0) {
    visibleCart(true);
  }
};

var hideCartDetails = function () {
  visibleCart(false);
};

var showOrder = function () {
  visibleCatalog(false);
};

var showCatalog = function () {
  visibleCatalog(true);
};

我们可以欣赏到代码变得更易读和有意义。现在,更新cart模板、catalog模板和order模板。

index.html中,考虑这一行:

<div class="row" id="catalogContainer">

用以下行替换它:

<div class="row" data-bind="if: visibleCatalog">

然后考虑以下行:

<div id="cartContainer" class="col-xs-6 well hidden" data-bind="template:{name:'cart'}"></div>

用这个来替换它:

<div class="col-xs-6" data-bind="if: visibleCart">
  <div class="well" data-bind="template:{name:'cart'}"></div>
</div>

重要的是要知道,if 绑定和模板绑定不能共享相同的data-bind属性。这就是为什么在这个模板中我们从一个元素转向两个嵌套元素。换句话说,这个例子是不允许的:

<div class="col-xs-6" data-bind="if:visibleCart, template:{name:'cart'}"></div>

最后,考虑这一行:

<div class="row hidden" id="orderContainer" data-bind="template:{name:'order'}">

用这个来替换它:

<div class="row" data-bind="ifnot: visibleCatalog">
  <div data-bind="template:{name:'order'}"></div>
</div>

通过我们所做的更改,显示或隐藏元素现在取决于我们的数据而不是我们的 CSS。这样做要好得多,因为现在我们可以使用ififnot绑定来显示和隐藏任何我们想要的元素。

让我们粗略地回顾一下我们现在的文件:

我们有我们的index.html文件,其中包含主容器、模板和库:

<!DOCTYPE html>
<html>
<head>
  <title>KO Shopping Cart</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
  <link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>

<div class="container-fluid">
  <div class="row" data-bind="if: visibleCatalog">
    <div class="col-xs-12" data-bind="template:{name:'header'}"></div>
    <div class="col-xs-6" data-bind="template:{name:'catalog'}"></div>
    <div class="col-xs-6" data-bind="if: visibleCart">
      <div class="well" data-bind="template:{name:'cart'}"></div>
    </div>
  </div>
  <div class="row" data-bind="ifnot: visibleCatalog">
    <div data-bind="template:{name:'order'}"></div>
  </div>
  <div data-bind="template: {name:'add-to-catalog-modal'}"></div>
  <div data-bind="template: {name:'finish-order-modal'}"></div>
</div>

<!-- templates -->
<script type="text/html" id="header"> ... </script>
<script type="text/html" id="catalog"> ... </script>
<script type="text/html" id="add-to-catalog-modal"> ... </script>
<script type="text/html" id="cart-widget"> ... </script>
<script type="text/html" id="cart-item"> ... </script>
<script type="text/html" id="cart"> ... </script>
<script type="text/html" id="order"> ... </script>
<script type="text/html" id="finish-order-modal"> ... </script>
<!-- libraries -->
<script type="text/javascript" src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/bootstrap.min.js"></script>
<script type="text/javascript" src="img/knockout.debug.js"></script>
<script type="text/javascript" src="img/product.js"></script>
<script type="text/javascript" src="img/cartProduct.js"></script>
<script type="text/javascript" src="img/viewmodel.js"></script>
</body>
</html>

我们还有我们的viewmodel.js文件:

var vm = (function () {
  "use strict";
  var visibleCatalog = ko.observable(true);
  var visibleCart = ko.observable(false);
  var catalog = ko.observableArray([...]);
  var cart = ko.observableArray([]);
  var newProduct = {...};
  var totalItems = ko.computed(function(){...});
  var grandTotal = ko.computed(function(){...});
  var searchTerm = ko.observable("");
  var filteredCatalog = ko.computed(function () {...});
  var addProduct = function (data) {...};
  var addToCart = function(data) {...};
  var removeFromCart = function (data) {...};
  var showCartDetails = function () {...};
  var hideCartDetails = function () {...};
  var showOrder = function () {...};
  var showCatalog = function () {...};
  var finishOrder = function() {...};
  return {
    searchTerm: searchTerm,
    catalog: filteredCatalog,
    cart: cart,
    newProduct: newProduct,
    totalItems:totalItems,
    grandTotal:grandTotal,
    addProduct: addProduct,
    addToCart: addToCart,
    removeFromCart:removeFromCart,
    visibleCatalog: visibleCatalog,
    visibleCart: visibleCart,
    showCartDetails: showCartDetails,
    hideCartDetails: hideCartDetails,
    showOrder: showOrder,
    showCatalog: showCatalog,
    finishOrder: finishOrder
  };
})();
ko.applyBindings(vm);

在调试时将视图模型全局化是很有用的。在生产环境中这样做并不是好的实践,但在调试应用程序时是很好的。

Window.vm = vm;

现在你可以从浏览器调试器或 IDE 调试器轻松访问你的视图模型。

除了在第一章中编写的产品模型之外,我们还创建了一个名为CartProduct的新模型:

var CartProduct = function (product, units) {
  "use strict";
  var _product = product,
    _units = ko.observable(units);
  var subtotal = ko.computed(function(){...});
  var addUnit = function () {...};
  var removeUnit = function () {...};
  return {
    product: _product,
    units: _units,
    subtotal: subtotal,
    addUnit : addUnit,
    removeUnit: removeUnit
  };
};

你已经学会了如何使用 Knockout 管理模板,但也许你已经注意到,在index.html文件中拥有所有模板并不是最佳的方法。我们将讨论两种机制。第一种更像是自制的,而第二种是许多 Knockout 开发者使用的外部库,由 Jim Cowart 创建,名为Knockout.js-External-Template-Enginegithub.com/ifandelse/Knockout.js-External-Template-Engine)。

使用 jQuery 管理模板

由于我们希望从不同的文件加载模板,让我们将所有的模板移到一个名为views的文件夹中,并且每个模板都用一个文件表示。每个文件的名称将与模板的 ID 相同。因此,如果模板的 ID 是cart-item,那么文件应该被称为cart-item.html,并且将包含完整的cart-item模板:

<script type="text/html" id="cart-item"></script>

使用 jQuery 管理模板

包含所有模板的 views 文件夹

现在在viewmodel.js文件中,删除最后一行(ko.applyBindings(vm))并添加此代码:

var templates = [
  'header',
  'catalog',
  'cart',
  'cart-item',
  'cart-widget',
  'order',
  'add-to-catalog-modal',
  'finish-order-modal'
];

var busy = templates.length;
templates.forEach(function(tpl){
  "use strict";
  $.get('views/'+ tpl + '.html').then(function(data){
    $('body').append(data);
    busy--;
    if (!busy) {
      ko.applyBindings(vm);
    }
  });
});

此代码获取我们需要的所有模板并将它们附加到 body。一旦所有模板都加载完成,我们就调用applyBindings方法。我们应该这样做,因为我们是异步加载模板,我们需要确保当所有模板加载完成时绑定我们的视图模型。

这样做已足以使我们的代码更易维护和易读,但如果我们需要处理大量的模板,仍然存在问题。而且,如果我们有嵌套文件夹,列出所有模板就会变成一个头疼的事情。应该有更好的方法。

使用koExternalTemplateEngine管理模板

我们已经看到了两种加载模板的方式,它们都足以管理少量的模板,但当代码行数开始增长时,我们需要一些允许我们忘记模板管理的东西。我们只想调用一个模板并获取内容。

为此目的,Jim Cowart 的库koExternalTemplateEngine非常完美。这个项目在 2014 年被作者放弃,但它仍然是一个我们在开发简单项目时可以使用的好库。在接下来的章节中,您将学习更多关于异步加载和模块模式的知识,我们将看到其他目前正在维护的库。

我们只需要在js/vendors文件夹中下载库,然后在我们的index.html文件中链接它,放在 Knockout 库的下面即可。

<script type="text/javascript" src="img/knockout.debug.js"></script>
<script type="text/javascript" src="img/koExternalTemplateEngine_all.min.js"></script>

现在你应该在viewmodel.js文件中进行配置。删除模板数组和foreach语句,并添加以下三行代码:

infuser.defaults.templateSuffix = ".html";
infuser.defaults.templateUrl = "views";
ko.applyBindings(vm);

这里,infuser是一个我们用来配置模板引擎的全局变量。我们应该指示我们的模板将具有哪个后缀名,以及它们将在哪个文件夹中。

我们不再需要<script type="text/html" id="template-id"></script>标签,所以我们应该从每个文件中删除它们。

现在一切应该都正常了,我们成功所需的代码并不多。

KnockoutJS 有自己的模板引擎,但是您可以看到添加新的引擎并不困难。如果您有其他模板引擎的经验,如 jQuery Templates、Underscore 或 Handlebars,只需将它们加载到您的index.html文件中并使用它们,没有任何问题。这就是 Knockout 的美丽之处,您可以使用任何您喜欢的工具。

你在本章学到了很多东西,对吧?

  • Knockout 给了我们 CSS 绑定,根据表达式激活和停用 CSS 类。

  • 我们可以使用 style 绑定向元素添加 CSS 规则。

  • 模板绑定帮助我们管理已在 DOM 中加载的模板。

  • 使用foreach绑定可以在集合上进行迭代。

  • foreach内部,Knockout 给了我们一些魔术变量,如$parent$parents$index$data$root

  • 我们可以在foreach绑定中使用as绑定来为每个元素获取别名。

  • 我们可以只使用 jQuery 和 CSS 来显示和隐藏内容。

  • 我们可以使用ififnotvisible绑定来显示和隐藏内容。

  • jQuery 帮助我们异步加载 Knockout 模板。

  • 您可以使用koExternalTemplateEngine插件以更有效的方式管理模板。这个项目已经被放弃了,但它仍然是一个很好的解决方案。

摘要

在本章中,您已经学会了如何使用共享相同视图模型的模板来拆分应用程序。现在我们知道了基础知识,扩展应用程序会很有趣。也许我们可以尝试创建产品的详细视图,或者给用户选择订单发送位置的选项。您将在接下来的章节中学习如何做这些事情,但是只使用我们现在拥有的知识进行实验会很有趣。

在下一章中,我们将学习如何扩展 Knockout 行为。这将有助于格式化数据并创建可重用的代码。您将学习自定义绑定和组件是什么,以及它们如何帮助我们编写可重用和优雅的代码。

本章的代码在 GitHub 上:

github.com/jorgeferrando/knockout-cart/archive/chapter2.zip

第三章:自定义绑定和组件

通过前两章学到的所有概念,你可以构建出大部分真实世界中遇到的应用程序。当然,如果只凭借这两章的知识编写代码,你应该非常整洁,因为你的代码会变得越来越庞大,维护起来会很困难。

有一次一个谷歌工程师被问及如何构建大型应用程序。他的回答既简短又雄辩:。不要编写大型应用程序。相反,编写小型应用程序,小型的隔离代码片段互相交互,并用它们构建一个大系统。

我们如何编写小型、可重用和独立的代码片段来扩展 Knockout 的功能?答案是使用自定义绑定和组件。

自定义绑定

我们知道什么是绑定,它是我们写在data-bind属性中的一切。我们有一些内置的绑定。点击和值是其中的两个。但我们可以编写我们自己的自定义绑定,以整洁的方式扩展我们应用程序的功能。

编写自定义绑定非常简单。它有一个基本结构,我们应该始终遵循:

ko.bindingHandlers.yourBindingName = {
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    // This will be called when the binding is first applied to an element
    // Set up any initial state, event handlers, etc. here
  },
  update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    // This will be called once when the binding is first applied to an element,
    // and again whenever any observables/computeds that are accessed change
    // Update the DOM element based on the supplied values here.
  }
};

Knockout 有一个内部对象叫做bindingHandlers。我们可以用自定义绑定扩展这个对象。我们的绑定应该有一个名称,在bindingHandlers对象内用来引用它。我们的自定义绑定是一个具有两个函数initupdate的对象。有时你应该只使用其中一个,有时两个都要用。

init方法中,我们应该初始化绑定的状态。在update方法中,我们应该设置代码以在其模型或值更新时更新绑定。这些方法给了我们一些参数来执行这个任务:

  • element:这是与绑定有关的 DOM 元素。

  • valueAccessor:这是绑定的值。通常是一个函数或可观察对象。使用ko.unwrap来获取值更安全,比如var value = ko.unwrap(valueAccessor());

  • allBindings:这是一个对象,你可以用它来访问其他绑定。你可以使用allBindings.get('name')来获取一个绑定,或使用allBindings.has('name')来查询绑定是否存在。

  • viewModel:在 Knockout 3.x 中已弃用。你应该使用bindingContext.$databindigContext.$rawData代替。

  • bindingContext:使用绑定上下文,我们可以访问熟悉的上下文对象,如$root$parents$parent$data$index来在不同的上下文中导航。

我们可以为许多事物使用自定义绑定。例如,我们可以自动格式化数据(货币或日期是明显的例子),或增加其他绑定的语义含义。给绑定起名叫toggle比仅仅设置clickvisible绑定来显示和隐藏元素更加描述性。

自定义绑定

新的文件夹结构与自定义绑定和组件

这个 toggle 绑定

要向我们的应用程序添加新的自定义绑定,我们将创建一个名为custom的新文件夹,放在我们的js文件夹中。然后,我们将创建一个名为koBindings.js的文件,并将其链接到我们的index.html文件中,放在我们的模板引擎的下方:

<script type="text/javascript" src="img/koExternalTemplateEngine_all.min.js"></script>
<script type="text/javascript" src="img/koBindings.js"></script>

我们的第一个自定义绑定将被称为toggle。我们将使用此自定义绑定来更改布尔变量的值。通过这种行为,我们可以显示和隐藏元素,即我们的购物车。只需在koBindings.js文件的开头编写以下代码。

ko.bindingHandlers.toggle = {
  init: function (element, valueAccessor) {
    var value = valueAccessor();
    ko.applyBindingsToNode(element, {
      click: function () {
          value(!value());
      }
    });
  }
};

在这种情况下,我们不需要使用update方法,因为我们在初始化绑定时设置了所有行为。我们使用ko.applyBingidsToNode方法将click函数链接到元素上。applyBindingsToNode方法具有与applyBindings相同的行为,但我们设置了一个上下文,一个从 DOM 中获取的节点,其中应用了绑定。我们可以说applyBindingsapplyBindingsToNode($('body'), viewmodel)的别名。

现在我们可以在我们的应用程序中使用这个绑定。更新views/header.html模板中的showCartDetails按钮。删除以下代码:

<button class="btn btn-primary btn-sm" data-bind="click:showCartDetails, css:{disabled:cart().length  < 1}">Show Cart Details
</button>

更新以下按钮的代码:

<button class="btn btn-primary btn-sm" data-bind="toggle:visibleCart, css:{disabled:cart().length  < 1}">
  <span data-bind="text: visibleCart()?'Hide':'Show'">
  </span> Cart Details
</button>

现在我们不再需要showCartDetailshideCartDetails方法了,我们可以直接使用toggle绑定攻击visibleCart变量。

通过这个简单的绑定,我们已经删除了代码中的两个方法,并创建了一个可重用的代码,不依赖于我们的购物车视图模型。因此,您可以在任何想要的项目中重用 toggle 绑定,因为它没有任何外部依赖项。

我们还应该更新cart.html模板:

<button type="button" class="close pull-right" data-bind="toggle:visibleCart"><span>&times;</span></button>

一旦我们进行了此更新,我们意识到不再需要使用hideCartDetails。要彻底删除它,请按照以下步骤操作:

  1. finishOrder函数中,删除以下行:

    hideCartDetails();
    
  2. 添加以下行:

    visibleCart(false);
    

没有必要保留只管理一行代码的函数。

货币绑定

自定义绑定提供的另一个有用的工具是格式化应用于节点数据的选项。例如,我们可以格式化购物车的货币字段。

在 toggle 绑定的下方添加以下绑定:

ko.bindingHandlers.currency = {
  symbol: ko.observable('$'),
  update: function(element, valueAccessor, allBindingsAccessor){
    return ko.bindingHandlers.text.update(element,function(){
      var value = +(ko.unwrap(valueAccessor()) || 0),
        symbol = ko.unwrap(allBindingsAccessor().symbol !== undefined? allBindingsAccessor().symbol: ko.bindingHandlers.currency.symbol);
      return symbol + value.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, "$1,");
    });
  }
};

在这里,我们不需要初始化任何内容,因为初始状态和更新行为是相同的。必须要知道,当initupdate方法执行相同的操作时,只需使用update方法。

在这种情况下,我们将返回我们想要的格式的数字。首先,我们使用内置绑定称为 text 来更新我们元素的值。这个绑定获取元素和一个函数,指示如何更新此元素内部的文本。在本地变量 value 中,我们将写入 valueAccessor 内部的值。记住 valueAccessor 可以是一个 observable;这就是为什么我们使用 unwrap 方法。我们应该对 symbol 绑定执行相同的操作。symbol 是我们用来设置货币符号的另一个绑定。我们不需要定义它,因为此绑定没有行为,只是一个写/读绑定。我们可以使用 allBindingsAccesor 访问它。最后,我们返回连接两个变量的值,并设置一个正则表达式将值转换为格式化的货币。

我们可以更新 catalogcart 模板中的价格绑定。

<td data-bind="currency:price, symbol:'€'"></td>

我们可以设置我们想要的符号,价格将被格式化为:€100,或者如果我们设置符号为 $ 或空,则将看到 $100(如果价格值为 100)。

货币绑定

货币自定义绑定

注意观察如何轻松地添加越来越多有用的绑定以增强 Knockout 的功能。

货币绑定

使用 $root 上下文显示的容器进行调试。

创建一个调试绑定 – toJSON 绑定。

当我们开发我们的项目时,我们会犯错误并发现意外的行为。Knockout 视图模型很难阅读,因为我们没有普通对象,而是 observables。因此,也许在开发过程中,拥有一个显示视图模型状态的方法和容器可能很有用。这就是为什么我们要构建一个 toJSON 绑定,将我们的视图模型转换为一个普通的 JSON 对象,我们可以在屏幕上或控制台中显示。

ko.bindingHandlers.toJSON = {
  update: function(element, valueAccessor){
    return ko.bindingHandlers.text.update(element,function(){
      return ko.toJSON(valueAccessor(), null, 2);
    });
  }
};

我们已经使用 ko.toJSON 对象将我们获取的值转换为 JSON 对象。

此函数具有与原生 JSON.stringify 函数相同的接口。它将三个参数作为参数:

第一个参数是我们想要转换为普通 JSON 对象的对象。

第二个是替换参数。它可以是一个函数或一个数组。它应该返回应添加到 JSON 字符串中的值。有关替换参数的更多信息,请参阅以下链接:

developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_native_JSON#The_replacer_parameter

最后一个表示应该应用于格式化结果的空格。因此,在这种情况下,我们说我们将使用 valueAccesor() 方法中包含的对象,不使用替换函数,并且将缩进两个空格。

要看到它的作用,我们应该将此行放在具有 container-fluid 类的元素的末尾:

<pre class="well well-lg" data-bind="toJSON: $root"></pre>

现在在这个<div>标签里,我们可以将$root上下文视为一个 JSON 对象。$root上下文是我们整个 Knockout 上下文的顶部,所以我们可以在这个框中看到我们所有的视图模型。

为了让这在没有原生 JSON 序列化程序的老浏览器上工作(例如,IE 7 或更早版本),你还必须引用 json2.js 库。

github.com/douglascrockford/JSON-js/blob/master/json2.js

你可以在这个链接中了解更多关于 Knockout 如何将 observables 转换为普通 JSON:knockoutjs.com/documentation/json-data.html

通过我们的绑定语义化

有时候,我们写的代码对我们来说似乎很简单,但当我们仔细看时,我们意识到它并不简单。例如,在 Knockout 中,我们有内置的 visible 绑定。很容易认为如果我们想要隐藏某些东西,我们只需写:data-bind="visible:!isVisible",并且每次我们想要隐藏某些东西时都写这个。这并不够清晰。我们想要表达什么?这个元素应该默认隐藏吗?当变量不可见时它应该可见吗?

最好的方法是写一个名为hidden的绑定。如果你有一个hidden绑定,你可以写data-bind="hidden: isHidden;",这听起来更清晰,不是吗?这个绑定很简单,让我们看看以下的代码:

ko.bindingHandlers.hidden = {
  update: function (element, valueAccessor) {
    var value = ! ko.unwrap(valueAccessor());
    ko.bindingHandlers.visible.update(element, function () { 
      return value; 
    });
  }
};

我们只是使用visible类型的bindingHandler来改变valueAccessor方法的值。所以我们创建了一个更加有含义的绑定。

看看 Knockout 有多么强大和可扩展。我们可以构建越来越多的行为。例如,如果我们想要练习自定义绑定,我们可以创建一个接收照片数组而不仅仅是一张照片的自定义图像绑定,然后我们可以创建一个轮播。我们可以创建我们自己的链接绑定,帮助我们在我们的应用程序中导航。可能性是无限的。

现在,让我们看看如何将一个 jQuery 插件集成到我们的绑定中。

将一个 jQuery 插件包装成自定义绑定

Knockout 和 jQuery 兼容。实际上,没有必要将一个 jQuery 插件包装成一个绑定。它会工作,因为 Knockout 和 jQuery 是兼容的。然而,正如我们之前提到的,jQuery 是一个 DOM 操作库,所以我们需要设置一个 ID 来定位我们想要应用插件的元素,这将创建一个依赖关系。如果我们将插件包装在一个自定义绑定中,我们可以通过元素和valueAccessor参数访问元素和它的值,并且我们可以通过allBindings对象传递我们需要的一切。

我们将集成一个简单的插件叫做iCheck,这将为我们的复选框提供一个很酷的主题。

首先下载iCheck插件并将iCheck.js文件放入js文件夹中。然后将skins文件夹保存到css文件夹中。iCheck插件的下载链接如下:

github.com/fronteed/iCheck/archive/2.x.zip

使用index.html文件链接cssjavascript文件:

<link rel="stylesheet" type="text/css" href="css/iCheck/skins/all.css"><!-- set it just below bootstap -->
<script type="text/javascript" src="img/icheck.js">
</script><!-- set it just below jquery -->

现在我们需要初始化插件并更新元素的值。在这种情况下,initupdate方法是不同的。因此,我们需要编写当绑定开始工作时发生的情况以及当值更新时发生的情况。

.

将 jQuery 插件封装到自定义绑定中

将 iCheck 添加到我们的项目中

iCheck插件仅通过给我们的复选框提供样式来工作。现在的问题是我们需要将这个插件与我们的元素链接起来。

iCheck的基本行为是$('input [type=checkbox]').icheck(config)。当复选框的值更改时,我们需要更新我们绑定的值。幸运的是,iCheck有事件来检测值何时更改。

这个绑定只会管理iCheck的行为。这意味着可观察值的值将由另一个绑定处理。

使用checked绑定是有道理的。分别使用这两个绑定,以便iCheck绑定管理呈现,而checked绑定管理值行为。

将来,我们可以移除icheck绑定或者使用另一个绑定来管理呈现,复选框仍将正常工作。

按照我们在本章第一部分看到的init约定,我们将初始化插件并在init方法中设置事件。在update方法中,我们将在由checked绑定处理的可观察值更改时更新复选框的值。

注意我们使用allBindingsAccesor对象来获取已检查绑定的值:

ko.bindingHandlers.icheck = {
  init: function (element, valueAccessor, allBindingsAccessor) {
    var checkedBinding = allBindingsAccessor().checked;
    $(element).iCheck({
      checkboxClass: 'icheckbox_minimal-blue',
      increaseArea: '10%'
    });
    $(element).on('ifChanged', function (event) {
      checkedBinding(event.target.checked);
    });
  },
  update: function (element,valueAccessor, allBindings) {
    var checkedBinding = allBindingsAccessor().checked;
    var status = checked?'check':'uncheck';
    $(element).iCheck(status);
  }
};

现在我们可以使用这个来以隔离的方式在我们的应用程序中创建酷炫的复选框。我们将使用这个插件来隐藏和显示我们的搜索框。

将此添加到header.html模板中显示购物车详情 / 隐藏购物车详情按钮的下方:

<input type="checkbox" data-bind="icheck, checked:showSearchBar"/> Show Search options

然后转到catalog.html文件,在搜索栏中添加一个可见的绑定,如下所示:

<div class="input-group" data-bind="visible:showSearchBar">
  <span class="input-group-addon">
    <i class="glyphicon glyphicon-search"></i> Search
  </span>
  <input type="text" class="form-control" data-bind="textInput:searchTerm">
</div>

将变量添加到视图模型中,并在return语句中设置它,就像我们对所有其他变量所做的那样:

var showSearchBar = ko.observable(true);

现在你可以看到一个酷炫的复选框,允许用户显示和隐藏搜索栏:

将 jQuery 插件封装到自定义绑定中

组件 - 隔离的视图模型

自定义绑定非常强大,但有时我们需要更强大的行为。我们想要创建一个对应用程序的其余部分表现为黑匣子的隔离元素。这些类型的元素被称为组件。组件有自己的视图模型和模板。它还有自己的方法和事件,我们也可以说它本身就是一个应用程序。当然,我们可以使用依赖注入将我们的组件与我们的主应用程序视图模型链接起来,但是组件可以与给它正确数据的每个应用程序一起工作。

我们可以构建诸如表格、图表和您能想象到的一切复杂组件。要学习如何构建一个组件,您可以构建一个简单的组件。我们将创建一个add-to-cart按钮。这是一个连接我们的目录和购物车的组件,所以通过这个组件我们可以隔离我们的目录和我们的购物车。它们将通过这个组件连接,这个组件只是一个按钮,接收购物车和目录中的商品,并且将有将商品插入到购物车的所有逻辑。这是非常有用的,因为购物车不需要关心插入的商品,目录也不需要。另外,如果您需要在插入商品之前或之后执行一些逻辑,您可以在一个隔离范围内执行。

组件-隔离视图模型

组件有与主应用程序交互的隔离视图模型

一个组件的基本结构如下:

ko.components.register('component-name', {
  viewModel: function(params) {
    // Data: values you want to initilaize
    this.chosenValue = params.value;
    this.localVariable = ko.observable(true);
    // Behaviors: functions
    this.externalBehaviour = params.externalFunction;
    this.behaviour = function () { ... }
  },
  template:
    '<div>All html you want</div>'
});

使用这个模式的帮助,我们将构建我们的add-to-cart按钮。在custom文件夹内创建一个名为components.js的文件,并写入以下内容:

ko.components.register('add-to-cart-button', {
  viewModel: function(params) {
    this.item = params.item;
    this.cart = params.cart;

    this.addToCart = function() {
      var data = this.item;
      var tmpCart = this.cart();
      var n = tmpCart.length;
      var item = null;

      while(n--) {
        if (tmpCart[n].product.id() === data.id()) {
          item = tmpCart[n];
        }
      }

      if (item) {
        item.addUnit();
      } else {
        item = new CartProduct(data,1);
        tmpCart.push(item);
        item.product.decreaseStock(1);
      }

      this.cart(tmpCart);
    };
  },
  template:
    '<button class="btn btn-primary" data-bind="click:addToCart">
       <i class="glyphicon glyphicon-plus-sign"></i> Add
    </button>'
});

我们将要添加到购物车的商品和购物车本身作为参数发送,并定义addToCart方法。这个方法是我们在视图模型中使用的,但现在被隔离在这个组件内部,所以我们的代码变得更清晰了。模板是我们在目录中拥有的用于添加商品的按钮。

现在我们可以将我们的目录行更新如下:

<tbody data-bind="{foreach:catalog}">
  <tr data-bind="style:{color:stock() < 5?'red':'black'}">
    <td data-bind="{text:name}"></td>
    <td data-bind="{currency:price, symbol:''}"></td>
    <td data-bind="{text:stock}"></td>
    <td>
      <add-to-cart-button params= "{cart: $parent.cart, item: $data}">
      </add-to-cart-button>
    </td>
  </tr>
</tbody>

高级技术

在这一部分,我们将讨论一些高级技术。我们并不打算将它们添加到我们的项目中,因为没有必要,但知道如果我们的应用程序需要时可以使用这些方法是很好的。

控制后代绑定

如果我们的自定义绑定有嵌套绑定,我们可以告诉我们的绑定是否 Knockout 应该应用绑定,或者我们应该控制这些绑定如何被应用。 我们只需要在init方法中返回{ controlsDescendantBindings: true }

ko.bindingHandlers.allowBindings = {
  init: function(elem, valueAccessor) {
    return { controlsDescendantBindings: true };
  }
};

这段代码告诉 Knockout,名为allowBindings的绑定将处理所有后代绑定:

<div data-bind="allowBindings: true">
  <!-- This will display 'New content' -->
  <div data-bind="text: 'New content'">Original content</div>
</div>
<div data-bind="allowBindings: false">
  <!-- This will display 'Original content' -->
  <div data-bind="text: 'New content'">Original content</div>
</div>

如果我们想用新属性扩展上下文,我们可以用新值扩展bindingContext属性。然后我们只需要使用ko.applyBindingsToDescendants来更新其子项的视图模型。当然我们应该告诉绑定它应该控制后代绑定。如果我们不这样做,它们将被更新两次。

ko.bindingHandlers.withProperties = {
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    var myVM = { parentValues: valueAccessor, myVar: 'myValue'};
    var innerBindingContext = bindingContext.extend(myVM);
    ko.applyBindingsToDescendants(innerBindingContext, element);
    return { controlsDescendantBindings: true };
  }
};

这里我们并不创建一个子上下文。我们只是扩展父上下文。如果我们想创建子上下文来管理后代节点,并且能够使用$parentContext魔术变量来访问我们的父上下文,我们需要使用createChildContext方法来创建一个新上下文。

var childBindingContext = bindingContext.createChildContext(
  bindingContext.$rawData,
  null, //alias of descendant item ($data magic variable)
  function(context) {
    //manage your context variables
    ko.utils.extend(context, valueAccessor());
  });
ko.applyBindingsToDescendants(childBindingContext, element);
return { controlsDescendantBindings: true }; //Important to not bind twice

现在我们可以在子节点内部使用这些魔术变量:

<div data-bind="withProperties: { displayMode: 'twoColumn' }">
  The outer display mode is <span data-bind="text: displayMode"></span>.
  <div data-bind="withProperties: { displayMode: 'doubleWidth' }">
    The inner display mode is <span data-bind="text: displayMode"></span>, but I haven't forgotten that the outer display mode is <span data-bind="text: $parentContext.displayMode"></span>.
  </div>
</div>

通过修改绑定上下文和控制后代绑定,您将拥有一个强大而先进的工具来创建自己的自定义绑定机制。

使用虚拟元素

虚拟元素是允许使用 Knockout 注释的自定义绑定。您只需要告诉 Knockout 我们的绑定是允许虚拟的。

ko.virtualElements.allowedBindings.myBinding = true;
ko.bindingHandlers.myBinding = {
  init: function () { ... },
  update: function () { ... }
};

要将我们的绑定添加到允许的虚拟元素中,我们写下这个:

<!-- ko myBinding:param -->
<div></div>
<!-- /ko

虚拟元素具有操作 DOM 的 API。您可以使用 jQuery 操作虚拟元素,因为 Knockout 的一个优点是它与 DOM 库完全兼容,但是我们在 Knockout 文档中有完整的虚拟元素 API。这个 API 允许我们执行在实现控制流绑定时所需的类型的转换。有关虚拟元素的自定义绑定的更多信息,请参考以下链接:

knockoutjs.com/documentation/custom-bindings-for-virtual-elements.html

在绑定之前预处理数据

我们能够在绑定应用之前预处理数据或节点。这在显示数据之前格式化数据或向我们的节点添加新类或行为时非常有用。您也可以设置默认值,例如。我们只需要使用preprocesspreproccessNode方法。使用第一个方法,我们可以操纵我们绑定的值。使用第二个方法,我们可以操纵我们绑定的 DOM 元素(模板),如下所示:

ko.bindingHandlers.yourBindingHandler.preprocess = function(value) {
  ...
};

我们可以使用钩子preprocessNode操纵 DOM 节点。每当我们用 Knockout 处理 DOM 元素时,都会触发此钩子。它不绑定到一个具体的绑定。它对所有已处理的节点触发,因此您需要一种机制来定位要操纵的节点。

ko.bindingProvider.instance.preprocessNode = function(node) { 
  ...
};

摘要

在本章中,您已经学习了如何使用自定义绑定和组件扩展 Knockout。自定义绑定扩展了我们可以在data-bind属性中使用的选项,并赋予了我们使代码更可读的能力,将 DOM 和数据操作隔离在其中。另一方面,我们有组件。组件有它们自己的视图模型。它们本身就是一个孤立的应用程序。它们帮助我们通过彼此交互的小代码片段构建复杂的应用程序。

现在您已经知道如何将应用程序拆分成小代码片段,在下一章中,您将学习如何以不显眼的方式使用事件以及如何扩展可观察对象以增加 Knockout 的性能和功能。

要下载本章的代码,请转到 GitHub 存储库github.com/jorgeferrando/knockout-cart/tree/chapter3

第四章:管理 KnockoutJS 事件

我们的应用程序与用户之间的交互是我们需要解决的最重要问题。在过去的三章中,我们一直专注于业务需求,现在是时候考虑如何使最终用户更容易使用我们的应用程序了。

事件驱动编程是一种强大的范式,它能让我们更好地隔离我们的代码。KnockoutJS 给了我们几种处理事件的方式。如果我们想使用声明范式,可以使用点击绑定或者事件绑定。

有两种不同的方式来声明事件。声明范式说我们可以在我们的 HTML 中写 JavaScript 和自定义标签。另一方面,命令范式告诉我们应该将 JavaScript 代码与 HMTL 标记分开。为此,我们可以使用 jQuery 来编写不显眼的事件,也可以编写自定义事件。我们可以使用 bindingHandlers 来包装自定义事件,以便在我们的应用程序中重复使用它们。

事件驱动编程

当我们使用顺序编程来编写我们的应用程序时,我们会准确地知道我们的应用程序将会如何行为。通常情况下,我们在我们的应用程序与外部代理没有交互时使用这种编程范式。在网页开发中,我们需要使用事件驱动的编程范式,因为最终用户会主导应用程序的流程。

即使我们之前还没谈论过事件,我们知道它们是什么,因为我们一直在使用网页开发中最重要的事件之一,即点击事件。

用户可以触发许多事件。正如我们之前提到的,点击事件是用户可以在键盘上按键的地方;我们还可以从计算机那里接收事件,比如就绪事件,以通知我们 DOM 元素都已加载完毕。现在,如果我们的屏幕是可以触摸的,我们也有触摸事件。

我们还可以定义我们自定义的事件。如果我们想要通知实体但又不想创建它们之间的依赖关系,这就很有用。例如,假设我们想向购物车中添加物品。现在添加物品到购物车的责任在于视图模型。我们可以创建一个购物车实体,它封装了所有的购物车行为:添加、编辑、删除、显示、隐藏等等。如果我们开始在我们的代码中写: cart.add, cart.deletecart.show,那么我们的应用程序将依赖于 cart 对象。如果我们在我们的应用程序中创建事件,那么我们只需要触发它们,然后忘记接下来会发生什么,因为事件处理程序将为我们处理。

事件驱动编程能够减少耦合,但也降低内聚。我们应该选择在多大程度上要保持你的代码可读。事件驱动编程有时候是一个好的解决方案,但有一条规则我们应该始终遵循:KISS(保持简单,傻瓜)。所以,如果一个事件是一个简单的解决方案,就采用它。如果事件只是增加了代码行数,却没有给我们带来更好的结果,也许你应该考虑依赖注入作为更好的方法。

事件驱动的编程

事件驱动的编程工作流程

点击事件

在过去的三章中,我们一直在使用点击绑定。在这一章中,您将学习更多关于这个事件。点击事件是用户与应用程序进行交互的基本事件,因为鼠标一直是外设的首选(也是键盘)。

您可能已经了解到,如果将函数附加到点击绑定上,那么这个函数将会随着点击事件触发。问题在于,在 Knockout 中,点击事件不接受参数。据我们所知,我们点击函数的参数是预定义的。

传递更多参数

如我们所提到的,我们绑定到点击事件的函数具有预定义的签名:function functionName(data, event){...},并且这两个参数已经被分配:data 是绑定到元素的数据,event 是点击事件对象。那么如果我们想传递更多的参数会发生什么呢?我们有三种解决方案,如下所示:

  • 第一种是在视图模型中绑定参数:

    function clickEventFunctionWithParams(p1, p2, data, event) {
      //manageEvent
    }
    
    function clickEventFunction(data, event) {
      clickEventFunctionWithParams('param1', 'param2', data, event);
    }
    
  • 第二种选择是内联编写函数。如果我们想直接从模板中的上下文对象传递参数,那么这是一个有趣的选择。

    <button data-bind="click: function(data, event) {
      clickEventFunctionWithParams($parent.someVariable, $root.otherVariable, data, event);
    }">Click me</button>
    
  • 我们的第三个和最终的解决方案是第二个的变体,但更加优雅:

    <button data-bind="
      click: clickEventFunctionWithParams.bind($data, 'param1', 'param2')"
    >Click me</button>
    

我们可以使用最接近我们需求的那个。例如,如果我们想要传递的参数是视图模型中的常量或可观察对象,我们可以使用第一个。但是,如果我们需要传递上下文变量,比如$parent,我们可以使用最后一个。

bind函数是 JavaScript 原生的。它使用$data作为上下文创建另一个函数,然后将其余的参数应用到自身。您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind找到更多信息。

允许默认点击操作

默认情况下,KnockoutJS 阻止了点击时的默认操作。这意味着如果您在锚标签(<a>)中使用了点击操作,浏览器将执行我们已经链接的操作,而不会导航到链接的href。这种默认行为非常有用,因为如果您使用点击绑定,通常是因为您想执行不同的操作。如果您想允许浏览器执行默认操作,只需在函数末尾返回true

function clickEventFunction(data, event) {
  //run your code...

  //it allows to run the default behavior.
  //In anchor tags navigates to href value.
  return true;
}

事件冒泡

默认情况下,Knockout 允许点击事件继续冒泡到任何更高级别的事件处理程序。如果您的元素有一个也处理点击事件的父级,那么您将触发两个函数。为了避免冒泡事件,您需要包含一个名为clickBubble的附加绑定,并将其设置为false

<button data-bind="{
  click: clickEventFunction,
  clickBubble: false
}">Click me</button>

事件冒泡

事件冒泡的工作流程

事件类型

浏览器可以抛出许多类型的事件。 您可以在developer.mozilla.org/en-US/docs/Web/Events找到完整的参考资料。

正如我们所知,每个浏览器都有自己的一套指令; 因此,我们可以将事件分类为以下几组:

  • 标准事件:这些事件在官方 Web 规范中定义,应该在各种浏览器中普遍存在。

  • 非标准事件:这些事件是为每个浏览器引擎专门定义的。

  • Mozilla 特定事件:这些事件用于插件开发,包括以下内容:

    • 插件特定事件

    • XUL 事件

事件绑定

为了捕获和处理所有这些不同的事件,Knockout 有event绑定。 我们将使用它在文本上方和离开时显示和隐藏调试面板,借助以下代码的帮助:

  1. index.html 模板的第一个更新如下。 用这个新的 HTML 替换调试 div:

    <div data-bind="event: {
      mouseover:showDebug,
      mouseout:hideDebug
    }">
      <h3 style="cursor:pointer">
        Place the mouse over to display debug
      </h3>
      <pre class="well well-lg" data-bind="visible:debug, toJSON: $root"></pre>
    </div>
    

    该代码表示,当我们将鼠标悬停在div元素上时,我们将显示调试面板。 最初,只显示h3标签内容。

  2. 当我们将鼠标悬停在h3标签上时,我们将更新调试变量的值,并显示调试面板。 为了实现这一点,我们需要使用以下代码更新我们的视图模型:

    var debug = ko.observable(false);
    
    var showDebug = function () {
      debug(true);
    };
    
    var hideDebug = function () {
      debug(false);

      };
    
  3. 然后我们需要更新我们的接口(视图模型的返回值)。

    return {
      debug: debug,
      showDebug:showDebug,
      hideDebug:hideDebug,
      searchTerm: searchTerm,
      catalog: filteredCatalog,
      cart: cart,
      newProduct: newProduct,
      totalItems:totalItems,
      grandTotal:grandTotal,
      addProduct: addProduct,
      addToCart: addToCart,
      removeFromCart:removeFromCart,
      visibleCatalog: visibleCatalog,
      visibleCart: visibleCart,
      showSearchBar: showSearchBar,
      showCartDetails: showCartDetails,
      hideCartDetails: hideCartDetails,
      showOrder: showOrder,
      showCatalog: showCatalog,
      finishOrder: finishOrder
    };
    

现在当鼠标悬停在h3标签上时,调试面板将显示。 试试吧!

无侵入 jQuery 事件

在过去几年里,从 HTML 模板中删除所有 JavaScript 代码已经成为一个良好的做法。 如果我们从 HTML 模板中删除所有 JavaScript 代码并将其封装在 JavaScript 文件中,我们就是在进行命令式编程。 另一方面,如果我们在 HTML 文件中编写 JavaScript 代码或使用组件和绑定,我们就是在使用声明式编程。 许多程序员不喜欢使用声明式编程。 他们认为这使得设计人员更难以处理模板。 我们应该注意,设计人员不是程序员,他们可能不理解 JavaScript 语法。 此外,声明式编程将相关代码拆分为不同的文件,可能使人们难以理解整个应用程序的工作方式。 此外,他们指出,双向绑定使模型不一致,因为它们在没有任何验证的情况下即时更新。 另一方面,有人认为声明式编程使代码更易于维护,模块化和可读性强,并且说如果您使用命令式编程,您需要在标记中填充不必要的 ID 和类。

没有绝对的真理。你应该在两种范式之间找到平衡。声明式本质在消除常用功能并使其变得简单方面表现得很出色。foreach 绑定及其兄弟,以及语义 HTML(组件),使代码易于阅读并消除了复杂性。我们必须自己用 JavaScript 编写,使用选择器与 DOM 交互,并为团队提供一个共同的平台,使他们可以专注于应用程序的工作原理,而不是模板和模型之间的通信方式。

还有其他框架,如 Ember、React 或 AngularJS,它们成功地使用了声明式范式,因此这并不是一个坏主意。但是,如果你感觉更舒适地使用 jQuery 定义事件,你将学会如何做。我们将以不引人注目的方式编写 确认订单 按钮。

首先,删除 data-bind 属性并添加一个 ID 来定位按钮:

<button id="confirmOrderBtn" class="btn btn-primary btn-sm">
  Confirm Order
</button>

现在,在 applyBindings 方法的上方写入这段 JavaScript 代码:

$(document).on('click', '#confirmOrderBtn').click(function() {
  vm.showOrder();
});
ko.applyBindings(vm);

这两种方法都是正确的;决定选择哪种范式是程序员的决定。

如果我们选择以 jQuery 的方式编写我们的事件,将所有事件合并到文件中也是一个好习惯。如果你没有很多事件,你可以有一个名为 events.js 的文件,或者如果你有很多事件,你可以有几个文件,比如 catalog.events.jscart.events.js

使用 jQuery 实现不引人注目的事件

命令式范式与声明式范式

委托模式

当我们处理大量数据时,普通的事件处理会影响性能。有一种技术可以提高事件的响应时间。

当我们直接将事件链接到项目时,浏览器为每个项目创建一个事件。然而,我们可以将事件委托给其他元素。通常,这个元素可以是文档或元素的父级。在这种情况下,我们将其委托给文档,即添加或移除产品中的一个单位的事件。问题在于,如果我们只为所有产品定义一个事件管理器,那么我们如何设置我们正在管理的产品?KnockoutJS 为我们提供了一些有用的方法来成功实现这一点,ko.dataForko.contextFor

  1. 我们应该通过分别添加 add-unitremove-unit 类来更新 cart-item.html 文件中的添加和移除按钮:

    <span class="input-group-addon">
      <div class="btn-group-vertical">
        <button class="btn btn-default btn-xs add-unit">
          <i class="glyphicon glyphicon-chevron-up"></i>
        </button>
        <button class="btn btn-default btn-xs remove-unit">
          <i class="glyphicon glyphicon-chevron-down"></i>
        </button>
      </div>
    </span>
    
  2. 然后,我们应该在 确认订单 事件的下方添加两个新事件:

     $(document).on("click", ".add-unit", function() {
      var data = ko.dataFor(this);
      data.addUnit();
    });
    
    $(document).on("click", ".remove-unit", function() {
      var data = ko.dataFor(this);
      data.removeUnit();
    });
    
  3. 使用 ko.dataFor 方法,我们可以获得与我们在 KnockoutJS 上下文中使用 $data 获得的相同内容。有关不引人注目的事件处理的更多信息,请访问knockoutjs.com/documentation/unobtrusive-event-handling.html

  4. 如果我们想要访问上下文,我们应该使用 ko.contextFor;就像这个例子一样:

    $(document).on("click", ".add-unit", function() {
      var ctx = ko.contextFor(this);
      var data = ctx.$data;
      data.addUnit();
    });
    

因此,如果我们有数千种产品,我们仍然只有两个事件处理程序,而不是数千个。以下图表显示了代理模式如何提高性能:

代理模式

代理模式提高了性能。

构建自定义事件

有时,我们需要使应用程序中的两个或多个实体进行通信,这些实体彼此不相关。例如,我们希望将我们的购物车保持独立于应用程序。我们可以创建自定义事件来从外部更新它,购物车将对此事件做出反应;应用所需的业务逻辑。

我们可以将事件拆分为两个不同的事件:点击和动作。因此,当我们点击上箭头添加产品时,我们触发一个新的自定义事件来处理添加新单位的操作,删除产品时同样如此。这为我们提供了关于应用程序中正在发生的事情的更多信息,我们意识到一个通用含义的事件,比如点击,只是获取数据并将其发送到更专业的事件处理程序,该处理程序知道该怎么做。这意味着我们可以将事件数量减少到只有一个。

  1. viewmodel.js文件末尾创建一个click事件处理程序,抛出一个自定义事件:

    $(document).on("click", ".add-unit", function() {
      var data = ko.dataFor(this);
      $(document).trigger("addUnit",[data]);
    });
    
    $(document).on("click", ".remove-unit", function() {
      var data = ko.dataFor(this);
      $(document).trigger("removeUnit, [data]);
    });
    
    $(document).on("addUnit",function(event, data){
      data.addUnit();
    });
    $(document).on("removeUnit",function(event, data){
      data.removeUnit();
    });
    

    粗体行展示了我们如何使用 jQuery 触发方法来发出自定义事件。与关注触发动作的元素不同,自定义事件将焦点放在被操作的元素上。这给了我们一些好处,比如代码清晰,因为自定义事件在其名称中有关于其行为的含义(当然我们可以称事件为event1,但我们不喜欢这种做法,对吧?)。

    您可以在 jQuery 文档中阅读更多关于自定义事件的内容,并查看一些示例,网址为learn.jquery.com/events/introduction-to-custom-events/

  2. 现在我们已经定义了我们的事件,是时候将它们全部移到一个隔离的文件中了。我们将这个文件称为cart/events.js。这个文件将包含我们应用程序的所有事件。

    //Event handling
    (function() {
      "use strict";
      //Classic event handler
      $(document).on('click','#confirmOrder', function() {
        vm.showOrder();
      });
      //Delegated events
      $(document).on("click", ".add-unit", function() {
        var data = ko.dataFor(this);
        $(document).trigger("addUnit",[data]);
      });
      $(document).on("click", ".remove-unit", function() {
        var data = ko.dataFor(this);
        $(document).trigger("removeUnit, [data]);
      })
      $(document).on("addUnit",function(event, data){
       data.addUnit();
      });
      $(document).on("removeUnit",function(event, data){
       data.removeUnit();
      });
    })();
    
  3. 最后,将文件添加到脚本部分的末尾,就在viewmodel.js脚本的下方:

    <script type="text/javascript" src="img/events.js"></script>
    

我们应该注意到现在与购物车的通信是使用事件完成的,并且我们没有证据表明有一个名为cart的对象。我们只知道我们要与之通信的对象具有两个方法的接口,即addUnitremoveUnit。我们可以更改接口中的对象(HTML),如果我们遵守接口,它将按照我们的期望工作。

事件和绑定

我们可以将事件和自定义事件包装在bindingHandlers中。假设我们希望仅在按下Enter键时过滤产品。这使我们能够减少对过滤方法的调用,并且如果我们正在对服务器进行调用,这种做法可以帮助我们减少流量。

custom/koBindings.js文件中定义自定义绑定处理程序:

ko.bindingHandlers.executeOnEnter = {
  init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
    var allBindings = allBindingsAccessor();
    $(element).keypress(function (event) {
      var keyCode = (event.which ? event.which : event.keyCode);
      if (keyCode === 13) {
        allBindings.executeOnEnter.call(viewModel);
        return false;
      }
      return true;
    });
  }
};

由于这是一个事件,我们应该记住事件初始化可以在init方法本身中设置。我们用 jQuery 捕获keypress事件并跟踪被按下的键。Enter键的键码是 13。如果我们按下Enter键,我们将在视图模型的上下文中调用executeOnEnter绑定值。这就是allBindings.executeOnEnter.call(viewModel);所做的。

然后,我们需要更新我们的视图模型,因为我们的过滤目录是一个计算的可观察数组,每当按下键时都会更新自身。现在我们需要将这个计算的可观察数组转换为一个简单的可观察数组。因此,请根据以下方式更新您的filteredCatalog变量:

//we set a new copy from the initial catalog
var filteredCatalog = ko.observableArray(catalog());

意识到以下更改的后果:

var filteredCatalog = catalog();

我们不是在制作副本,而是在创建一个引用。如果我们这样做,当我们过滤目录时,我们将丢失项目,而且我们将无法再次获取它们。

现在我们应该创建一个过滤目录项目的方法。这个函数的代码与我们在上一个版本中拥有的计算值类似:

var filterCatalog = function () {
  if (!catalog()) {
    filteredCatalog([]);
  }
  if (!filter) {
    filteredCatalog(catalog());
  }
  var filter = searchTerm().toLowerCase();
  //filter data
  var filtered = ko.utils.arrayFilter(catalog(), function(item){
    var strProp = ko.unwrap(item["name"]).toLocaleLowerCase();
    if (strProp && (strProp.indexOf(filter) !== -1)) {
      return true;
    }
    return false;
  });
  filteredCatalog(filtered);
};

现在将其添加到return语句中:

return {
  debug: debug,
  showDebug:showDebug,
  hideDebug:hideDebug,
  searchTerm: searchTerm,
  catalog: filteredCatalog,
  filterCatalog:filterCatalog,
  cart: cart,
  newProduct: newProduct,
  totalItems:totalItems,
  grandTotal:grandTotal,
  addProduct: addProduct,
  addToCart: addToCart,
  removeFromCart:removeFromCart,
  visibleCatalog: visibleCatalog,
  visibleCart: visibleCart,
  showSearchBar: showSearchBar,
  showCartDetails: showCartDetails,
  hideCartDetails: hideCartDetails,
  showOrder: showOrder,
  showCatalog: showCatalog,
  finishOrder: finishOrder
};

最后一步是更新catalog.html模板中的搜索元素:

<div class="input-group" data-bind="visible:showSearchBar">
  <span class="input-group-addon">
    <i class="glyphicon glyphicon-search"></i> Search
  </span>
  <input type="text" class="form-control"
  data-bind="
    textInput: searchTerm,
    executeOnEnter: filterCatalog"
  placeholder="Press enter to search...">
</div>

现在,如果您在搜索框中输入内容,输入项目将不会更新;然而,当您按下Enter键时,过滤器会应用。

这是在插入新代码后我们的文件夹结构的样子:

事件和绑定

文件夹结构

摘要

在本章中,您已经学会了如何使用 Knockout 和 jQuery 管理事件。您已经学会了如何结合这两种技术,以根据项目的要求应用不同的技术。我们可以使用声明性范例来组合事件附加、bindingHandlers和 HTML 标记,或者我们可以使用 jQuery 事件将事件隔离在 JavaScript 代码中。

在下一章中,我们将解决与服务器通信的问题。您将学习如何验证用户输入,以确保我们向服务器发送干净和正确的数据。

我们还将学习模拟数据服务器端的技术。使用模拟库将帮助我们开发我们的前端应用程序,而无需一个完整的操作服务器。为了发送 AJAX 请求,我们将启动一个非常简单的服务器来运行我们的应用程序,因为浏览器默认不允许本地 AJAX 请求。

请记住,您可以在 GitHub 上检查本章的代码:

github.com/jorgeferrando/knockout-cart/tree/chapter4