許多時候,學會一種技術的有效方式就是使用它解決實際中的問題。在這一節,我們將學習使用 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 函數來執行。