Contents
×
《Pro AngularJS》学习笔记
  •  
    1 预备知识
    •  
      1.1 预备知识
    •  
      1.2 你的第一个ng应用
    •  
      1.3 在上下文中使用ng
    •  
      1.4 HTML与Bootstrap初步
    •  
      1.5 Javascript初步
    •  
      1.6 SportsStore: 一个实际应用
      •  
        1.6.1 Getting started
      •  
        1.6.2 使用伪数据
      •  
        1.6.3 显示Category列表
    •  
      1.7 SportsStore: 导航与查询
      •  
        1.7.1 工作基础
      •  
        1.7.2 使用真实的产品数据
      •  
        1.7.3 创建局部视图
      •  
        1.7.4 创建购物车
      •  
        1.7.5 添加URL导航
      •  
        1.7.6 开始结算过程
    •  
      1.8 SportsStore: 订单与管理
  •  
    2 使用AngularJS
    •  
      2.1 The Anatomy of an AngularJS App
    •  
      2.2 Using Binding and Template Directives
    •  
      2.3 Using Element and Event Directives
    •  
      2.4 Working with Forms
    •  
      2.5 Using Controllers and Scopes
    •  
      2.6 Using Filters
    •  
      2.7 创建自定义ng指令
    •  
      2.8 Creating Complex Directives
    •  
      2.9 Advanced Directive Features
  •  
    3 AngularJS的服务
    •  
      3.1 Working with Modules and Services
    •  
      3.2 Services for Global Objects, Errors, and Expressions
    •  
      3.3 Services for Ajax and Promises
    •  
      3.4 Services for REST
    •  
      3.5 Services for Views
    •  
      3.6 Services for Animation and Touch
    •  
      3.7 Services for Provision and Injection
    •  
      3.8 Unit Testing
  •  
    4 AngularJS 开发响应式布局页面
    •  
      4.1 meta 数据中的 viewpost

《Pro AngularJS》学习笔记

Keywords: AngularJS
Original published url: http://www.geiliedu.com/manual/17
Shared: Visits: 1361 Created at: 2016.01.06 Updated at: 2016.02.05 Author: geiliedu(282055808@qq.com)

1 预备知识

1.1 预备知识

浏览器:使用本书用Google Chrome,安装Batarang AngularJS扩展。

编辑器:任何文本文件编辑器或者IDE软件均可。

安装Node.js:译者的环境是ubuntu12.04,采用包管理器安装方法。同时会安装npm,node.js包管理器。

以下是译者的安装方法:

# curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash -
# sudo apt-get install -y nodejs

同时,使用npm包管理器,有时会碰到需要编译源码的情况,所以需要安装编译工具:

# sudo apt-get install -y build-essential

安装服务器:这里说的服务器是指用npm安装connect包。

npm install connect

书中这里的这个例子:

var connect = require('connect');
connect.createServer(
    connect.static("../angularjs")
).listen(5000);

用了connect包的static方法处理静态文件,但是近期的版本这个函数去掉了。

提示

可以用另外一个包serve-static来处理静态文件。安装serve-static很简单:npm install serve-static。然后上面的例子变成这样了:

var serveStatic = require('serve-static')
var connect = require('connect')
var http = require('http')
var app = connect()
// respond to all requests
app.use(serveStatic(__dirname+'', {'extensions':['html','js','css']}))
.use(function(req, res){
  res.end('Hello from Connect!\n');
})
//create node.js http server and listen on port
http.createServer(app).listen(3000)

安装测试系统包npm install -g karma,具体使用在Unit Testing章节中说明。

建目录并下载ng相关文件

  • 下载ng文件,除了angular.js文件,还需要如下的ng扩展库:

    File Description Used in Chapter
    angular-touch.js Provides touchscreen event support. 23
    angular-animate.js Provides animations when content changes. 23
    angular-mocks.js Provides mock objects for unit testing. 27
    angular-route.js Provides URL routing. 21
    angular-sanitize.js Provides escaping for dangerous content. 19
    angular-locale-fr-fr.js Provides localization details for French as it is spoken in France. 14

  • 下载Bootstrap:bootstrap.css和bootstrap-theme.css (本书不需要bootstrap的js文件,因为它和ng没有关系。)

  • 安装Deploydnpm install deployd -g。Deployd是构建web api快速原型的首选服务器。

    注意: Deployd依赖于Mongodb,所以最好先安装mongodb: sudo apt-get install mongodb

1.2 你的第一个ng应用

(省略,可以直接跳到SportsStore: 一个实际应用)

1.3 在上下文中使用ng

(省略,可以直接跳到SportsStore: 一个实际应用)

1.4 HTML与Bootstrap初步

(coming soon…)

1.5 Javascript初步

(coming soon…)

1.6 SportsStore: 一个实际应用

1.6.1 Getting started

  • 创建deplyd应用并启动该应用服务
建Deployd应用:
dpd create sportsstore
启动Deployd服务:
dpd –p 5500 sportsstore\app.dpd
输入
dashboard
  • 用浏览器打开 pic

  • 增加resources

点击绿色的‘+’号按钮,选择Collection,在弹出框中填/products,点击创建。

点击PROPERTIES,然后在属性定义框中增加如下字段:

Name Type Required?
name string Yes
description string Yes
category string Yes
price number Yes

  • **添加数据***

点击DATA,然后在数据输入框中填入如下数据:

