knockoutJS 快速上手

时间:2024-01-18 14:06:17

翻译:Knockout 快速上手 - 3: knockoutJS 快速上手

许多时候,学会一种技术的有效方式就是使用它解决实际中的问题。在这一节,我们将学习使用 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 函数来执行。

分类: javascript