许多时候,学会一种技术的有效方式就是使用它解决实际中的问题。在这一节,我们将学习使用 Knockout 来创建一个常见的应用,库存管理应用。
应用概览
在创建我们的应用之前,我们需要一个公司,来理解应用解决的问题。我们的应用将能够完成下列任务:
- 浏览公司销售的每种产品,跟踪 SKU 数量和说明。
- 对每种产品的价格,费用和数量进行赋值。
- 当公司决定销售某种新产品的时候,可以创建新的产品。
- 当公司停售某种产品的时候,可以删除这种产品。
第一步 定义命名空间
在我们实际开始开发应用之前,很重要的一个问题就是规划我们如何组织我们的程序,将我们应用的代码与浏览器界面和本地函数进行分离。你可能奇怪对于这么小的应用我们为什么要这么做。对于 JavaScript 应用的最佳实践来说,这么做无论如何都是非常重要的。通过命名空间,即使对于一个很小的应用来说,在以后随着应用的不断扩展,也可以确保容易进行维护,并且与第三方的组件进行分隔。( 例如许多的脚本插件 )
我们将在前面创建的 app.js 中定义我们的命名空间。下面代码就是定义定名空间的代码。
// Define the namespace window.myApp = {}; |
第二步 创建模型
我们创建的第一个模型将用来表示我们的产品对象。我们通过创建一个名为 Product.js 的文件来完成这个任务。文件的内容如下所示。
(function (myApp) { // Product Constructor Function function Product() { var self = this; // "SKU" property self.sku = ko.observable(""); // "Description" property self.description = ko.observable(""); // "Price" property self.price = ko.observable(0.00); // "Cost" property self.cost = ko.observable(0.00); // "Quantity" property self.quantity = ko.observable(0); } // add to our namespace myApp.Product = Product; }(window.myApp)); |
在这段代码中,我们定义了一个函数作为 Product 的构造器。如你所见,我们将这个函数定义在一个称为立即执行的函数表达式中 ( IIFE )。我们为了如下的原因使用这个模式:
- 这使得我们定义了一个 JavaScript 的作用域,防止污染全局命名空间 ( 像 window 和 document 所处的命名空间 )。这使得我们在调试的时候,不会在本地的函数,比如 windows 中看到和使用我们定义的 Product 函数。
- 这使得我们可以创建私有的函数,在其他的代码中禁止访问。如果我们定义了 Product 函数之后,没有将它添加到 myApp 命名空间中,就没有代码可以在 IIFE 之外访问我们的 Product 构造器。这在创建复杂逻辑的时候非常理想,在某种程度上可以防止其它的对象访问和重写我们的逻辑。
在构造器函数内部,每个属性都创建在 self 对象之上。self 对象是一个指向新创建的 Product 对象的引用。在 JavaScript 中,this 是一个关键字,但是程序员经常被它不同的含义所困惑。这使由于它可以表示多种不同的对象 ( 比如调用对象,全局对象等等 )。为了防止这个问题,我们创建一个局部变量 self ,这样,我们就可以确信它总是表示我们当前的对象实例。
最后,每个属性的值被赋予一个 Knockout 的 Observable 实例。Observable 是 Knockout 中创建可以在属性发生变化的时候触发事件的属性的简单方式 ( 这是 Knockout 中的一个核心概念,我们在后继内容中还要深入讨论 )。通过将属性的初始值传递给这个函数,我们得到一个包装了初始值的函数返回值。可以通过调用这个包装函数来为属性赋值和取值。下面的实例演示了如何使用我们的构造器和属性。
// Usage // create an instance of the Product class var productA = new myApp.Product(); // "set" the 'sku' property productA.sku('12345') // "get" the 'sku' property value var skuNumber = productA.sku(); |
第三步 创建模型使用的视图
现在,我们已经定义了我们的模型类。我们需要创建一个视图在屏幕上显示模型,以便用户可以看到我们的产品数据。我们将使用 HTML 来创建这个视图。我们将使用很简单的布局来显示产品的信息。
<div id="productView"> <p> SKU: <span data-bind="text: sku"></span> </p> <p> Description: <span data-bind="text: description"></span> </p> <p> Cost: <span data-bind="text: cost"></span> </p> <p> Price: <span data-bind="text: price"></span> </p> <p> Quantity: <span data-bind="text: quantity"></span> </p> </div> |
这里,我们使用 Knockout 的 text 绑定来显示产品的信息。text 绑定将属性的值转化为 string 之后,设置 HTML 元素的 innerText 属性 ( 通常使用 span 元素 )。
第四步 创建 ViewModel 管理模型
这里,我们将会需要创建业务逻辑,来处理创建产品,删除产品来管理我们的产品列表。我们还需要某种数组来来管理我们的产品列表。因此,我们将建新的类来实现所有的功能、数组、对象以便绑定到用户界面上。我们需要的类就是 ViewModel.
像我们现在创建应用一样,刚开始的 ViewModel 我们仅仅定义一个属性 selectedProduct。这个属性表示我们当前显示在屏幕上进行处理的单个产品,在 js 文件夹中添加一个名为 ProductsViewModel.js 的脚本文件,在其中添加如下代码。
// Products ViewModel (function (myApp) { // constructor function function ProductsViewModel() { var self = this; // the product that we want to view/edit self.selectedProduct = ko.observable(); } // add our ViewModel to the public namespace myApp.ProductsViewModel = ProductsViewModel; }(window.myApp)); |
第五步 使用 Observable 数组
我们公司的业务需要销售多种产品,所以,我们需要保持一个当前产品的列表。在 JavaScript 中,管理和维护一个对象集合的数据结构就是数组。Knockout 更进一步,提供了一个名为 ObservableArray 的对象。后面我会进一步讨论这个对象,这个对象在成员发生变化的时候,会抛出相应的事件通知,这就允许 Knockout 可以在 ObservableArray 发生变化的时候保持用户界面和我们数据结构的同步。
Knockout 的 ObservableArray 与标准的 JavaScript 数组拥有相同的使用方式,包括 ( push, pop, slice, splice ) 等等。所以,如果你使用过 JavaScript 的 Array 话,使用起来非常自然和流畅。
为了创建公司产品的主列表,为们需要为我们的视图模型添加一个新的属性 productCollection 。
// the product that we want to view/edit self.selectedProduct = ko.observable(); // the product collection self.productCollection = ko.observableArray([]); |
第六步 从 ObservableArray 中添加和删除模型
现在,我们已经拥有了一个公司所有产品的列表,下面我们实现向这个列表添加产品和删除产品的逻辑。
添加产品的逻辑仍然比较简单,可以在这个过程中添加一些验证和检查。但是尽可能地简单和清楚。
// creates a new product and sets it up // for editing self.addNewProduct = function () { // create a new instance of a Product var p = new myApp.Product(); // set the selected Product to our new instance self.selectedProduct(p); }; // logic that is called whenever a user is done editing // a product or done adding a product self.doneEditingProduct = function () { // get a reference to our currently selected product var p = self.selectedProduct(); // ignore if it is null if (!p) { return; } // check to see that the product // doesn't already exist in our list if (self.productCollection.indexOf(p) > -1) { self.selectedProduct(null); return; } // add the product to the collection self.productCollection.push(p); // clear out the selected product self.selectedProduct(null); }; |
在这些代码中,我们计划在用户添加新的产品调用addNewProduct 的时候,使用新创建的 Product 对象填充我们当前选中的对象selectedProduct,然后可以开始进行编辑。在用户完成编辑之后,调用doneEditingProduct 的时候,注意需要检查selectedProduct 是否为空,不为空的话,将这个对象添加到产品列表中。
删除产品的逻辑更加简单一些,我们直接检查selectedProduct 是否为空,如果不为空,就直接从列表中删除它。
// logic that removes the selected product // from the collection self.removeProduct = function () { // get a reference to our currently selected product var p = self.selectedProduct(); // ignore if it is null if (!p) { return; } // empty the selectedProduct self.selectedProduct(null); // simply remove the item from the collection return self.productCollection.remove(p); }; |
最后,在用户界面上,我们需要提供一些按钮,用户可以通过它们调用这些业务逻辑。我们添加按钮,绑定按钮的 click 事件到视图模型的相关属性上,如下所示:
<div id="content"> <div id="productView" data-bind="with: selectedProduct"> <p> SKU: <span data-bind="text: sku"></span> </p> <p> Description: <span data-bind="text: description"></span> </p> <p> Cost: <span data-bind="text: cost"></span> </p> <p> Price: <span data-bind="text: price"></span> </p> <p> Quantity: <span data-bind="text: quantity"></span> </p> </div> <div id="buttonContainer"> <button type="button" data-bind="click: addNewProduct">Add</button> <button type="button" data-bind="click: removeProduct">Remove</button> <button type="button" data-bind="click: doneEditingProduct">Done</button> </div> </div> |
第七步 编辑模型的属性
到现在为止,我们仍然没有办法编辑产品列表中每个产品的属性。所以,需要修改我们的视图以便实现双向的绑定。Knockout 的 value 绑定可以帮助我们实现这个目的,但是只能在 input 元素上使用这个绑定。下面我们修改一下我们的视图,如下所示:
<div id="productView"> <form> <fieldset> <legend>Product Details</legend> <label> SKU: <input type="text" data-bind="value: sku" /> </label> <br /> <label> Description: <input type="text" data-bind="value: description" /> </label> <br /> <label> Cost: <input type="text" data-bind="value: cost" /> </label> <br /> <label> Price: <input type="text" data-bind="value: price" /> </label> <br /> <label> Quantity: <input type="text" data-bind="value: quantity" /> </label> </fieldset> </form> </div> |
现在,我们基于表单的视图可以支持编辑产品的属性了。我将会提到这一点,我们需要添加一些输入的验证来保证 Cost 和 Price 中提供了正确的金额,还有Quantity 中是正确的整数。实际上这些问题有些超出了本教程的范围,在互联网上你可以找到很多实现这些功能的脚本库。
第八步 创建主从视图
终于,我们已经创建了管理数据的逻辑,以及通过 HTML 提供了一个非常友好的用户界面,实现了管理公司产品的功能。让我们继续前进,为用户创建一个好用的主从界面视图。
首先,我们需要确认产品视图正确绑定在我们选定的产品上,而且,产品视图只有在选中产品实例之后,才会显示出来。Knockout 提供了一个称为 with 的绑定来实现这些功能。后面我们会详细讨论这些问题。但是 with 绑定不仅提供选中产品的 null 检测,还实现了将绑定的上下文从 ProductViewModel 切换到 selectedProduct ( 这样我们就可以在数据绑定的语法中直接引用这些属性 )。
由于只有在我们选中一个产品的时候,Remove 和 Done 按钮才是可见的,我们将为这两个按钮添加一个 visible 绑定,用来检查 selectedProduct 属性是否已经有值。也可以为 Add 按钮做类似的工作,完成这些功能的代码如下所示。
<div id="buttonContainer"> <button type="button" data-bind="click: addNewProduct, visible: (selectedProduct() ? false : true)">Add</button> <button type="button" data-bind="click: removeProduct, visible: (selectedProduct() ? true : false)">Remove</button> <button type="button" data-bind="click: doneEditingProduct, visible: (selectedProduct() ? true : false)">Done</button> </div> |
最后,我们还需要提供一个显示产品列表的视图来方便用户管理产品。通常是一个表格,列表等等。或者一些控件来实现这些功能。Knockout 足够强大,我们可以直接使用原始的 HTML 来显示产品列表 ProductCollection。
我们使用基本的 select 元素来实现基本的列表。Knockout 提供了一个 options绑定,支持我们将一个 ObservableArray 绑定到 select 元素。我们还将会提供第二个 Observable 绑定来保持视图中选中的产品。为了达到这个目的,我们在 select 元素中使用 value 绑定来绑定到选中的项目,在视图模型中,我们增加一个新的绑定属性listViewSelectedItem,,下面的代码演示了新建的属性。属性后面的 subscription 用来传递这个属性的任何变化到我们的selectedProduct 属性中。
// the product that we want to view/edit self.selectedProduct = ko.observable(); // the product collection self.productCollection = ko.observableArray([]); // product list view selected item self.listViewSelectedItem = ko.observable(null); // push any changes in the list view to our // main selectedProduct self.listViewSelectedItem.subscribe(function (product) { if (product) { self.selectedProduct(product); } }); |
我们的列表视图实现如下所示:
<div id="productListView"> <select id="productList" size="10" style="min-width: 120px;" data-bind="options: productCollection, value: listViewSelectedItem, optionsText: 'sku'"> </select> </div> |
在前面代码中,使用了optionsText 绑定来绑定 ObservableArray 中每个元素的属性,开始的时候,我们设置 Product 的 sku 属性,但是我们如何能够同时看到 sku 属性和 description 属性的值呢?我们可以通过 Computed Observable 来实现,很快我们就会讨论这个特性,现在,我们在 Product 类中添加一个计算出 sku 属性和 description 属性的新属性。
// Computed Observables // simply combines the Sku and Description properties self.skuAndDescription = ko.computed(function () { var sku = self.sku() || ""; var description = self.description() || ""; return sku + ": " + description; }); |
在添加了skuAndDescription 属性之后,应该更新一下产品列表视图,可以将optionsText 属性的值重新设置为skuAndDescription 来代替原来的 sku。
第九步 应用绑定
为了让我们的应用能够实际运行,我们需要启动 Knockout 的绑定处理,我们需要确认在所有的脚本正确加载之后,在 ViewModel 初始化之后,执行绑定处理过程。我建议的方式是在 app.js 中如下处理。
// Define the namespace window.myApp = {}; (function (myApp) { // constructor functio for App function App() { // core logic to run when all // dependencies are loaded this.run = function () { // create an instance of our ViewModel var vm = new myApp.ProductsViewModel(); // tell Knockout to process our bindings ko.applyBindings(vm); } } // make sure its public myApp.App = App; }(window.myApp)); |
在 app.js 中创建了初始化逻辑之后,我们需要创建 app 的实例,然后调用 run 方法,在页面最后的位置添加如下的代码。
<script type="text/javascript"> var app = new myApp.App(); app.run(); </script> |
为了教学的目的,我将这段代码放在页面几乎最后的位置,我们还有其他的方式可以使用,比如通过 jQuery 的 ready 函数来执行。