Name Description Category Price
Kayak A boat for one person Watersports 275
Lifejacket Protective and fashionable Watersports 48.95
Soccer Ball FIFA-approved size and weight Soccer 19.5
Corner Flags Give your playing field a professional touch Soccer 34.95
Stadium Flat-packed 35,000-seat stadium Soccer 79500.00
Thinking Cap Improve your brain efficiency by 75% Chess 16
Unsteady Chair Secretly give your opponent a disadvantage Chess 29.95
Human Chess Board A fun game for the family Chess 75
Bling-Bling King Gold-plated, diamond-studded King Chess 1200

  • 浏览器查看数据

在浏览器中输入地址:http://otarelease.vfyh.com:5500/products,将看到如下数据(注:地址是译者的电脑,读者需自行更改。另下列数据是经格式化处理的,原始返回数据是连在一起的):

[
  {
    "category": "Watersports", 
    "description": "A boat for one person", 
    "name": "Kayak", 
    "price": 275, 
    "id": "736dde57db19a8f0"
  }, 
  {
    "category": "Watersports", 
    "description": "Protective and fashionable", 
    "name": "Lifejacket", 
    "price": 48.95, 
    "id": "482395efdb7709ba"
  }, 
  {
    "category": "Soccer", 
    "description": "FIFA-approved size and weight", 
    "name": "Soccer Ball", 
    "price": 19.5, 
    "id": "c30119e9cae49859"
  }, 
  {
    "category": "Soccer", 
    "description": "Give your playing field a professional touch", 
    "name": "Corner Flags", 
    "price": 34.95, 
    "id": "088ddd20c7c0d820"
  }, 
  {
    "category": "Soccer", 
    "description": "Flat-packed 35,000-seat stadium", 
    "name": "Stadium", 
    "price": 79500, 
    "id": "943b4bb7420048f7"
  }, 
  {
    "category": "Chess", 
    "description": "Improve your brain efficiency by 75%", 
    "name": "Thinking Cap", 
    "price": 16, 
    "id": "c5ed02332e5d0876"
  }, 
  {
    "category": "Chess", 
    "description": "Secretly give your opponent a disadvantage", 
    "name": "Unsteady Chair", 
    "price": 29.95, 
    "id": "5d071492e859685e"
  }, 
  {
    "category": "Chess", 
    "description": "A fun game for the family ", 
    "name": "Human Chess Board", 
    "price": 75, 
    "id": "afc1326104d8cbbc"
  }, 
  {
    "category": "Chess", 
    "description": "Gold-plated, diamond-studded King", 
    "name": "Bling-Bling King", 
    "price": 1200, 
    "id": "5f24bcd863935af4"
  }
]
  • 创建ng应用的目录: 创建的目录如下表所示。

    Name Description
    components Contains self-contained custom AngularJS components.
    controllers Contains the application’s controllers. I describe controllers in Chapter 13.
    filters Contains custom filters. I describe filters in depth in Chapter 14.
    ngmodules Contains optional AngularJS modules. I describe the optional modules throughout this book and will give references for each of them as I apply them to the SportsStore application.
    views Contains the partial views for the SportsStore application. Views contain a mix of directives and filters, which I described in Chapters 10–17.

  • 拷贝相关文件,使之如下图所示:

pic

  • 创建app.html,这是本项目的骨架,代码如下:
    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
            angular.module("sportsStore", []);
        </script>
    </head>
    <body>
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
    </div>
    <div class="panel panel-default row">
        <div class="col-xs-3">
            Categories go here
    </div>
    <div class="col-xs-8">
        Products go here
    </div>
    </div>
    </body>
    </html>

此时从浏览器上看app.html内容,如下图所示: pic

此时的node.js的应用server.js是这样写的:

var serveStatic = require('serve-static')
var connect = require('connect')
var http = require('http')
var app = connect()
// respond to all requests
app.use(serveStatic(__dirname+'/ngapp-sportsstore', {'extensions':['html','js','css']}))
.use(function(req, res){
  res.end('Hello from Connect!\n');
})
//create node.js http server and listen on port
http.createServer(app).listen(5000)

node.js的启动命令还是和第一章一样的:# node server.js

注意: 我们在这里使用的两个服务器的端口是不一样的。node.js的是5000, deployd的是5500。

1.6.2 使用伪数据

  • 创建控制器controllers/sportsStore.js,这个控制器将用于整个项目,所以叫做顶层控制器(top-level controller),其余的控制器只是在具体的页面中使用。 内容如下:
angular.module("sportsStore")
    .controller("sportsStoreCtrl", function ($scope) {
        $scope.data = {
            products: [
                { name: "Product #1", description: "A product",
                    category: "Category #1", price: 100 },
                { name: "Product #2", description: "A product",
                    category: "Category #1", price: 110 },
                { name: "Product #3", description: "A product",
                    category: "Category #2", price: 210 },
                { name: "Product #4", description: "A product",
                    category: "Category #3", price: 202 }]
        };
    });

注意: 这里的module方法调用没有第二个参数,表明是取出之前创建好的名称为’sportsStore’模块,而不是创建新的模块。

  • 显示产品数据

对之前的代码做下列修改

  • 引入controllers/sportsStore.js

  • body标签中用ng-controller指令,定义控制器名称,同时还表示scope的范围是整个body。

  • 在products的div中,使用ng-repeat完成data的迭代生成视图。这里的data是$scope的属性,$scope是ng重要的对象(或者说概念),属于model(模型)一类的东西,可理解为ng-controller所在的元素范围。

  • 两个花括号引起来的就是具体的数据或者属性以及过滤器等内容。

  • 其中还用到了filter(过滤器)currency,默认是美元,在后面的使用过滤器章节会说到如何本地化货币符号。

