1.1 Introduction

2016-06-27 00:39:47 1,390 1

说明:本教程基于angularJS 1.5.8翻译

本节通过一个简单的案例介绍AngularJS的所有核心概念。更加深入的讲解参见后面的章节。

概念说明
模板(Template)带有Angular扩展标记的HTML
指令(Directive)用于通过自定义属性和元素扩展HTML的行为
模型(Model)用于显示给用户并且与用户互动的数据
作用域(Scope)用来存储模型(Model)的语境(context)。模型放在这个语境中才能被控制器、指令和表达式等访问到
表达式(Expression)模板中可以通过它来访问作用域(Scope)中的变量和函数
编译器(Compiler)用来编译模板(Template),并且对其中包含的指令(Directive)和表达式(Expression)进行实例化
过滤器(Filter)负责格式化表达式(Expression)的值,以便呈现给用户
视图(View)用户看到的内容(即DOM
数据绑定(Data Binding)自动同步模型(Model)中的数据和视图(View)表现
控制器(Controller)视图(View)背后的业务逻辑
依赖注入(Dependency Injection)负责创建和自动装载对象或函数
注入器(Injector)用来实现依赖注入(Injection)的容器
模块(Module)用来配置注入器
服务(Service)独立于视图(View)的、可复用的业务逻辑

第一个案例:Data binding

在接下来的案例中,我们将会构建一个表单,用于计算发票不同货币形式的价格。首先我们要完成输入发票的数量和价格,计算出其总价的功能。

<!doctype html>
<html lang="en">
<head>
     <meta charset="UTF-8">
     <title>Example - example-guide-concepts-1-production</title>
     <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<body >
<div ng-app ng-init="qty=1;cost=2">
     <b>Invoice:</b>
     <div>
         Quantity: <input type="number" min="0" ng-model="qty">
     </div>
     <div>
         Costs: <input type="number" min="0" ng-model="cost">
     </div>
     <div>
         <b>Total:</b> {{qty * cost | currency}}
     </div>
</div>
</body></html>

Image.png

当我们改变输入框中的值时,会发现总价Total实时的随之进行变化。

让我们来看看内部发生了什么?

首先代码看起来就像正常的HTML,但是多了一些新的标记(markup)。在AngularJS中,这样的一个文件称之为模板(template)。当angularJS启动时,会通过HTML编译器(Compiler)来解析和处理这些新的标记。经过加载、转变和渲染之后的DOM称之为视图(view)。

在AngularJS中,我们在HTML中添加的新的标记大致可以分为2类:指令(Directive)和表达式(Expression)。

1、指令:指令指的是我们可以在HTML文件中为已有的元素添加新的属性,或者直接添加新的元素。AngularJS会为我们解析这些属性或者元素来实现特定的功能。在上面这个案例中,我们使用了ng-app属性,这个指令可以帮助我们自动初始化我们的应用。AngularJS为input元素也定义了一些指令来添加额外的功能。ng-model指令存储/更新输入表单的值。

2、表达式:表达式的完整语法为{{ expression | filter }},当编译器遇到这个语法时,将会将其替换为表达式计算后的值。在模板中的表达式有点类似于JavaScript的代码片段,允许AngularJS读写变量的值。需要注意的是,这些变量并不是全局变量。就像JavaScript函数中的变量是有作用域的,AngularJS提供了一个scope对象让我们来访问变量。在某个作用域scope上存储的变量称之为model,在作用域的有效范围内都可以使用。在上面这个案例中,表达式的作用是让AngularJS读取输入项中的值并将其相乘。

在上面这个案例中,表达式中还使用了过滤器(filter)。filter的作用是格式化显示给用看查看的表达式的值。具体到案例中的currency,将数字格式化为货币的方式显示。

上述案例还隐含了一个重要的信息,Angular提供实时的绑定。不论何时输入项中的值发生了变化,表达式中的值都会被重新计算,DOM中显示的内容会改为更新后的值。这个概念称之为双向数据绑定(two-way data binding.)。

最后我们用一张图来串联起来上述概念:

concepts-databinding1.png

提示:

     通过自定义指令来访问DOM:在Angular中,应用唯一应该访问DOM的地方是通过指令。这很重要,因为对DOM的访问很难进行测试。如果你需要直接访问DOM,那么你应该通过自定义指令来实现。在指令章节,我们将会介绍如何做。

添加UI逻辑:Controller
让我们在上面的案例中添加一个逻辑,从而允许以不同的货币形式显示发票的花费。

html

<!doctype html>
<html lang="en">
<head>
     <meta charset="UTF-8">
     <title>Example - example-guide-concepts-2-production</title>
     <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
     <script src="invoice1.js"></script>
</head>
<body >
<div ng-app="invoice1" ng-controller="InvoiceController as invoice">
     <b>Invoice:</b>
     <div>
         Quantity: <input type="number" min="0" ng-model="invoice.qty" required >
     </div>
     <div>
         Costs: <input type="number" min="0" ng-model="invoice.cost" required >
         <select ng-model="invoice.inCurr">
             <option ng-repeat="c in invoice.currencies">{{c}}</option>
         </select>
     </div>
     <div>
         <b>Total:</b>
     <span ng-repeat="c in invoice.currencies">
       {{invoice.total(c) | currency:c}}
     </span>
         <button class="btn" ng-click="invoice.pay()">Pay</button>
     </div>
</div>
</body>
</html>

invoice1.js

angular.module('invoice1', [])
    .controller('InvoiceController', function() {
        this.qty = 1;
        this.cost = 2;
        this.inCurr = 'EUR';
        this.currencies = ['USD', 'EUR', 'CNY'];
        this.usdToForeignRates = {
            USD: 1,//以美元作为基准
            EUR: 0.74,//欧元
            CNY: 6.09 //人民币
        };

        this.total = function total(outCurr) {
            return this.convertCurrency(this.qty * this.cost, this.inCurr, outCurr);
        };
        this.convertCurrency = function convertCurrency(amount, inCurr, outCurr) {
            return amount * this.usdToForeignRates[outCurr] / this.usdToForeignRates[inCurr];
        };
        this.pay = function pay() {
            window.alert("Thanks!");
        };
    });

显示效果

QQ截图20160627002746.png

改变了什么?

首先有了一个新的js文件,包含了一个控制器(controller)。更详细的,这个文件包含了一个构造器函数(  constructor function ,js中的概念,自行查阅)用来创建实际上的controller实例。使用controller的目的是对表达式和指令(二者位于模板中)暴露变量和函数(二者位于js代码中)。

除了在新的js文件中我们添加了controller的代码,在HTML中我们添加了一个指令ng-controller。这个指令告诉Angular新的 InvoiceController 负责位于这个指令内部的HTML元素的处理。语法 InvoiceController as invoice告诉Angular实例化controller,并存储在当前scope的invoice变量中。

我们同样改变了页面中的表达式,通过controller来读写变量的值,这是通过在ng-model属性值加上前缀"invoice."实现的。可能的货币形式定义在controller中,并且通过ng-repeat指令添加到模板中。由于controller含有一个total函数,我们同样可以通过语法 {{ invoice.total(...) }}绑定这个函数的结果到dom中。

数据绑定依然是实时的,无论何时函数的计算结果发生了变化,DOM中的显示将会自动更新。

支付按钮使用了指令ng-click,点击得到对应的提示。

在js文件中,我们还创建了一个模块(module)对象,controller在module中进行注册。在后面,我们将会详细的介绍module。下图展示了所有上面提到的这些是如何联合在一起工作的。

concepts-databinding2.png

与视图相独立的业务逻辑:Services

目前,InvoiceController包含了案例中的所有逻辑。当应用不断变得庞大时,将controller中与视图无关的逻辑移到一个服务(service)是比较好的做法,这样它就可以被应用中的其他部分所复用。在后面,我们将会改变这个服务通过网络来加载汇率,通过调用  Yahoo Finance API。而不需要改动controller的代码。

现在我们来重构案例中的代码,移动汇率转换的代码到另外一个文件中。

html

<!doctype html>
<html lang="en">
<head>
     <meta charset="UTF-8">
     <title>Example - example-guide-concepts-21-production</title>
     <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
     <script src="finance2.js"></script>
     <script src="invoice2.js"></script>
</head>
<body >
<div ng-app="invoice2" ng-controller="InvoiceController as invoice">
     <b>Invoice:</b>
     <div>
         Quantity: <input type="number" min="0" ng-model="invoice.qty" required >
     </div>
     <div>
         Costs: <input type="number" min="0" ng-model="invoice.cost" required >
         <select ng-model="invoice.inCurr">
             <option ng-repeat="c in invoice.currencies">{{c}}</option>
         </select>
     </div>
     <div>
         <b>Total:</b>
     <span ng-repeat="c in invoice.currencies">
       {{invoice.total(c) | currency:c}}
     </span>
         <button class="btn" ng-click="invoice.pay()">Pay</button>
     </div>
</div>
</body>
</html>

invoice2.js

angular.module('invoice2', ['finance2'])
    .controller('InvoiceController', ['currencyConverter', function(currencyConverter) {
        this.qty = 1;
        this.cost = 2;
        this.inCurr = 'EUR';
        this.currencies = currencyConverter.currencies;

        this.total = function total(outCurr) {
            return currencyConverter.convert(this.qty * this.cost, this.inCurr, outCurr);
        };
        this.pay = function pay() {
            window.alert("Thanks!");
        };
    }]);

finance2.js

angular.module('finance2', [])
    .factory('currencyConverter', function() {
        var currencies = ['USD', 'EUR', 'CNY'];
        var usdToForeignRates = {
            USD: 1,
            EUR: 0.74,
            CNY: 6.09
        };
        var convert = function (amount, inCurr, outCurr) {
            return amount * usdToForeignRates[outCurr] / usdToForeignRates[inCurr];
        };

        return {
            currencies: currencies,
            convert: convert
        };
    });

显示结果

QQ截图20160627002746.png

改变了什么?

我们移动了 convertCurrency 函数和已有的货币形式到新的文件finance2.js。但是controller现在如何使用一个与之独立的函数呢?

依赖注入(Dependency Injection)的概念开始发挥作用了。依赖注入(DI)是一种软件设计模式,用来处理对象和函数如何创建以及获取它们的依赖。Angular中的所有概念(directives, filters, controllers, services, ...)的创建和使用都是通过依赖注入。在Angular中,DI容器称之为injector。

为了使用DI,需要有一个地方来注册所有应该联合在一起工作的组件。这就是module出现的原因,当Angular启动的时候,将会使用ng-app指令定义的模块,以及这个模块依赖的所有其他模块。

在上面这个案例中,模板包含指令 ng-app="invoice2",这告诉Angular使用invoice2模块作为应用的主模块(main module)。代码片段 angular.module('invoice2', ['finance2'])指定invoice2模块依赖于finance2模块。通过这种方式,Angular同时使用 InvoiceController currencyConverter 服务。

回到开始的问题:InvoiceController如何获取到currencyConvertor函数的一个引用?在Angular中,这简单的通过在构造器函数中定义一个参数实现。通过这种方式,injector可以按照正确的顺序创建对象并且将之前创建的对象传递到依赖于其的其他对象上。在我们的案例中,InvoiceController有一个参数称为 currencyConverter,通过这种方式,Angular知道controller和service之间的依赖关系,并且将service的实例传递给controller。

案例中的最后一个改变是,我们现在传递一个数组给 module.controller函数,而不是一个纯函数。数组首先包含controller依赖的服务的名字。数组的最后一个元素是controller的构造器函数。Angular使用这种数组语法来定义依赖,从而使用即使简化了代码之后DI依然可以正常工作,最有可能发生的情况是重命名controller构造器函数参数的名称,例如使用a。

图解:

concepts-module-service.png

访问后端

让我们通过从Yahoo Finance API来获取汇率的方式来结束我们的案例。下面的案例演示了这是如何完成的。

<!doctype html>
<html lang="en">
<head>
     <meta charset="UTF-8">
     <title>Example - example-guide-concepts-3-production</title>
     <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
     <script src="invoice3.js"></script>
     <script src="finance3.js"></script>
</head>
<body >
<div ng-app="invoice3" ng-controller="InvoiceController as invoice">
     <b>Invoice:</b>
     <div>
         Quantity: <input type="number" min="0" ng-model="invoice.qty" required >
     </div>
     <div>
         Costs: <input type="number" min="0" ng-model="invoice.cost" required >
         <select ng-model="invoice.inCurr">
             <option ng-repeat="c in invoice.currencies">{{c}}</option>
         </select>
     </div>
     <div>
         <b>Total:</b>
     <span ng-repeat="c in invoice.currencies">
       {{invoice.total(c) | currency:c}}
     </span>
         <button class="btn" ng-click="invoice.pay()">Pay</button>
     </div>
</div>
</body>
</html>

invoice3.js

angular.module('invoice3', ['finance3'])
    .controller('InvoiceController', ['currencyConverter', function(currencyConverter) {
        this.qty = 1;
        this.cost = 2;
        this.inCurr = 'EUR';
        this.currencies = currencyConverter.currencies;

        this.total = function total(outCurr) {
            return currencyConverter.convert(this.qty * this.cost, this.inCurr, outCurr);
        };
        this.pay = function pay() {
            window.alert("Thanks!");
        };
    }]);

finance3.js

angular.module('finance3', [])
    .factory('currencyConverter', ['$http', function($http) {
        var YAHOO_FINANCE_URL_PATTERN =
            '//query.yahooapis.com/v1/public/yql?q=select * from '+
            'yahoo.finance.xchange where pair in ("PAIRS")&format=json&'+
            'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
        var currencies = ['USD', 'EUR', 'CNY'];
        var usdToForeignRates = {};

        var convert = function (amount, inCurr, outCurr) {
            return amount * usdToForeignRates[outCurr] / usdToForeignRates[inCurr];
        };

        var refresh = function() {
            var url = YAHOO_FINANCE_URL_PATTERN.
            replace('PAIRS', 'USD' + currencies.join('","USD'));
            return $http.jsonp(url).then(function(response) {
                var newUsdToForeignRates = {};
                angular.forEach(response.data.query.results.rate, function(rate) {
                    var currency = rate.id.substring(3,6);
                    newUsdToForeignRates[currency] = window.parseFloat(rate.Rate);
                });
                usdToForeignRates = newUsdToForeignRates;
            });
        };

        refresh();

        return {
            currencies: currencies,
            convert: convert
        };
    }]);

显示结果

QQ截图20160627003913.png

改变了什么?

我们的finance模块的服务 currencyConverter 现在使用$http,Angular提供的一个内置的服务来访问服务器后端。$http是对XMLHttpRequest和JSONP的封装。