AngularJS 简介(Introduction)
AngularJS是一个开发动态Web应用的框架。它让你可以使用HTML作为模板语言并且可以通过扩展的HTML语法来使应用组件更加清晰和简洁。它的创新之处在于,通过数据绑定和依赖注入减少了大量代码,而这些都在浏览器端通过JavaScript实现,能够和任何服务器端技术完美结合。
Angular是为了扩展HTML在构建应用时本应具备的能力而设计的。对于静态文档,HTML是一门很好的声明式的语言,但对于构建动态WEB应用,它无能为力。所以,构建动态WEB应用往往需要一些技巧才能让浏览器配合我们的工作。
通常,我们通过以下手段来解决动态应用和静态文档之间不匹配的问题:
- 类库 - 一些在开发WEB应用时非常有用的函数的集合。你的代码起主导作用,并且决定何时调用类库的方法。例如:
jQuery
等。 - 框架 - 一种WEB应用的特殊实现,你的代码只需要填充一些具体信息。框架起主导作用,并且决定何时调用你的代码。例如:
knockout
,ember
等。
Angular另辟蹊径,它尝试去扩展HTML的结构来弥合以文档为中心的HTML与实际Web应用所需要的HTML之间的鸿沟。Angular通过指令(directive)扩展HTML的语法。例如:
- 通过
{{}}
进行数据绑定。 - 使用DOM控制结构来进行迭代或隐藏DOM片段。
- 支持表单和表单验证。
- 将逻辑代码关联到DOM元素上。
- 将一组HTML做成可重用的组件。
一个完整的前端解决方案
在构建WEB应用的前端时,Angular提供的不是一个部分解决方案,而是一个完整的解决方案。它能够处理所有你写过的混杂了DOM和AJAX的代码,并能够将它们组织的结构良好。这使得Angular在决定应该怎样构建一个CRUD应用时显得甚至有些“偏执(opinionated)”,但是尽管它“偏执”,它也尝试确保使用它构建的应用能够灵活的适应变化。下面是Angular的一些出众之处:
- 构建一个CRUD应用时可能用到的所有技术:数据绑定、基本模板指令、表单验证、路由、深度链接、组件重用、依赖注入。
- 可测试性:单元测试、端到端测试、模拟对象(mocks)、测试工具。
- 拥有一定目录结构和测试脚本的种子应用。
Angular的可爱之处
Angular通过给开发者呈现更高层次的抽象来简化应用的开发。和其他的抽象一样,它也以损失灵活性为代价。换句话说,Angular并不是适合任何应用的开发,Angular考虑的是构建CRUD应用。幸运的是,绝大多数WEB应用都是CRUD应用。为了理解Angular适用哪些场合,知道它不适合哪些场合是很有帮助的。
对于像游戏和有图形界面的编辑器之类的应用,会进行频繁且复杂的DOM操作,和CRUD应用不同。因此,可能不适合用Angular来构建。在这种场景下,使用更低抽象层次的类库可能会更好,例如:jQuery
。
Angular之道
Angular是建立在这样的信念之上的:即声明式的代码用在构建用户界面和组装软件组件时更好,而命令式的代码更擅长展现业务逻辑。
- 将应用逻辑与DOM操作解耦,会大大提高代码的可测试性。
- 平等看待应用的测试和开发,测试的难度很大程度上取决于代码的结构。
- 将前端与服务器端解耦,这样使得前端的开发和服务器端的开发可以齐头并进,实现两边代码的重用。
- 框架在整个应用的开发流程中指导开发者:从用户界面设计到实现业务逻辑,再到测试。
- 化繁为简,化整为零总是好的。
Angular将把你从下面的苦海中解脱出来:
- 使用回调:回调会降低代码的可读性,是代码变得零散。移除像回调之类的常见代码是件好事,大幅移除因为JavaScript这门语言的不足而使你不得不写的代码,从而让应用显得更加清晰。
- 以编程的方式操作HTML DOM:操作HTML DOM是AJAX应用中很基础的一部分,但它不灵活并且容易出错。通过声明式的语句,描述UI该怎样随着状态的改变而变化,能让你从低级的DOM操作中解脱出来。绝大多数Angular的应用开发中,开发者都不需要自己去写低级的操作DOM的代码,尽管如果你非要这样的话,也是可以的。
- 在用户界面中读写数据:AJAX应用中的绝大多数操作都是CRUD操作。一个典型的流程是从服务器端取到数据组装成内部对象,然后写到HTML的表单中,在用户对表单进行修改之后,进行表单验证,显示表单验证错误信息,然后将数据重新组装成内部对象,再发给服务器。在这个流程中有很多重复的代码要写,而Angular消除了在这个流程中几乎所有的重复代码,使得代码看起来只是在描述所有的执行流程,而不是所有的实现细节。
-
在开始前写大量的初始化代码:一般需要写很多的基础性的代码才能完成一个基本的AJAX的Hello World应用。在Angular的应用中,你可以通过一些服务来初始化应用,这些服务都是以类似于Guice的方式进行依赖注入的。这会让你很快进入功能开发。另外,你还能完全控制自动化测试的初始化过程。
AngularJS 概念概述(Conceptual Overview)
在创建第一个应用程序之前,你需要理解Angular中的一些概念。 在本章,通过一个简单的例子,你可以快速接触Angular中所有重要的部分。 但是,我不会花时间去解释细节,而是着重于帮助你建立一个全局性的“概观”。 更深入的解释,请参见AngularJS-PhoneCat。
概念 | 说明 |
---|---|
模板(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)的、可复用的业务逻辑 |
例子的第一步:数据绑定
下面我们将编写一个表单,用来计算一个订单在不同币种下的总价。
首先,我们做一个单币种表单,它有数量和单价两个输入框,并且把数量和单价相乘得出该订单的总价。
源码
<!doctype html> <html ng-app> <head> <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script> </head> <body> <div ng-init="qty=1;cost=2"> <b>订单:</b> <div> 数量: <input type="number" ng-model="qty" required > </div> <div> 单价: <input type="number" ng-model="cost" required > </div> <div> <b>总价:</b> </div> </div> </body> </html>
你可以先在Demo区体验一下操作,接下来我们将通读这个例子,并且详细讲解这里所发生的事情。
这看起来很像标准的HTML,只是带了一些新的标记。在Angular中,像这样的文件叫做“模板(template)”。 当Angular启动你的应用时,它通过“编译器(compiler)”来解析并处理模板中的这些新标记。 这些经过加载、转换、渲染而成的DOM就叫做“视图(view)”
第一类新标记叫做“指令(directive)”。 它们通过HTML中的属性或元素来为页面添加特定的行为。上面的例子中,我们使用了
属性,与此相关的指令(directive)则负责自动初始化我们的应用程序。 Angular还为ng-app
元素定义了一个指令,它负责添加额外的行为到这个元素上。例如,当它发现了input
input
元素的required
属性,它就会自动进行验证,确保所输入的文本不会是空白。
指令则负责从变量(比如这里的qty、cost等)加载ng-model
input
元素的value
值,并且把input
元素的value
值写回变量中。并且,还会根据对input
元素进行校验的结果自动添加相应的css类。 比如在上面这个例子中,我们就通过这些css类把空白input
元素的边框设置为红色,来表示无效的输入。
第二类新标记是双大括号{{ expression | filter }}
,其中expression是“表达式”语句,filter是“过滤器”语句。 当编译器遇到这种标记时,它会把这些标记替换为这个表达式的计算结果。 模板中的"表达式"是一种类似于JavaScript的代码片段,它允许你读写变量。注意,表达式中所用的变量并不是全局变量。 就像JavaScript函数定义中的变量都属于某个作用域一样,Angular也为这些能从表达式中访问的变量提供了一个“作用域(scope)”。 这些存储于Angular作用域(Scope)中的变量叫做Scope变量,这些变量所代表的数据叫做“模型(model)”。 在上面的例子中,这些标记告诉Angular:“从这两个input
元素中获取数据,并把它们乘在一起。”
上面这个例子中还包含一个"过滤器(filter)"。 过滤器格式化表达式的值,以便呈现给用户。 上面的例子中
过滤器把一个数字格式化为金额的形式进行输出。currency
这个例子中最重要的一点是:Angular提供了动态(live)的绑定: 当input
元素的值变化的时候,表达式的值也会自动重新计算,并且DOM所呈现的内容也会随着这些值的变化而自动更新。 这种模型(model)与视图(view)的联动就叫做“双向数据绑定”。
添加UI逻辑:控制器
现在,我们添加一些逻辑,以便让这个例子支持不同的币种。它将允许我们使用不同的币种来输入、计算和支付这个订单。
源码
<!doctype html> <html ng-app="invoice1"> <head> <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script> <script src="invoice1.js"></script> </head> <body> <div ng-controller="InvoiceController as invoice"> <b>订单:</b> <div> 数量: <input type="number" ng-model="invoice.qty" required > </div> <div> 单价: <input type="number" ng-model="invoice.cost" required > <select ng-model="invoice.inCurr"> <option ng-repeat="c in invoice.currencies"></option> </select> </div> <div> <b>总价:</b> <span ng-repeat="c in invoice.currencies"> </span> <button class="btn" ng-click="invoice.pay()">支付</button> </div> </div> </body> </html>
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] * 1 / this.usdToForeignRates[inCurr]; }; this.pay = function pay() { window.alert("谢谢!"); }; });
这次有什么变动?
首先,出现了一个新的JavaScript文件,它包含一个被称为"控制器(controller)"的函数。 更确切点说:这个文件中定义了一个构造函数,它用来在将来真正需要的时候创建这个控制器函数的实例。 控制器的用途是导出一些变量和函数,供模板中的表达式(expression)和指令(directive)使用。
在创建一个控制器的同时,我们还往HTML中添加了一个
指令。 这个指令告诉Angular,我们创建的这个ng-controller
InvoiceController
控制器将会负责管理这个带有ng-controller指令的div节点,及其各级子节点。 InvoiceController as invoice
这个语法告诉Angular:创建这个InvoiceController
的实例,并且把这个实例赋值给当前作用域(Scope)中的invoice
变量。
同时,我们修改了页面中所有用于读写Scope变量的表达式,给它们加上了一个invoice.
前缀。 我们还把可选的币种作为一个数组定义在控制器中,并且通过
指令把它们添加到模板。 由于控制器中还包含了一个ng-repeat
total
函数,我们也能在DOM中使用{{invoice.total(...) }}
表达式来绑定总价的计算结果。
同样,这个绑定也是动态(live)的,也就是说:当invoice.total函数的返回值变化的时候,DOM也会跟着自动更新。 表单中的“支付”按钮附加上了指令
。 这意味着当它被点击时,会自动执行ngClick
invoice.pay()
这个表达式,即:调用当前作用域中的pay函数。
在这个新的JavaScript文件中,我们还创建了一个模块(module),并且在这个模块中注册了控制器(controller)。 接下来我们就讲一下模块(module)这个概念。
下面这个图表现的是我们声明了这个控制器(controller)之后,它们是如何协作的。
与视图(View)无关的业务逻辑:服务(Service)
现在,InvoiceController
包含了我们这个例子中的所有逻辑。如果这个应用程序的规模继续成长,最好的做法是:把控制器中与视图无关的逻辑都移到"服务(service)"中。 以便这个应用程序的其他部分也能复用这些逻辑。
接下来,就让我们重构我们的例子,并且把币种兑换的逻辑移入到一个独立的服务(service)中。
源码
<!doctype html> <html ng-app="invoice2"> <head> <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script> <script src="finance2.js"></script> <script src="invoice2.js"></script> </head> <body> <div ng-controller="InvoiceController as invoice"> <b>订单:</b> <div> 数量: <input type="number" ng-model="invoice.qty" required > </div> <div> 单价: <input type="number" ng-model="invoice.cost" required > <select ng-model="invoice.inCurr"> <option ng-repeat="c in invoice.currencies"></option> </select> </div> <div> <b>总价:</b> <span ng-repeat="c in invoice.currencies"> </span> <button class="btn" ng-click="invoice.pay()">支付</button> </div> </div> </body> </html>
angular.module('finance2', []) .factory('currencyConverter', function() { var currencies = ['USD', 'EUR', 'CNY'], usdToForeignRates = { USD: 1, EUR: 0.74, CNY: 6.09 }; return { currencies: currencies, convert: convert }; function convert(amount, inCurr, outCurr) { return amount * usdToForeignRates[outCurr] * 1 / usdToForeignRates[inCurr]; } });
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("谢谢!"); }; }]);
这次有什么改动?
我们把convertCurrency
函数和所支持的币种的定义独立到一个新的文件:finance.js
。但是控制器怎样才能找到这个独立的函数呢?
这下该“依赖注入(Dependency Injection)”出场了。依赖注入(DI)是一种设计模式(Design Pattern),它用于解决下列问题:我们创建了对象和函数,但是它们怎么得到自己所依赖的对象呢? Angular中的每一样东西都是用依赖注入(DI)的方式来创建和使用的,比如指令(Directive)、过滤器(Filter)、控制器(Controller)、服务(Service)。 在Angular中,依赖注入(DI)的容器(container)叫做"注入器(injector)"。
要想进行依赖注入,你必须先把这些需要协同工作的对象和函数注册(Register)到某个地方。在Angular中,这个地方叫做“模块(module)”。
在上面这个例子中: 模板包含了一个ng-app="invoice2"
指令。这告诉Angular使用invoice2
模块作为该应用程序的主模块。 像angular.module('invoice', ['finance'])
这样的代码告诉Angular:invoice
模块依赖于finance
模块。 这样一来,Angular就能同时使用InvoiceController
这个控制器和currencyConverter
这个服务了。
刚才我们已经定义了应用程序的所有部分,现在,需要Angular来创建它们了。 在上一节,我们了解到控制器(controller)是通过一个工厂函数创建的。 而对于服务(service),则有多种方式来定义它们的工厂函数(参见服务指南)。 上面这个例子中,我们通过一个返回currencyConverter
函数的函数作为创建currencyConverter
服务的工厂。 (译注:js中的“工厂(factory)”是指一个以函数作为“返回值”的函数)
回到刚才的问题:InvoiceController
该怎样获得这个currencyConverter
函数的引用呢? 在Angular中,这非常简单,只要在构造函数中定义一些具有特定名字的参数就可以了。 这时,注入器(injector)就可以按照正确的依赖关系创建这些对象,并且根据名字把它们传入那些依赖它们的对象工厂中。 在我们的例子中,InvoiceController
有一个叫做currencyConverter
的参数。 根据这个参数,Angular就知道InvoiceController
依赖于currencyConverter
,取得currencyConverter
服务的实例,并且把它作为参数传给InvoiceController
的构造函数。
这次改动中的最后一点是我们现在把一个数组作为参数传入到module.controller
函数中,而不再是一个普通函数。 这个数组前面部分的元素包含这个控制器所依赖的一系列服务的名字,最后一个元素则是这个控制器的构造函数。 Angular可以通过这种数组语法来定义依赖,以免js代码压缩器(Minifier)破坏这个“依赖注入”的过程。 因为这些js代码压缩器通常都会把构造函数的参数重命名为很短的名字,比如a
,而常规的依赖注入是需要根据参数名来查找“被注入对象”的。 (译注:因为字符串不会被js代码压缩器重命名,所以数组语法可以解决这个问题。)
访问后端
现在开始最后一个改动:通过Yahoo Finance API来获得货币之间的当前汇率。 下面的例子将告诉你在Angular中应该怎么做。
源码
<!doctype html> <html ng-app="invoice3"> <head> <script src="http://code.angularjs.org/1.2.25/angular.min.js"></script> <script src="invoice3.js"></script> <script src="finance3.js"></script> </head> <body> <div ng-controller="InvoiceController as invoice"> <b>订单:</b> <div> 数量: <input type="number" ng-model="invoice.qty" required > </div> <div> 单价: <input type="number" ng-model="invoice.cost" required > <select ng-model="invoice.inCurr"> <option ng-repeat="c in invoice.currencies"></option> </select> </div> <div> <b>总价:</b> <span ng-repeat="c in invoice.currencies"> </span> <button class="btn" ng-click="invoice.pay()">Pay</button> </div> </div> </body> </html>
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("谢谢!"); }; }]);
angular.module('finance3', []) .factory('currencyConverter', ['$http', function($http) { var YAHOO_FINANCE_URL_PATTERN = 'http://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', currencies = ['USD', 'EUR', 'CNY'], usdToForeignRates = {}; refresh(); return { currencies: currencies, convert: convert, refresh: refresh }; function convert(amount, inCurr, outCurr) { return amount * usdToForeignRates[outCurr] * 1 / usdToForeignRates[inCurr]; } function refresh() { 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; }); } }]);
这次有什么变动?
这次我们的finance
模块中的currencyConverter
服务使用了$http
服务 —— 它是由Angular内建的用于访问后端API的服务。 是对XMLHttpRequest
以及JSONP的封装。详情参阅$http的API文档 - $http
。