修改后的app.html如下:

    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
        angular.module("sportsStore", []);
        </script>
        <script src="controllers/sportsStore.js"></script>
    </head>
    <body ng-controller="sportsStoreCtrl">
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
    </div>
    <div class="panel panel-default row">
        <div class="col-xs-3">
        Categories go here
        </div>
        <div class="col-xs-8">
        <div class="well" ng-repeat="item in data.products">
            <h3>
            <strong>{{item.name}}</strong>
            <span class="pull-right label label-primary">
                {{item.price | currency}}
            </span>
            </h3>
            <span class="lead">{{item.description}}</span>
        </div>
        </div>
    </div>
    </body>
    </html>

现在的页面结果如下: pic

1.6.3 显示Category列表

下一步要做的是列出商品的种类(category)列显于左边,方便用户点击过滤产品。此功能的要求在左侧显示种类导航,用户点击导航后过滤出该种类的商品,更新右边的内容显示该种类的商品。

  • 创建商品种类列表

种类列表的数据,不宜使用硬编码的伪数据来实现,而要从商品列表中提取种类名称,种类在种类列表中要求唯一。我在filters目录中创建一个文件叫customFilters.js,内容如下:

angular.module("customFilters", [])
    .filter("unique", function () {
        return function (data, propertyName) {
            if (angular.isArray(data) && angular.isString(propertyName)) {
                var results = [];
                var keys = {};
                for (var i = 0; i < data.length; i++) {
                    var val = data[i][propertyName];
                    if (angular.isUndefined(keys[val])) {
                        keys[val] = true;
                        results.push(val);
                    }
                }
                return results;
            } else {
                return data;
            }
        }
    });

说明

  • 我在这里创建一个新的module的目的想说明如何在一个项目中定义和组合不同的module。并没有一个硬性的规定应该在现有的模块中添加组件还是创建新的模块。原则是可重用的模块一般是单独定义。

  • 用module对象的filter方法创建过滤器。

  • 创建种类导航链接

对app.html的修改:

  • 对’sportsStore’的修改:现在他有依赖的module,所以第二个参数变成[“customFilters”], 而不是[]。

  • 引入filters/customFilters.js文件。

  • 在导航div中用ng-click定义点击事件,用ng-repeat迭代显示种类,orderBy、unique过滤器。其中unique过滤器就是filters/customFilters.js中定义的取出商品种类的过滤器。

修改后的代码如下:

    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
        angular.module("sportsStore", ["customFilters"]);
        </script>
        <script src="controllers/sportsStore.js"></script>
        <script src="filters/customFilters.js"></script>
    </head>
    <body ng-controller="sportsStoreCtrl">
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
    </div>
    <div class="panel panel-default row">
        <div class="col-xs-3">
        <a ng-click="selectCategory()"
           class="btn btn-block btn-default btn-lg">Home</a>
        <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'"
           ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg">
            {{item}}
        </a>
        </div>
        <div class="col-xs-8">
        <div class="well" ng-repeat="item in data.products">
            <h3>
            <strong>{{item.name}}</strong>
            <span class="pull-right label label-primary">
                {{item.price | currency}}
            </span>
            </h3>
            <span class="lead">{{item.description}}</span>
        </div>
        </div>
    </div>
    </body>
    </html>

效果如下: pic

  • 种类导航点击事件

创建文件controllers/productListControllers.js,内容如下:

angular.module("sportsStore")
    .controller("productListCtrl", function ($scope, $filter) {
        var selectedCategory = null;
        $scope.selectCategory = function (newCategory) {
            selectedCategory = newCategory;
        }
        $scope.categoryFilterFn = function (product) {
            return selectedCategory == null ||
                product.category == selectedCategory;
        }
    });

其中两个方法selectCategorycategoryFilterFn

  • 使用productListCtrl控制器以及过滤商品 > app.html的修改:
  • 引入controllers/productListControllers.js

  • 在body的子元素中(导航框、商品框的父元素,class为’row’的div)使用ng-controller指明productListCtrl控制器。像这样被包含的控制器可以使用上级的$scope中的数据和方法。详情在使用controller和scope章节说明。

  • 在商品列表框中用filter过滤器指定categoryFilterFn为过滤条件。filter是个容易搞混的过滤器,详情在Using Filters章节说明。

修改完毕后的代码如下:

    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
        angular.module("sportsStore", ["customFilters"]);
        </script>
        <script src="controllers/sportsStore.js"></script>
        <script src="filters/customFilters.js"></script>
        <script src="controllers/productListControllers.js"></script>
    </head>
    <body ng-controller="sportsStoreCtrl">
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
    </div>
    <div class="panel panel-default row" ng-controller="productListCtrl">
        <div class="col-xs-3">
        <a ng-click="selectCategory()"
           class="btn btn-block btn-default btn-lg">Home</a>
        <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'"
           ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg">
            {{item}}
        </a>
        </div>
        <div class="col-xs-8">
        <div class="well" ng-repeat="item in data.products | filter:categoryFilterFn">
            <h3>
            <strong>{{item.name}}</strong>
            <span class="pull-right label label-primary">
                {{item.price | currency}}
            </span>
            </h3>
            <span class="lead">{{item.description}}</span>
        </div>
        </div>
    </div>
    </body>
    </html>

页面效果如下: pic

  • 高亮显示当前种类

以上所实现的项目,商品种类的导航链接没有显示当前选中的行。以下通过修改种类条目css的class的方式实现对其外观的修改,达到高亮显示当前条目的目的。

productListControllers.js的修改:

  • 通过module的constant方法定义当前选中类别的class。

  • 为在controller中使用这个常量,在controller的定义中要把该常量声明为一个依赖对象。

  • 在controller中添加getCategoryClass方法。该方法中判断如果selectedCategory和参数匹配,就返回上述的常量,否则就返回空串,达到高亮显示的目的。

