[译]AngularJS 1.3.0 开发者指南(二) -- 概念综述
这部分通过示例简略的介绍了Angular的所有重要内容,详情见向导部分.
概念 | 描述 |
---|---|
Template(模板) | 带有扩展标记的HMTL片断 |
Directives(指令) | 通过自定义属性和元素扩展HTML |
Model(模型) | 展示给用户或者用户操作的数据 |
Scope(作用域) | 存储模型的上下文环境,在这个上下文环境中控制器,指令和表达式才能访问到它 |
Expressions(表达式) | 访问作用域中变量和函数 |
Compiler(编译器) | 解析模板,实例化指令和表达式 |
Fiter(过滤器) | 格式化展示给用的表达式的值 |
View(视图) | 用户看到的内容(DOM) |
Data Binding(数据绑定) | 模型和视图之间的数据同步 |
Controller(控制器) | 视图之后的业务逻辑 |
Dependency Injection(依赖注入) | 创建和自动装载对象和函数 |
Injector(注入器) | 用于实现依赖注入的容器 |
Module(模块) | 包括控制器,服务,过滤器,指令在内的web应用的不同部分,可以配置注入器的容器 |
Service(服务) | 可重用的,独立于视图的业务逻辑 |
第一个示例: 数据绑定
在下面的例子中,我们将创建一个表单来计算订单的金额.
首先输入数量和单价,它们的乘积就是订单的总金额:
index.html
<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>
试着运行上面的实时预览,然后我们将通读示例源码,描述它是怎样运行的.
这看起来像是普通的HTML,只是多了些新的标记. 在Angular中,像这样一个文件被叫做"template(模板)". Angular启动应用时,它会使用"compiler(编译器)"解析并且处理模板中的新标记. 这些被加载,转换和渲染的DOM就是"view(视图)".
第一种新的标记叫做"directives(指令)". 指令适用于在HTML属性或元素中添加特定的行为. 上面示例中,我们使用了指令ng-app
属性,它的作用就是初始化我们的应用. Angular也为input
元素定义了指令,通过这个指令可以为元素添加额外的行为. 通过ng-model
指令可以将输入框中的值存储到变量中,或将变量中的值更新到输入框中.
通过自定义指令访问DOM: 在Angular中,应用唯一一个可以访问DOM的地方就是在指令内部. 这一点非常重要,因为需要访问DOM的实现是非常难以测试的. 如果你真的需要直接访问DOM,那么你应该创建一个自定义指令来完成访问DOM的操作. 指令指南中介绍了如何创建自定义指令.
第二种新的标记是双花括号{{ expression | filter }}
: 当编译器遇到这种标记时,它会用实际的值去替换标记本身. 模板中的"expression(表达式)"就像是Javascript代码片段,它可以对变量进行读写操作. 需要注意的是,那些变量并不是全局变量. 就在定义在Javascript函数中的变量属于某一个作用域一样,Angular也为表达式中访问的变量提供了一个"scope(作用域)". 这些存储在作用域变量中的值被称为"model(模型)". 在上面的示例中,这些告诉Angular: "从输入框中取得数据,并将它们相乘".
上面的示例中包含一个"filter(过滤器)". 过滤器可以将表达式的值格式化后展示给用户. 在上面的示例中,过滤器currency
将一个数字格式化为货币的格式进行输出.
重要的一点是,在上面的示例中,Angular提供的动态绑定机制: 输入的值无论什么时候改变,表达式的值都会自动重新计算,DOM元素也会自动更新显示的值. 这就是Angular所提供的模型与视图之间的"双向数据绑定机制".
添加UI逻辑: 控制器
现在让我们在上面的示例中添加一些业务逻辑,使我们可以使用不同的货币输入,计算金额并完成支付.
invoice.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!"); }; });
index.html
<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>
做了哪些修改 ?
首先,添加了一个JavaScript文件,其中有个被叫做"controller(控制器)"的函数. 更确切的说,这个文件中包含一个构造函数,这个构造函数可以创建控制器实例. 控制器的作用是给表达式和指令暴露变量和函数,供它们使用.
除这个包含控制器代码的JavaScript文件以外,我们还在HTML中添加了ng-controller
指令. 这个指令告诉Angular,这个新的控制器InvoiceController
会负责管理包含它的元素及其所有子元素. InvoiceController as invoice
这种语法告诉Angular要初始化这个控制器,并且将它赋值给当前作用域的变量invoice
.
我们还修改了页面中读写变更的所有表达式,给他们添加了控制器的实例名称invoice.
作为前缀. 我们在控制器中定义一些货币的各类,并且通过指令ng-repeat
把它们添加到模板中. 由于控制器还还有个total
函数,我们还能将它的结果值通过表达式{{ invoice.total(...) }}
绑定在DOM元素上.
当然,这个绑定也是动态的,换句话说,无论函数结果什么时候发生变化,DOM都会自动修改其展示的值. 付款按钮使用了指令ngClick
,无论什么时候点击这个按钮,它将计算相应表达式的值.
在这个新增的JavaScript文件中,我们还创建一个module(模块),并且将控制器注册在了这个模块上. 我们将在下一部分对模块进行讨论.
下图展示的是加入了控制器之后,应用中的每一部分是如何协作的:
独立于视图的业务逻辑: Services(服务)
现在,控制器InvoiceController
包含了示例中的所有逻辑. 当这个应用继续扩展,那么最好的作法是将与视图无关的业务逻辑从控制器中移动至"service(服务)"中,这样它都能更好的被应用的其他部分重用. 日后,我们也可以修改脚本,将它改成从网络中加载汇率,例如Yahoo的金融API,而不会修改控制器.
我们重构下我们的示例,将货币转换移动到另一个文件的service(服务)中.
finance2.js
angular.module('finance2', []) .factory('currencyConverter', function() { var currencies = ['USD', 'CNY']; var usdToForeignRates = { USD: 1, CNY: 6.09 }; var convert = function (amount, outCurr) { return amount * usdToForeignRates[outCurr] / usdToForeignRates[inCurr]; }; return { currencies: currencies, convert: convert }; });
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, outCurr); }; this.pay = function pay() { window.alert("Thanks!"); }; }]);
index.html
<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>
有哪些变化? 我们将函数convertCUrrency
和已经定义好的货币变量移动到finance2.js
中. 但是控制器是怎样引用已经分离出去的函数的呢 ?
轮到"Dependency Injection(依赖注入)"出场了. Dependency Injection (DI)依赖注入是一种软件设计模式,它解决了对象和函数是如何得到已经创建好的并且被它们所依赖的对象的引用的. Angular中的每个部分(指令,控制器,服务...)都是通过依赖注入机制来创建和关联的. 在Angular中,DI窗口被称叫"injector".
为了使用DI(依赖注入),所有需要协作的部件都需要统一注册的一个位置. 在Angular中,"mudules(模块)"就是为解决这个问题. Angular从指令ng-app
开始启动,它会根据模块名称加载模块配置,包括此模块所依赖的所有模块.
在上面的示例中: 模板中包含了指令ng-app="invoice2"
. Angular就会使用模块invoice2
作为整个应用的主模块. 代码段angular.module('invoice2',['finance2'])
指定了模块invoice2
依赖于模块finance2
. 这样,Angular就即可以使用控制器InvoiceController
以及服务currencyConverter
.
既然Angular了解应用所有部分的定义,那么就可以来创建它们. 在上一段中,我们是使用一个工厂方法创建了控制器. 而对于服务来说,有多种定义它们工厂的方式(见服务指南). 在上面的示例中,我使用一个返回currencyConverter
函数的函数作为创建currencyConverter
服务的工厂.
回到最初的问题: 控制器InvoiceController
是怎样获得函数currencyConverter
的引用的? 在Angular中,通过定义构造函数的参数就可以做到这一点. 通过这种方式,注入器(injector)根据正确的依赖顺序创建这些对象,并将创建好的对象传入依赖它们的工厂中. 在我们的示例中,控制器InvoiceController
的构造函数有一个命名为currencyConverter
的参数. 通过这个参数,Angular便知道控制器与服务之间的依赖关系,并且把服务对象作为参数来调用控制器的构造函数.
这次改动中的最后一点是,我们将一个数组传入了module.controller
函数中,而不再是一个普通函数. 首先,这个数组包含了控制器所依赖的服务的名称. 数组的最后一个元素则是控制器的构造函数. Angular通过这种数组的语法定义依赖关系,使得依赖注入发生在压缩代码之后,因为代码压缩通常都会将控制器构造函数的名称重命名为很短的名称,比如a
.
访问后台
让我们通从Yahoo金融API获取汇率来完成我们的示例. 下面的示例将展示Angular是怎样做的:
invoice3.js
angular.module('invoice3', ['finance3']) .controller('InvoiceController', outCurr); }; this.pay = function pay() { window.alert("Thanks!"); }; }]);
finance3.js
angular.module('finance3', ['$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', 'CNY']; var usdToForeignRates = {}; var convert = function (amount, 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).success(function(data) { var newUsdToForeignRates = {}; angular.forEach(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, refresh: refresh }; }]);
index.html
<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>
修改了什么? finance
模块的currencyConverter
服务使用了$http
服务,一个Angular内置的服务,使用它可以访问后台服务器. $http
就是封装了XMLHttpRequest
和JSONP
传输.