前言
鑽研ABP框架的日子,遇到了很多新的知識,因為對自己而言是新知識,所以經常卡在很多地方,遲遲不能有所突破,作為一個稍有上進心的程序員,內心絕對是不服輸的,也絕對是不畏困難的,心底必然有這樣一股力量“I must conquer it!”。比如,以前沒用過AutoMapper,那我就去學,最后將學到的寫成了博客,和大家一起分享,爭取讓那些還沒接觸的人少走彎路。現在,在前端設計時,ABP很多用到的都是AngularJS,而之前做項目都是用的JQuery,所以繼續研究ABP也是相當有阻力。但是,不能因為有阻力就拖延,最終導致半途而廢吧!因而,還是得學,畢竟技多不壓身嘛!況且,JQuery是MPA【經典】,而AngularJS是SPA【年輕】,這兩種框架都掌握了,也算是挺圓滿的吧。
現在回到正題,這篇博客,我們將會看到如何使用AngularJS創建Web應用。對初學者而言,涵蓋並解釋一些AngularJS框架的基本特征。我設計了一個簡單的關於產品的CRUD的例子,解釋了演示中所有的代碼片段。
背景
當前,AngularJS作為Javascript的MVC(也有人說是MV*,暫且不糾結這個)框架被廣泛使用,它為更快且更容易地開發響應式的Web提供了強大的機制。作為MVC框架,它將Web前端代碼分成三個組件Model,View和Controller。因此,在data model,應用邏輯(Controllers)和view展示之間有明確的分離,讓你更容易地關注關鍵的開發區域。view接收來自model的數據來展示。當用戶通過點擊或者敲擊鍵盤和應用交互時,controller通過改變模型中的數據進行響應。最終,view得到了發生在model中的變化這個通知,從而它能更新展示的內容。
在Angular應用中,view是DOM(文檔對象模型),controller是javascript類,model數據存儲在對象屬性中。AngularJS將集成在客戶端的html和數據傳輸給瀏覽器。就像Jquery庫一樣,為了使用新鮮的model狀態更新UI,你必須和DOM交互。例如,無論你何時觀察到model中的任何改變,你都必須和DOM交互來影響這個改變(如果以前經常使用Jquery,那么這句話不難理解)。然而,AngularJS提供了數據綁定,我們不必將數據從一個地方移到另一個地方,只需要使用javascript屬性映射UI部件,然后它就會自動同步了。
使用代碼
源代碼下載
現在開始寫代碼,讓我們一步一步地創建這個簡單的CRUD操作案例。
添加一個index.html頁面,並添加一個屬性ng-app屬性,例如:
<html ng-app="demoApp">
這句代碼就將我們的應用定義為AngularJS應用。因為AngularJS是SPA(單頁面應用)框架,所以它會使用html視圖模板作為分部視圖來響應確定的路由。為了渲染一個分部視圖,我們會給一個div標簽添加一個ng-view屬性,所有的分部視圖都會渲染在這個div標簽中。
<div ng-view=""></div>
下載兩個文件angular.js 和angular-route.js,在index.html中添加這些文件的引用。或者你也可以用CDN的靜態資源,比如百度CDN的靜態資源http://apps.bdimg.com/libs/angular.js/1.4.6/angular.js:
<script src="../Scripts/angular.min.js"></script> <script src="../Scripts/angular-route.min.js"></script>
分部視圖
現在來點我們CRUD案例的真實的內容,我們會開發兩個分部視圖,ProductList.html和ProductEdit.html。先來看一下ProductList.html的代碼:
<div class="container"> <h2 class="page-title">產品列表</h2> <div class="searchbar"> <ul class="entity-tabular-fields"> <li> <label>搜索:</label> <span class="field-control"> <input type="text" ng-model="filter.productName" value=" " /> </span> <label></label> </li> </ul> </div> <h2><a href="#/ProductEdit">新增產品</a></h2> <table class="items-listing"> <thead> <tr> <th>代碼</th> <th>名稱</th> <th>描述</th> <th>類別</th> <th>操作</th> </tr> </thead> <tbody> <tr ng-repeat="product in products|filter:filter.productName"> <td><a href="#/ProductEdit?code={{product.code}}">{{product.code}}</a></td> <td>{{product.name}}</td> <td>{{product.description}}</td> <td>{{product.category}}</td> <td><a href="#/?code={{product.code}}">刪除</a></td> </tr> </tbody> </table> </div>
效果圖:
除了html和css,更要關注angular指令的使用。在<tbody>中,我們使用了指令ng-repeat="product in products | filter:filter.productName"。products是一個javascript數組,它是存儲在內存中的。該指令告訴javascript循環遍歷數組中的每個元素(如果你使用過C#的話,那么它就相當於C#中的foreach),並且為每個元素生成一個<tr>標簽。接下來,我們添加了過濾器,該過濾器根據productName過濾出我們搜索條件中要求的一些元素,也許正如你看到的,它是一個對象屬性,該屬性通過數據綁定指令ng-model="filter.productName"綁定在搜索框上。因此無論你在搜索框中輸入什么,產品數組都會使用匹配的字符串過濾。為了輸出一些值到html上,我們使用angular的數據綁定語法{{output_Expression}},例如表格單元的產品屬性:
<td>{{product.name}}</td>
對於第一個單元格,我們顯示了product.code,它會鏈接到第二個分部視圖ProductEdit.html。
最后一個單元格也包含了一個到列表展示頁面的鏈接,它會使用產品代碼product.code作為查詢字符串。后面我們會看到models/controllers的Javascript代碼。現在看一下ProductEdit.html代碼:
<div class="container"> <h2 class="page-title">產品編輯</h2> <br/> <ul class="entity-tabular-fields"> <li class="entity-field-row"> <label>產品代碼:</label> <span class="field-control"> <input type="text" ng-model="currentProduct.code"/> <label></label> </span> </li> <li class="entity-field-row"> <label>產品名稱:</label> <span class="field-control"> <input type="text" ng-model="currentProduct.name" /> <label></label> </span> </li> <li class="entity-field-row"> <label>產品描述:</label> <span class="field-control"> <input type="text" ng-model="currentProduct.description"/> </span> <label></label> </li> <li class="entity-field-row"> <label>產品種類:</label> <span class="field-control"> <input type="text" ng-model="currentProduct.category"/> </span> <label></label> </li> <li class="entity-field-row"> <span class="field-control"> <button ng-click="saveProduct()">保存</button> </span> </li> </ul> </div>
效果圖:
注意我們給所有的input標簽加了ng-model屬性,這個將model(product)屬性和相應的UI元素綁定在一起。在這個案例中,會自動地創建一個新的叫做“currentProduct”的產品對象,並且使用用戶在文本框中輸入的值生成該對象的屬性。最后,在button標簽中添加了一個ng-click的屬性,該屬性綁定了按鈕的點擊事件saveProduct,該事件在當前的$scope中。這個$scope,你可以把它認為一個特殊的Javascript對象,我們當前的視圖所有必須的對象或者方法都綁定在$scope上,我們可以從視圖中訪問在$scope中聲明的任何東西。隨着我們繼續推進model或者controller代碼,你會更清楚的。
Javascript/Angular 部分
現在,該看看Javascript部分的代碼了。之前已經在index.html中添加了兩個AngularJS庫的引用,現在再添加一個Javascript文件,我們命名為“App.js”,並將該文件的引用添加到index頁面中。
首先,通過調用angular.module聲明我們的應用對象。
var demoApp = angular.module('demoApp', ['ngRoute']);
第一個參數“demoApp”是我們定義的模塊名稱,第二參數是我們的模塊必須要有的依賴數組,當前博客的案例中,我們只依賴“ngRoute”。
現在我們為分部視圖配置路由:
//配置路由
demoApp.config(function($routeProvider) { $routeProvider .when('/', { controller: "ProductController", templateUrl: "partials/ProductList.html" }) .when('/ProductEdit', { controller: "ProductController", templateUrl: "partials/ProductEdit.html" }) .otherwise({ redirectTo: '/' }); });
參數$routeProvider傳到路由配置的函數中,然后使用了一個鏈式的when函數來尋找url路徑,並且傳遞一個對象來定義和controller相聯系的目標視圖。Controller屬性定義了在templateUrl屬性中指定的分部視圖會使用哪一個控制器。我們的例子中只使用了一個控制器ProductController。otherwise()函數的作用是,如果沒有指定的路徑匹配到請求的url路徑,就會匹配的默認視圖。可以聯想一下switch…case…default語法。
是時候定義ProductController了,在AngularJS中,有多種方式定義控制器。我么現在使用一個controllers對象,然后把所有的controller加到這個對象中,最后將這個對象配置給我們app模塊的controllers屬性。
每一個controller都必須傳遞一個$scope參數,視圖要使用它來訪問data model中的數據。在$scope中定義的任何數據都可以在視圖中直接訪問。如果需要的話也可以使用一些其他可選的參數$route
, $routeParams
, $location
。
在這個控制器中,我們添加了和ProductFactory交互的支持函數,通過添加到$scope對象中使得這些函數在視圖中可以使用。我們用到了一個事件“$viewContentLoaded”,這個事件在分部視圖加載到div標簽中時觸發。因為我們對於添加和編輯任務都是用的ProductEdit頁面,因此我將被編輯的產品的code作為查詢字符串參數。然后在這個事件中檢測查詢參數code,如果有此參數,視圖就是編輯模式,否則就是一個創建新產品的頁面。刪除也是類似的原理,如果我們在查詢字符串參數中檢測到了code,那么就刪除產品,然后刷新列表。
你可能會注意到控制器中的另一個參數ProductFactory,這是我們需要從控制器訪問數據的服務組件。Angular框架允許用不同的方式創建服務組件。最常用的一種方式是使用工廠創建。我們需要在應用模塊中添加該服務,該應用模塊包含兩個參數:服務名稱和工廠函數。別害怕,它不過是一個返回新對象的簡單函數。

1 //定義controllers對象 2 var controllers = {}; 3 4 controllers.ProductController= function($scope,$route,$routeParams,$location,ProductFactory) { 5 $scope.products = []; 6 7 var init= function() { 8 $scope.products = ProductFactory.getProducts(); 9 } 10 11 var initProductEdit= function() { 12 var code = $routeParams.code; 13 if (code==undefined) { 14 $scope.currentProduct = {}; 15 } else { 16 $scope.currentProduct = ProductFactory.loadProductByCode(code); 17 } 18 } 19 20 $scope.$on('$viewContentLoaded', function() { 21 var tempalteUrl = $route.current.templateUrl; 22 if (tempalteUrl=="partials/ProductEdit.html") { 23 initProductEdit(); 24 }else if (tempalteUrl=="partials/ProductList.html") {//大小寫要和temlateUrl中的大小寫保持一致 25 var code = $routeParams.code; 26 if (code!=undefined) { 27 $scope.deleteProduct(code); 28 } 29 } 30 }); 31 32 init(); 33 34 $scope.saveProduct= function() { 35 ProductFactory.saveProduct($scope.currentProduct); 36 $location.search('code', null); 37 $location.path('/'); 38 } 39 40 $scope.deleteProduct= function(code) { 41 ProductFactory.deleteProduct(code); 42 $location.search('code', null); 43 $location.path('/'); 44 } 45 } 46 47 //將所有的控制器賦值給app模塊 48 demoApp.controller(controllers); 49 50 //定義工廠 51 demoApp.factory('ProductFactory', function () { 52 //初始化產品數組 53 var products = [ 54 {code:'P001',name:'Lumia 950XL',description:'win10系統最好的手機,帶有黑科技色彩',category:'mobile'}, 55 {code:'P002',name:'Lumia 950',description:'win10系統次好的手機,相比XL低個檔次',category:'mobile'}, 56 {code:'P003',name:'Surface Pro Book',description:'微軟最具創新的筆記本',category:'Notebook'}, 57 {code:'P004',name:'Surface Pro 4',description:'微軟最好的PC/平板二合一產品',category:'Surface'}, 58 { code: 'P005', name: 'Surface 4', description: '微軟次好的PC/平板二合一產品', category: 'Surface' }, 59 {code:'P006',name:'Surface Phone',description:'傳說中微軟下一代win10系統超旗艦手機',category:'mobile'} 60 ]; 61 62 var factory = {}; 63 factory.getProducts= function() { 64 return products; 65 } 66 67 factory.loadProductByCode= function(code) { 68 var productFound={}; 69 for (var i = 0; i < products.length; i++) { 70 if (products[i].code==code) { 71 productFound = products[i]; 72 break; 73 } 74 } 75 return productFound; 76 } 77 78 factory.saveProduct= function(product) { 79 var tempProduct = factory.loadProductByCode(product.code); 80 81 if (tempProduct == null || tempProduct == undefined) { 82 tempProduct = {}; 83 tempProduct.code = product.code; 84 tempProduct.name = product.name; 85 tempProduct.description = product.description; 86 tempProduct.category = product.category; 87 } else{ 88 89 tempProduct.code = product.code; 90 tempProduct.name = product.name; 91 tempProduct.description = product.description; 92 tempProduct.category = product.category; 93 94 products.push(tempProduct); 95 } 96 } 97 98 factory.deleteProduct= function(code) { 99 var tempProduct = factory.loadProductByCode(code); 100 101 if (tempProduct!=null) { 102 products.remove(tempProduct); 103 } 104 } 105 return factory; 106 });
如上面的代碼,我們使用模塊的工廠函數來定義工廠服務組件。代碼很明顯,它只包含了一些幫助屬性和方法。代碼末尾的deleteProduct函數使用了一個數組的remove函數,它不是默認的數組函數,我定義了一個函數,把它添加到Array.prototype中,使得它可以在網站中的任何地方都可以訪問。但要確保在應用加載的開始處理這個,在一些相關的代碼執行之前執行,比如我放到了index.html頁面中的script標簽中。
在工廠組件中,我們經常必須從server中存取數據,但在這篇博客中,我們只用到了臨時的內存中的產品數組。在下一篇博客中,我將從server的數據庫中存取數據。
注解:
$route:用於控制器和視圖(html分部視圖)的深度鏈接。查看更多。
$routeParams:該服務允許檢索當前路由參數的集合。查看更多。
$location:該服務分析瀏覽器地址欄中的URL(基於window.location),使得該URL對應用可用。查看更多。