productListControllers.js更新后的代码如下:

angular.module("sportsStore")
    .constant("productListActiveClass","btn-primary")
    .controller("productListCtrl", function ($scope, $filter) {
        var selectedCategory = null;
        $scope.selectCategory = function (newCategory) {
            selectedCategory = newCategory;
        }
        $scope.categoryFilterFn = function (product) {
            return selectedCategory == null ||
                product.category == selectedCategory;
        }
        $scope.getCategoryClass = function (category) {
            return selectedCategory == category ? productListActiveClass : "";
        }
    });

app.html的修改:

  • 在类别的a标签中添加ng-class="getCategoryClass(item)"指令即可。这里的item从上下文可知就是类别名称。

修改后的app.html代码片段:

    ...
    <div class="col-xs-3">
        <a ng-click="selectCategory()"
           class="btn btn-block btn-default btn-lg">Home</a>
        <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'"
           ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg"
           ng-class="getCategoryClass(item)" >
            {{item}}
        </a>
    </div>
    ...

效果如下: pic

  • 增加分页功能

控制器productListControllers.js修改:

  • 定义每页显示商品数常量productListPageCount

  • 控制器声明时,函数中添加对productListPageCount的依赖。

  • 初始化$scope.selectedPage和$scope.pageSize。

  • 修改selectCategory方法,变换种类后selectedPage复位为1。

  • 添加selectPage方法,实质上是更改selectedPage。

  • 添加getPageClass方法,以便高亮显示当前页的页码。

修改后的

angular.module("sportsStore")
    .constant("productListActiveClass","btn-primary")
    .constant("productListPageCount", 3)
    .controller("productListCtrl",
        function ($scope, $filter,
                  productListActiveClass,productListPageCount) {
        var selectedCategory = null;
        $scope.selectedPage = 1;
        $scope.pageSize = productListPageCount;
        $scope.selectCategory = function (newCategory) {
            selectedCategory = newCategory;
            $scope.selectedPage = 1;
        }
        $scope.selectPage = function (newPage) {
            $scope.selectedPage = newPage;
        }
        $scope.categoryFilterFn = function (product) {
            return selectedCategory == null ||
                product.category == selectedCategory;
        }
        $scope.getCategoryClass = function (category) {
            return selectedCategory == category ? productListActiveClass : "";
        }
        $scope.getPageClass = function (page) {
            return $scope.selectedPage == page ? productListActiveClass : "";
        }
    });

过滤器customFilters.js的修改:

  • 添加返回当前页商品的过滤器。

  • 添加返回页面计数数组的过滤器。

修改后的customFilters.js的代码如下:

angular.module("customFilters", [])
    .filter("unique", function () {
        return function (data, propertyName) {
            if (angular.isArray(data) && angular.isString(propertyName)) {
                var results = [];
                var keys = {};
                for (var i = 0; i < data.length; i++) {
                    var val = data[i][propertyName];
                    if (angular.isUndefined(keys[val])) {
                        keys[val] = true;
                        results.push(val);
                    }
                }
                return results;
            } else {
                return data;
            }
        }
    })
    .filter("range", function ($filter) {
        return function (data, page, size) {
            if (angular.isArray(data) && angular.isNumber(page) && angular.isNumber(size)) {
                var start_index = (page - 1) * size;
                if (data.length < start_index) {
                    return [];
                } else {
                    return $filter("limitTo")(data.splice(start_index), size);
                }
            } else {
                return data;
            }
        }
    })
    .filter("pageCount", function () {
        return function (data, size) {
            if (angular.isArray(data)) {
                var result = [];
                for (var i = 0; i < Math.ceil(data.length / size) ; i++) {
                    result.push(i);
                }
                return result;
            } else {
                return data;
            }
        }
    });

修改app.html更新视图:

  • 在迭代商品列表的ng-repeat指令处,添加过滤器range。

  • 在商品列表视图底部添加页码列表。

修改后的app.html代码片段如下:

    <div class="col-xs-8">
        <div class="well"
             ng-repeat=
                 "item in data.products | filter:categoryFilterFn | range:selectedPage:pageSize">
            <h3>
                <strong>{{item.name}}</strong>
                <span class="pull-right label label-primary">
                    {{item.price | currency}}
                </span>
            </h3>
            <span class="lead">{{item.description}}</span>
        </div>
        <div class="pull-right btn-group">
            <a ng-repeat=
                       "page in data.products | filter:categoryFilterFn | pageCount:pageSize"
               ng-click="selectPage($index + 1)" class="btn btn-default"
               ng-class="getPageClass($index + 1)">
                {{$index + 1}}
            </a>
        </div>
    </div>

效果图如下: pic

1.7 SportsStore: 导航与查询

1.7.1 工作基础

本章是在上一章SportsStore: 一个实际应用的基础上做开发的,如果你一步步跟着我们的进程照做,那么你手头已经有了基础代码,可以按本章的进程进一步做研发了。如果你手头没有代码,可到http://www.apress.com/下载。(译注:这个站点下载的方法不靠谱,根本就不到)

1.7.2 使用真实的产品数据

修改sportsStore.js:

  • 定义常量dataUrl,这个url可以取得产品数据,以json格式返回。译者本人使用的是虚拟机的deployd,在主系统(win7)中通过ajax访问,所以不能使用原文的localhost,而是http://otarelease.vfyh.com:5500/products

  • 修改controller的函数,添加$httpdataUrl的依赖。这里的$http是ng的一个service对象,用于访问服务器端api接口。和$scope类似都是ng的保留对象。

  • 更改$scope.data的定义,不再使用硬编码的数据,而是使用ajax取回的真实商品的json格式的数据。$http的get方法将返回一下promise,promise在服务器端有返回数据时回调success或者error定义的函数。如果对json不熟悉,请先暂停,马上补课。

  • error定义的函数,在服务器访问时出错时被回调,参数为ng描述错误信息的对象。详情在后续章节会描述。

更新后的sportsStore.js代码如下:

angular.module("sportsStore")
    .constant("dataUrl", "http://otarelease.vfyh.com:5500/products")
    .controller("sportsStoreCtrl", function ($scope, $http, dataUrl) {
        /*
        $scope.data = {
            products: [
                { name: "Product #1", description: "A product",
                    category: "Category #1", price: 100 },
                { name: "Product #2", description: "A product",
                    category: "Category #1", price: 110 },
                { name: "Product #3", description: "A product",
                    category: "Category #2", price: 210 },
                { name: "Product #4", description: "A product",
                    category: "Category #3", price: 202 }]
        };
        */
        $scope.data = {};
        $http.get(dataUrl)
            .success(function (data) {
                $scope.data.products = data;
            })
            .error(function (error) {
                $scope.data.error = error;
            });
    });

刷新后的页面如下图所示: pic

  • 处理ajax错误

app.html做如下处理:

  • 添加一个div显示错误,如果有错误则显示。错误信息中用ng-show指令而商品信息中用ng-hide指令,二者不会同时出现。

修改后的代码如下(因为后面要拆分app.html了,所以完整地贴一次):

    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
        angular.module("sportsStore", ["customFilters"]);
        </script>
        <script src="controllers/sportsStore.js"></script>
        <script src="filters/customFilters.js"></script>
        <script src="controllers/productListControllers.js"></script>
    </head>
    <body ng-controller="sportsStoreCtrl">
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
    </div>
    <div class="alert alert-danger" ng-show="data.error">
        Error ({{data.error.status}}). The product data was not loaded.
        <a href="/app.html" class="alert-link">Click here to try again</a>
    </div>
    <div class="panel panel-default row" ng-controller="productListCtrl" ng-hide="data.error">
        <div class="col-xs-3">
        <a ng-click="selectCategory()"
           class="btn btn-block btn-default btn-lg">Home</a>
        <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'"
           ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg"
           ng-class="getCategoryClass(item)" >
            {{item}}
        </a>
        </div>
        <div class="col-xs-8">
        <div class="well"
             ng-repeat=
             "item in data.products | filter:categoryFilterFn | range:selectedPage:pageSize">
            <h3>
            <strong>{{item.name}}</strong>
            <span class="pull-right label label-primary">
                {{item.price | currency}}
            </span>
            </h3>
            <span class="lead">{{item.description}}</span>
        </div>
        <div class="pull-right btn-group">
            <a ng-repeat=
                   "page in data.products | filter:categoryFilterFn | pageCount:pageSize"
               ng-click="selectPage($index + 1)" class="btn btn-default"
               ng-class="getPageClass($index + 1)">
            {{$index + 1}}
            </a>
        </div>
        </div>
    </div>
    </body>
    </html>

然后将sportsStore.js控制器使用的dataUrl地址改一下dployd的resource,比如改成http://otarelease.vfyh.com:5500/products_,这时刷新页面会看到如下错误: pic

1.7.3 创建局部视图

随着项目进展,视图文件会变得复杂和庞大,本节讲述把他们分开,然后用ng-include标签把他们整合的方法。

app.html的修改

  • 把产品数据部分的代码移出,放到views/productList.html
    <div class="panel panel-default row" ng-controller="productListCtrl"
         ng-hide="data.error">
        <div class="col-xs-3">
        <a ng-click="selectCategory()"
           class="btn btn-block btn-default btn-lg">Home</a>
        <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'"
           ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg"
           ng-class="getCategoryClass(item)">
            {{item}}
        </a>
        </div>
        <div class="col-xs-8">
        <div class="well"
             ng-repeat=
             "item in data.products | filter:categoryFilterFn | range:selectedPage:pageSize">
            <h3>
            <strong>{{item.name}}</strong>
            <span class="pull-right label label-primary">
                {{item.price | currency}}
            </span>
            </h3>
            <span class="lead">{{item.description}}</span>
        </div>
        <div class="pull-right btn-group">
            <a ng-repeat=
                   "page in data.products | filter:categoryFilterFn | pageCount:pageSize"
               ng-click="selectPage($index + 1)" class="btn btn-default"
               ng-class="getPageClass($index + 1)">
            {{$index + 1}}
            </a>
        </div>
        </div>
    </div>
  • 然后在app.html中引用(注意移出去的内容被ng-include标签语句替代了):
    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
        angular.module("sportsStore", ["customFilters"]);
        </script>
        <script src="controllers/sportsStore.js"></script>
        <script src="filters/customFilters.js"></script>
        <script src="controllers/productListControllers.js"></script>
    </head>
    <body ng-controller="sportsStoreCtrl">
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
    </div>
    <div class="alert alert-danger" ng-show="data.error">
        Error ({{data.error.status}}). The product data was not loaded.
        <a href="/app.html" class="alert-link">Click here to try again</a>
    </div>
    <ng-include src="'views/productList.html'"></ng-include>
    </body>
    </html>

注意

上面ng-include标签(同时又是ng-指令)中的src的值,还需要用单引号引起来,否则ng会把它当成$scope的属性了。

提示, 把view分开有如下三个好处:

  • 将应用分成更易管理的小块。

  • 分开后的view更具重用性。

  • 用户使用应用时,更方便刷新不同的区域。

1.7.4 创建购物车

购物车基本流程图pic

定义Cart模块和服务

新建components/cart/cart.js文件,该文件定义Cart模块以及服务

  • 创建components/cart目录

  • 和之前一样,angular.module方法创建一个叫’cart’的模块。

  • 这里使用了创建service的最简单的方法:调用module的factory方法。因为service是singleton(单实例模式)的,且全局可用,所以购物车使用了service方案。

  • factory的工作方式是’cart’服务被使用时,会启动它的工厂函数,工厂函数返回包含三个方法(addProduct(id, name, price), removeProduct(id)getProducts())的对象。

cart.js的代码如下:

angular.module("cart", [])
    .factory("cart", function () {
        var cartData = [];
        return {
            addProduct: function (id, name, price) {
                var addedToExistingItem = false;
                for (var i = 0; i < cartData.length; i++) {
                    if (cartData[i].id == id) {
                        cartData[i].count++;
                        addedToExistingItem = true;
                        break;
                    }
                }
                if (!addedToExistingItem) {
                    cartData.push({
                        count: 1, id: id, price: price, name: name
                    });
                }
            },
            removeProduct: function (id) {
                for (var i = 0; i < cartData.length; i++) {
                    if (cartData[i].id == id) {
                        cartData.splice(i, 1);
                        break;
                    }
                }
            },
            getProducts: function () {
                return cartData;
            }
        }
    });

创建Cart挂件

更新components/cart/cart.js:

  • 提示: 这个挂件(widget)可理解为一小块相对独立的view;通常使用自定义指令的方式实现。

  • 自定义ng指令通过module的directive方法创建,指令名称通过第一个参数传入,第二个参数为工厂函数。该工厂函数返回指令定义对象,注意它依赖于cart服务,因为它的控制器需要通过cart.getProducts()得到购物车的商品数据。cartSummary指令用到了三个属性,说明如下表:

Name Description
restrict 指明该指令如何用。我给它赋值为E,表示仅可以用作元素。最常用的是EA,表示可以用作元素或属性。
templateUrl 指明了局部视图的URL,该局部视图将会插入到指令的元素中。
controller 定义为局部视图提供数据和方法的控制器。

更新后的components/cart/cart.js内容如下:

angular.module("cart", [])
    .factory("cart", function () {
        var cartData = [];
        return {
            addProduct: function (id, name, price) {
                var addedToExistingItem = false;
                for (var i = 0; i < cartData.length; i++) {
                    if (cartData[i].id == id) {
                        cartData[i].count++;
                        addedToExistingItem = true;
                        break;
                    }
                }
                if (!addedToExistingItem) {
                    cartData.push({
                        count: 1, id: id, price: price, name: name
                    });
                }
            },
            removeProduct: function (id) {
                for (var i = 0; i < cartData.length; i++) {
                    if (cartData[i].id == id) {
                        cartData.splice(i, 1);
                        break;
                    }
                }
            },
            getProducts: function () {
                return cartData;
            }
        }
    })
    .directive("cartSummary", function (cart) {
        return {
            restrict: "E",
            templateUrl: "components/cart/cartSummary.html",
            controller: function ($scope) {
                var cartData = cart.getProducts();
                $scope.total = function () {
                    var total = 0;
                    for (var i = 0; i < cartData.length; i++) {
                        total += (cartData[i].price * cartData[i].count);
                    }
                    return total;
                }
                $scope.itemCount = function () {
                    var total = 0;
                    for (var i = 0; i < cartData.length; i++) {
                        total += cartData[i].count;
                    }
                    return total;
                }
            }
        };
    });
  • 后面的创建自定义ng指令会详细说明如何自定义ng指令。

  • components/cart/cartSummary.html 的内容如下:

    <style>
        .navbar-right { float: right !important; margin-right: 5px;}
        .navbar-text { margin-right: 10px; }
    </style>
    <div class="navbar-right">
        <div class="navbar-text">
        <b>Your cart:</b>
        {{itemCount()}} item(s),
        {{total() | currency}}
        </div>
        <a class="btn btn-default navbar-btn">Checkout</a>
    </div>

使用Cart挂件

app.html的修改

  • sportsStore模块添加对cart挂件的依赖。

  • 增加script标签引入components/cart/cart.js文件。

  • cart-summary指令作为标签添加到脚本。注:ng对驼峰命名的指令名称做了规范化,即第一个字母小写开始,之后的大写字母变小写并在该字母前加短横。

修改完毕的app.html代码如下:

    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
        angular.module("sportsStore", ["customFilters", "cart"]);
        </script>
        <script src="controllers/sportsStore.js"></script>
        <script src="filters/customFilters.js"></script>
        <script src="controllers/productListControllers.js"></script>
        <script src="components/cart/cart.js"></script>
    </head>
    <body ng-controller="sportsStoreCtrl">
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
        <cart-summary />
    </div>
    <div class="alert alert-danger" ng-show="data.error">
        Error ({{data.error.status}}). The product data was not loaded.
        <a href="/app.html" class="alert-link">Click here to try again</a>
    </div>
    <ng-include src="'views/productList.html'"></ng-include>
    </body>
    </html>

此时刷新页面后如下图所示: pic

添加将商品加入到购物车的按钮

controllers/productListController.js的修改

  • 控制器中添加对cart服务器的依赖。

  • 增加addProductToCart方法。

修改后的代码如下:

angular.module("sportsStore")
    .constant("productListActiveClass","btn-primary")
    .constant("productListPageCount", 3)
    .controller("productListCtrl",
        function ($scope, $filter,
                  productListActiveClass,productListPageCount, cart) {
        var selectedCategory = null;
        $scope.selectedPage = 1;
        $scope.pageSize = productListPageCount;
        $scope.selectCategory = function (newCategory) {
            selectedCategory = newCategory;
            $scope.selectedPage = 1;
        }
        $scope.selectPage = function (newPage) {
            $scope.selectedPage = newPage;
        }
        $scope.categoryFilterFn = function (product) {
            return selectedCategory == null ||
                product.category == selectedCategory;
        }
        $scope.getCategoryClass = function (category) {
            return selectedCategory == category ? productListActiveClass : "";
        }
        $scope.getPageClass = function (page) {
            return $scope.selectedPage == page ? productListActiveClass : "";
        }
        $scope.addProductToCart = function (product) {
            cart.addProduct(product.id, product.name, product.price);
        }
    });

views/productList.html的修改:

  • 商品详情处添加按钮,点击事件调用addProductToCart方法,参数为商品。
    ...
        <div class="well"
             ng-repeat=
                 "item in data.products | filter:categoryFilterFn | range:selectedPage:pageSize">
            <h3>
                <strong>{{item.name}}</strong>
                <span class="pull-right label label-primary">
                    {{item.price | currency}}
                </span>
            </h3>
            <button ng-click="addProductToCart(item)"
                    class="btn btn-success pull-right">
                Add to cart
            </button>
            <span class="lead">{{item.description}}</span>
        </div>
    ...

然后就可以将商品添加到购物车了,如下图所示: pic

1.7.5 添加URL导航

URL导航的作用是根据当前的URL自动地显示各个局部视图,使得构建大型的应用更加容易,用户可自由地四处导航。

定义URL路由

app.html的修改:

  • script标签引入ngmodules/angular-route.js。关于创建带provider的service的方法在Working with Modules and Services章节中详述。关于$route服务的详细用法在Services for Views中详述。

  • sportsStore模块创建时申明依赖ngRoute模块,ngRoute模块在angular-route.js中定义。

  • 用模块的config方法定义路由。config方法的参数为一函数,此函数以$routeProvider为参数。$routeProvider是$route服务的provider,用于设置URL路由。本例设置的路由为:

URL Effect
http://localhost:5000/app.html#/checkout Displays the checkoutSummary.html view
http://localhost:5000/app.html#/products Displays the productList.html view
http://localhost:5000/app.html#/other Displays the productList.html view (because of the fallback route defined by the otherwise method)
http://localhost:5000/app.html Displays the productList.html view (because of the fallback route defined by the otherwise method)
  • 通过ng-view指令显示当前路由的视图,也即根据路由插入局部视图。

修改后的app.html代码如下:

    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
        angular.module("sportsStore", ["customFilters", "cart","ngRoute"])
            .config(function($routeProvider){
                $routeProvider.when("/checkout", {
                templateUrl: "/views/checkoutSummary.html"
                });
                $routeProvider.when("/products", {
                templateUrl: "/views/productList.html"
                });
                $routeProvider.otherwise({
                templateUrl: "/views/productList.html"
                });
            });
        </script>
        <script src="controllers/sportsStore.js"></script>
        <script src="filters/customFilters.js"></script>
        <script src="controllers/productListControllers.js"></script>
        <script src="components/cart/cart.js"></script>
        <script src="ngmodules/angular-route.js"></script>
    </head>
    <body ng-controller="sportsStoreCtrl">
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
        <cart-summary />
    </div>
    <div class="alert alert-danger" ng-show="data.error">
        Error ({{data.error.status}}). The product data was not loaded.
        <a href="/app.html" class="alert-link">Click here to try again</a>
    </div>
    <ng-view />
    </body>
    </html>

使用URL路由导航

添加views/checkoutSummary.html文件,框架代码如下:

    <div class="lead">
        This is the checkout summary view
    </div>
    <a href="#/products" class="btn btn-primary">Back</a>

components/cart/cartSummary.html中的a标签加上href属性:

    <style>
        .navbar-right { float: right !important; margin-right: 5px;}
        .navbar-text { margin-right: 10px; }
    </style>
    <div class="navbar-right">
        <div class="navbar-text">
        <b>Your cart:</b>
        {{itemCount()}} item(s),
        {{total() | currency}}
        </div>
        <a href="#/checkout" class="btn btn-default navbar-btn">Checkout</a>
    </div>

经以上修改,页面点击Checkout按钮后得到如下图所示页面: pic

提示: 网址中的#之后的路由,具有锚点的一些特征,如果#删除后刷新,则重新加载app.html页面,等于是应用重新初始化,如果#后面的路由没有匹配,则使用默认的路由。读者可以手动在浏览器的地址栏修改url然后刷新测试一下。

1.7.6 开始结算过程

添加controllers/checkoutControllers.js,为sportsStore模块添加cartSummaryController控制器,该控制器可以使在结算页面中显示购物车内容,导航至订单页面或者继续购物。

  • 该控制器依赖cart服务,通过cart.getProducts()方法取到购物车中的商品信息。

  • 该控制器定义计算总金额的方法total,以及购物车中移除商品的方法remove。

代码如下:

angular.module("sportsStore")
    .controller("cartSummaryController", function($scope, cart) {
        $scope.cartData = cart.getProducts();
        $scope.total = function () {
            var total = 0;
            for (var i = 0; i < $scope.cartData.length; i++) {
                total += ($scope.cartData[i].price * $scope.cartData[i].count);
            }
            return total;
        }
        $scope.remove = function (id) {
            cart.removeProduct(id);
        }
    });

替换views/checkoutSummary.html的内容:

  • ng-controller指明cartSummaryController的scope。

  • ng-show,ng-hide根据购物车为空显示和隐藏不同的内容。

  • ng-repeat与table中迭代显示购物车中的商品

  • 使用URL路由添加导航链接

新的代码如下所示:

    <h2>Your cart</h2>
    <div ng-controller="cartSummaryController">
        <div class="alert alert-warning" ng-show="cartData.length == 0">
        There are no products in your shopping cart.
        <a href="#/products" class="alert-link">Click here to return to the catalogue</a>
        </div>
        <div ng-hide="cartData.length == 0">
        <table class="table">
            <thead>
            <tr>
            <th>Quantity</th>
            <th>Item</th>
            <th class="text-right">Price</th>
            <th class="text-right">Subtotal</th>
            </tr>
            </thead>
            <tbody>
            <tr ng-repeat="item in cartData">
            <td class="text-center">{{item.count}}</td>
            <td class="text-left">{{item.name}}</td>
            <td class="text-right">{{item.price | currency}}</td>
            <td class="text-right">{{ (item.price * item.count) | currency}}</td>
            <td>
                <button ng-click="remove(item.id)"
                    class="btn btn-sm btn-warning">Remove</button>
            </td>
            </tr>
            </tbody>
            <tfoot>
            <tr>
            <td colspan="3" class="text-right">Total:</td>
            <td class="text-right">
                {{total() | currency}}
            </td>
            </tr>
            </tfoot>
        </table>
        <div class="text-center">
            <a class="btn btn-primary" href="#/products">Continue shopping</a>
            <a class="btn btn-primary" href="#/placeorder">Place order now</a>
        </div>
        </div>
    </div>

修改app.html

  • 引入controllers/checkoutControllers.js

  • 添加complete和placeorder路由

修改后的app.html代码如下:

    <!DOCTYPE html>
    <html ng-app="sportsStore">
    <head>
        <title>SportsStore</title>
        <script src="angular.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
        angular.module("sportsStore", ["customFilters", "cart","ngRoute"])
            .config(function($routeProvider){
                $routeProvider.when("/complete", {
                templateUrl: "/views/thankYou.html"
                });
                $routeProvider.when("/placeorder", {
                templateUrl: "/views/placeOrder.html"
                });
                $routeProvider.when("/checkout", {
                templateUrl: "/views/checkoutSummary.html"
                });
                $routeProvider.when("/products", {
                templateUrl: "/views/productList.html"
                });
                $routeProvider.otherwise({
                templateUrl: "/views/productList.html"
                });
            });
        </script>
        <script src="controllers/sportsStore.js"></script>
        <script src="filters/customFilters.js"></script>
        <script src="controllers/productListControllers.js"></script>
        <script src="components/cart/cart.js"></script>
        <script src="ngmodules/angular-route.js"></script>
        <script src="controllers/checkoutControllers.js"></script>
    </head>
    <body ng-controller="sportsStoreCtrl">
    <div class="navbar navbar-inverse">
        <a class="navbar-brand" href="#">SPORTS STORE</a>
        <cart-summary />
    </div>
    <div class="alert alert-danger" ng-show="data.error">
        Error ({{data.error.status}}). The product data was not loaded.
        <a href="/app.html" class="alert-link">Click here to try again</a>
    </div>
    <ng-view />
    </body>
    </html>

至此,页面Checkout页面更新为下图所示的样子: pic

1.8 SportsStore: 订单与管理

2 使用AngularJS

2.1 The Anatomy of an AngularJS App

2.2 Using Binding and Template Directives

2.3 Using Element and Event Directives

2.4 Working with Forms

2.5 Using Controllers and Scopes

2.6 Using Filters

2.7 创建自定义ng指令

2.8 Creating Complex Directives

2.9 Advanced Directive Features

3 AngularJS的服务

3.1 Working with Modules and Services

3.2 Services for Global Objects, Errors, and Expressions

3.3 Services for Ajax and Promises

3.4 Services for REST

3.5 Services for Views

3.6 Services for Animation and Touch

3.7 Services for Provision and Injection

3.8 Unit Testing

4 AngularJS 开发响应式布局页面

4.1 meta 数据中的 viewpost

<!DOCTYPE html>
<html lang="en" ng-app="profileApp">
       <head>
           <meta charset="UTF-8">
           <meta name="viewport" content="width=device-width, initial-
   scale=1, maximum-scale=1, user-scalable=YES">
           <title>
               Responsive Profile Web Application
           </title>
           <link href='http://fonts.googleapis.com/css?family=Robo
   to:400,100,300,500,700' rel='stylesheet' type='text/css'>
           <link ng-if="styleType.length > 0"
                 ng-href='assets/css/{{styleType}}.css'
                 rel='stylesheet' type='text/css' >
       </head>
       <body>
           <!--AngularJS view -->
           <div ng-view="">
           </div>
           <!--external scripts start -->
           <script  src="assets/lib/angular.min.js"></script>
           <script  src="assets/lib/angular-route.min.js"></script>
           <script  src="assets/lib/angular-resource.min.js"></script>
           <script  src="assets/script/deviceTypeProvider.js"></script>
           <script  src="assets/script/profileController.js"></script>
           <script  src="assets/script/profileService.js"></script>
           <script  src="assets/script/app.js"></script>
           <!--external scripts end -->
       </body>
</html>

这段代码要关注<meta name "viewport"...这行代码,以及ng-href这行。viewport 指明了页面宽度为设备的宽度,css3中以此支持响应式布局。ng-href 这行为不同的设备加载不同的 css 文件。