基于AngularJS开发Web应用
AngularJS是用于创建动态Web应用的框架,它为克服HTML语言的缺点而生。
HTML能够很好的声明静态文档,灵活的进行页面布局。但是,它缺乏声明动态视图的能力。为了解决这一问题,前些年的Web开发,通常采用以下的技术手段:
- 基于服务器端脚本来生成HTML页面:这种方式的缺点是,需要大量的页面导航,并且增加了服务器CPU的负担。服务器端脚本有两种开发风格:
- 以脚本语言为中心:使用JSP、PHP、ASP等脚本语言完成开发工作,这很容易导致UI代码和应用逻辑的耦合,难以维护
- 以模板或标签库中心:使用Velocity、Structs标签库、EL表达式等框架,来生成HTML页面。这种风格可以较好的解耦UI代码和应用逻辑
- 以JavaScript为中心完成HTML页面绘制:例如ExtJS框架。这种方式可以非常灵活的生成并操控HTML页面,可以通过XHR避免页面导航。其缺点是复杂性高,完全丢弃了HTML语言本身的优势
AngularJS某种程度上结合了这两种方式的优点:
- 类似于模板或标签库。AngularJS扩展了HTML的语法,采用简洁易读的方式声明页面组件,支持数据绑定和依赖注入。重要的是,所有操作都在浏览器中执行,与服务器无关
- 类似于JavaScript框架。Angular实现了一个MVC框架,支持通过JavaScript对页面进行操控、与服务器通信(XHR)。可以实现单页面应用。单页面开发的支持对于移动WebApp非常重要,可以避免不必要的流量和性能(电量)损耗
AngularJS适合以CRUD为主要功能的应用,游戏、UI编辑器等需要复杂DOM操控的应用则不太适合,可以使用支持更低级抽象的JavaScript框架(例如jQuery)代替之。
- 把DOM操控从应用逻辑中解耦是个非常好的主意,可以极大的改善可测试性
- 可测试性和代码编写速度一样重要
- 把客户端和服务器端完全解耦,以便并行开发、分别支持两端的重用
- 让日常任务简单,让复杂任务可能
- 避免注册回调(监听器):到处出现的回调注册代码,让代码变得杂乱。AngularJS减少了你需要编写的大量样板式代码
- 避免编程式的操控DOM:AngularJS支持声明式的指出视图如何随着应用状态的变化而变化。避免了编程式的操控DOM
- 避免手工在UI和模型之间传递数据:AngularJS支持表单验证、数据绑定、自动UI更新
- 特殊标记(Markup):Angular在HTML中引入的新的记号,标记分为:
- 指令(Directive):以HTML属性或元素的形式出现。指令是AngularJS访问DOM元素唯一建议方式,遵循这一建议,可以增强可测试性。可以自己实现指令,以扩展Angular的功能
- 用于数据绑定的双花括号(Double curly braces):形式为 {{ expression | filter }} ,Angular把括号中的内容解析为文本并显示。expression是JavaScript风格的表达式;filter则负责格式化expression,让其更加可读
- 指令:用于为DOM添加特殊行为、或者转换DOM子树的Angular标记
- 模板(Template):带有特殊标记的HTML文件被称为模板,AngularJS会使用编译器解析、处理此模板,并转换、渲染HTML
- 数据绑定(Data binding):在UI和模型之间同步数据,AngularJS支持双向绑定(two-way data binding):当UI变化后,数据自动同步到对应的模型字段上;当模型字段变化后,UI自动更新
- 依赖注入(DI):AngularJS中所有组件,包括指令(directives)、过滤器(filters)、控制器(controllers)、服务(services)通过DI容器(Injector)创建和注入。依赖注入让所有组件能够自动获取其依赖对象的引用
- 模块(Modules):用于管理一组相互协作的组件
- 控制器(Controller):在AngularJS中,控制器是一个构造函数,通常会附带一组行为。控制器自动创建一个Scope,并负责初始化之
- 作用域(Scope):AngularJS中的Scope是模型的容器,其属性就是模型的属性。Scope还作为某个DOM分支下所有Markup的上下文对象(this)。正如DOM结构一样,Scope也可能形成树状
Angular把核心模块(angular.js)和可选模块(angular-***.js)的代码分离在不同的JavaScript文件中,可以到官网代码库下载。核心模块和每个可选模块都有对应的压缩版本。
CSS样式类 | 说明 |
ng-scope | 当Angular定义新的Scope后,对应HTML元素上被添加该样式类 |
ng-isolate-scope | 当Angular定义新的隔离Scope后,对应HTML元素上被添加该样式类 |
ng-binding | 当一个元素通过 ng-bind 、 {{}} 等方式进行了数据绑定后,Angular为该HTML元素添加该样式类 |
ng-invalid, ng-valid | 当一个表单控件没有通过/通过验证时,分别应用ng-invalid/ng-valid类 |
ng-pristine, ng-dirty | 当用户和一个表单控件交互过后,应用ng-dirty,否则应用ng-pristine |
ng-touched, ng-untouched | 当表单控件获取/失去焦点时,应用对应的样式类 |
过滤器 | 说明 | ||||
currency | 用于格式化为货币格式,可以设置货币符号 | ||||
date | 用于格式化日期、时间:
|
||||
filter |
从给定数组中选择一个子集,并将其生成一个新数组返回。通常用于过滤展示数据,支持三种类型的过滤器参数:
|
||||
json | 可以把JavaScript对象转换为JSON字符串,有利于调试 | ||||
limitTo | 对输入数字、字符串进行截断:
|
||||
lowercase | 转换为小写 | ||||
number | 格式化数字,如果输入非数字,生成空串:
|
||||
orderBy |
对数组进行排序。该过滤器可以接受两个参数,第二个参数设置为true则反转排序:
|
||||
uppercase | 转换为大写 |
指令 | 说明 | ||
ng-disabled |
值为布尔类型的HTML属性,只有当属性不存在时,才能达到布尔值false的效果,也就是说,根据HTML规范, selected="false" 和 selected="true" 的效果是一样的。 为此,Angular推出这些HTML属性的变体,规避HTML布尔属性反直觉特性,简化模板编写 |
||
ng-checked | |||
ng-readonly | |||
ng-selected | |||
ng-href | 当使用当前作用域中的属性动态创建URL时,使用该指令。当URL中的插值尚未生效时,Angular不会执行点击动作 | ||
ng-src | 当此指令中表达式尚未生效时,不显示图像 | ||
ng-app |
这两个指令分别用于声明Angular应用、控制器,它们的共同点是:会创建新的作用域:
|
||
ng-controller | |||
ng-include |
加载、编译并包含外部HTML模板片段到当前应用中。 模板的URL被同源策略限制——只有相同HTTP协议、域名下的URL才允许。同源策略可以通过CORS突破。 注意,ng-include会自动创建一个新的子作用域,如果要使用既有的作用域,必须指定ng-controller。 示例:
|
||
ng-switch |
可以根据属性值的不同,来切换渲染视图:
|
||
ng-view | 用于容纳路由使用的子模板 | ||
ng-if | 如果表达式为true,则对应元素插入到DOM中;否则从DOM中移除。它与ng-show/ng-hide的不同之处是,后两者仅仅会隐藏DOM节点 | ||
ng-repeat | 遍历一个集合或为集合中的每个元素生成一个模板实例,集合中每个元素都被赋予新的子Scope。并且新的Scope暴露以下属性:
|
||
ng-init | 设置当前作用域的内部初始状态:
|
||
{{ }} | 双括号操作符本质上是指令,它是ng-bind的简写形式,不需要显式的创建HTML元素,可以嵌在HTML文本节点中。 在屏幕可视的区域内使用{{ }}会导致页面加载时未渲染的元素发生闪烁,用ng-bind可以规避该问题。 |
||
ng-bind | Angular加载含有{{ }}的元素后并不会立刻渲染它们,导致未渲染内容闪烁。使用ng-bind可以避免闪烁:
|
||
ng-cloak | 在{{}}所在元素上使用该指令,可以避免页面闪烁——先显示未解析的{{}},然后闪烁为解析后的正确HTML 该指令会在解析完成之前隐藏目标元素,因而避免闪烁。该指令可以放在祖先元素上,从而对多个{{}}生效 |
||
ng-bind-template | 类似于ng-bind,可以绑定多个表达式:
|
||
ng-model |
来将input、select、text area或自定义表单控件同包含它们的作用域中的属性(如果属性尚不存在,会隐式的创建)进行绑定。该指令负责处理表单验证、在元素上设置ng-valid/ng-invalid等样式类、在父表单中注册控件:
|
||
ng-show ng-hide |
根据表达式的值显示或者隐藏HTML元素 |
||
ng-change |
当表单控件的值发生变化时,执行指定的表达式 |
||
ng-form |
用来在一个表单内部嵌套另一个表单,只有内部所有的子表单都合法时,外部的表单才会合法,在通过ng-repeat动态创建表单时,该指令很有用 Angular不会通过POST提交表单,除非指定了action属性。要指定提交表单时执行的JavaScript函数,可以:
|
||
ng-click |
指定元素被点击时执行指定的表达式 |
||
ng-select |
|
||
ng-submit | 当表单提交时,执行指定的表达式 | ||
ng-class | 动态设置元素的CSS样式类,该表达式的值是一个对象:
对象的属性名是CSS样式类名,属性值为布尔值,如果为true,则该样式类添加到元素上,否则从元素上移除 |
||
ng-attr-(suffix) | 指定一个HTML属性,并绕过某些浏览器检查 |
类型 | 名称 | 说明 |
emit | $includeContentLoaded | 当ngInclude的内容被重新加载时,从ngInclude指令上触发 |
$includeContentRequested |
当ngInclude的内容被请求时,从ngInclude指令上触发 |
|
$viewContentLoaded | 当ngView的内容被重新加载时,从当前ngView作用域上发送 | |
broadcast | $locationChangeStart | 通过$location服务对浏览器URL作更新时,触发该事件 |
$locationChangeSuccess | 当浏览器的地址成功变更,又没有阻止$locationChangeStart事件时,该事件从$rootScope触发 | |
$routeChangeStart | 在路由变更发生之前(解析路由变更所需的所有依赖项时),$routeChangeStart事件从$rootScope触发 | |
$routeChangeSuccess | 在所有路由依赖项跟着$routeChangeStart被解析之后, $routeChangeSuccess事件从 $rootScope上触发 |
|
$routeChangeError | 如果路由对象上任意的resolve属性被拒绝(Reject)了,$routeChangeError事件触发 | |
$routeUpdate | 如果$routeProvider上的reloadOnSearch属性被设置成false,并且使用了控制器的同一个 实例,$routeUpdate事件从$rootScope触发 |
|
$destroy | 在作用域被销毁之前,$destroy事件会在作用域上广播。子作用域可以利用此机会执行自身的清理 |
函数 angular.element() 可以把一个DOM元素或者HTML字符串包装为一个类似jQuery元素的对象。如果当前应用使用jQuery(其JS文件必须在Angular之前引入),那么该函数的返回值就是一个jQuery元素;否则,它是一个jqLite对象——Angular内置的jQuery的子集。jqLite更加轻量,和jQuery元素的API兼容。jqLite提供以下jQuery兼容方法:
方法 | 说明 | ||
addClass() | 添加指定的样式类到元素上: addClass( "myClass yourClass" ); | ||
after() | 插入内容到元素后面(作为兄弟元素): after( "<p>Test</p>" ) ,支持入参HTML、Element、Array | ||
append() | 作为最后一个子元素插入,支持入参HTML、Element、Array | ||
attr() | 不支持以函数作为入参。获取或者设置HTML属性,从jQuery1.6开始,对于尚未设置的HTML属性,总是返回undefined。如果要使用checked, selected, disabled等属性,应当使用prop()方法:
|
||
bind() | 签名:
bind( eventType [, eventData ], handler ) 不支持namespaces, selectors,eventData。给选中元素的一个或者多个事件添加监听器 |
||
children() | 不支持selectors。获取元素的子元素 | ||
clone() | 创建一个元素的深复制 | ||
contents() | 获取每个元素的子节点,返回的集合中包含文本和注释节点 | ||
css() | 仅检索inline样式,不调用getComputedStyle()。获取或设置元素的style属性值 | ||
data() | 存储或返回与元素关联的指定数据值 | ||
detach() | 从DOM中移除匹配的元素 | ||
empty() | 清除所有子节点 | ||
eq() | 获取指定索引位置的元素 | ||
find() | 限制通过标签名查找。过滤元素的子节点 | ||
hasClass() | 确定元素本身是否分配了给定的CSS类 | ||
html() | 获取或者设置元素的HTML内容 | ||
next() | 不支持selectors。获取紧跟元素的兄弟元素 | ||
on() | 不支持namespaces, selectors,eventData。类似于bind() | ||
off() | 不支持namespaces, selectors,eventData。类似于unbind() | ||
one() | 签名:
one( events [, data ], handler ) 不支持namespaces, selectors。仅执行一次的事件监听器 |
||
parent() | 不支持selectors。获取元素的父元素 | ||
prepend() | 将内容作为子元素插入到元素的开头 | ||
prop() | 获取或设置元素的属性值 | ||
ready() | 指定一个DOM加载完成时执行的函数 | ||
remove() | 从DOM中移除元素 | ||
removeAttr() | 从元素中移除一个属性 | ||
removeClass() | 从元素中移除一个、多个或者所有CSS类 | ||
removeData() | 从元素中移除先前存储的数据 | ||
replaceWith() | 使用提供的新内容替换元素 | ||
text() | 获取或者设置元素中合并的文本内容 | ||
toggleClass() | 从元素中添加或者移除一个或者多个CSS类 | ||
triggerHandler() | 传递假的事件对象给handler。执行附加给元素的某个事件的所有事件处理程序 | ||
unbind() | 不支持namespaces,eventData。通过名称移除一个事件处理程序 | ||
val() | 获取或设置元素的当前值 | ||
wrap() | 使用指定的HTML结构包裹元素 |
jqLite提供额外的方法:
方法 | 说明 |
controller(name) | 返回当前元素或者其祖先元素关联的控制器,默认获取关联到ngController指令的控制器,如果提供name参数,则name指定的控制器被返回 |
injector() | 返回当前元素或者其祖先元素的injector |
scope() | 返回当前元素或者其祖先元素关联的作用域对象 |
isolateScope() | 判断当前元素是否直接关联了一个隔离作用域 |
inheritedData() | 类似data(),但是会沿着DOM树上溯寻找,知道根元素 |
此外jqLite还提供了 $destroy 事件,在DOM节点被移除时,该事件触发。
本节使用简单的付款应用的例子,介绍AngularJS的基本概念。
下面是付款应用最初的版本,运行后,可以观察到表单字段和总计金额的自动联动关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<body> <!-- ng-app指令用于初始化Angular应用 --> <!-- ng-init指令用于执行初始化动作 --> <div ng-app ng-init="qty=1;cost=2"> <b>发货单:</b> <div> <!-- ng-model绑定表单元素到模型的字段,可以认为模型是一个单独的名字空间 --> 数量: <input type="number" min="0" ng-model="qty"> </div> <div> 金额: <input type="number" min="0" ng-model="cost"> </div> <div> <!-- 双括号标记,表达式 | 过滤器形式。可以访问模型字段并进行JavaScript运算 --> <b>总计:</b> {{qty * cost | currency}} </div> </div> </body> |
下面我们为付款应用添加一个AngularJS模块,并注册一个控制器类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//声明一个AngularJS模块,声明后的模块可以被ng-app引用 var app = angular.module( 'invoiceModule', [] ); //注册一个控制器类 app.controller( 'InvoiceController', function() { //构造函数 //成员变量 this.qty = 1; this.cost = 2; this.inCurr = '欧元'; this.currencies = [ '美元', '欧元', '人民币' ]; this.usdToForeignRates = { '美元' : 1, '欧元' : 0.74, '人民币' : 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( "谢谢!" ); }; } ); |
修改HTML文件,使用控制器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script type="text/javascript" src="lib/angular.js"></script> <script type="text/javascript" src="invoice-module.js"></script> </head> <body> <!-- 可以指定ng-app使用的主模块(Main module),AngularJS启动时,会使用初始化该模块,以及它依赖的所有其它模块--> <!-- ng-controller 指定该ng-app对应的控制器,该控制器对所有子元素负责 --> <!-- InvoiceController as invoice 相当于在当前Scope定义var invoice = new InvoiceController(); --> <div ng-app="invoiceModule" ng-controller="InvoiceController as invoice"> <b>发货单:</b> <div> <!-- ng-model可以绑定控制器的成员变量 --> 数量: <input type="number" min="0" ng-model="invoice.qty" required> </div> <div> 金额: <input type="number" min="0" ng-model="invoice.cost" required> </div> <div> 币种: <select ng-model="invoice.inCurr"> <!-- ng-repeat用于重复指令所在元素,c作为当前迭代元素 --> <option ng-repeat="c in invoice.currencies">{{c}}</option> </select> </div> <div> <b>总计:</b> <span ng-repeat="c in invoice.currencies"> {{invoice.total(c) | currency:c}} </span> <!-- ng-click,当按钮被点击时,执行的处理函数 --> <button class="btn" ng-click="invoice.pay()">付款</button> </div> </div> </body> </html> |
付款应用第二版的例子中,所有的逻辑都在控制器中。当应用程序变得复杂时,最好把视图无关的(View-independent)业务逻辑代码移动到服务层中,实现表现层与服务层的分离。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
angular.module( 'finance', [/*不依赖其它模块*/] ) //下面声明了一个名为currencyConverter的对象工厂,调用工厂函数将返回一个currencyConverter服务 .factory( 'currencyConverter', function() { //这是工厂函数,不是构造器 var currencies = [ '美元', '欧元', '人民币' ]; var usdToForeignRates = { '美元' : 1, '欧元' : 0.74, '人民币' : 6.09 }; var convert = function( amount, inCurr, outCurr ) { return amount * usdToForeignRates[outCurr] / usdToForeignRates[inCurr]; }; //这个对象才是currencyConverter服务,它包含一个属性、一个方法: return { currencies : currencies, convert : convert }; } ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
angular.module( 'invoiceModule', //模块名 [ 'finance' ] //所依赖的模块的列表 ) .controller( 'InvoiceController', [ //数组,前面是需要注入的组件的声明,最后是当前组件的构造函数 //这里会发生依赖注入,AngularJS发现currencyConverter是一个工厂,会调用它并生成服务对象,然后注入到InvoiceController的构造函数 'currencyConverter', //最后一个是控制器的构造函数,前面的这是依次需要注入的组件 function( currencyConverter ) { this.qty = 1; this.cost = 2; this.inCurr = '欧元'; 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( "谢谢!" ); }; } ] ); |
通常,Web应用都需要与后端HTTP服务进行通信,我们进一步改造付款应用。在这一版本中,我们让Service基于Ajax与后端通信:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
angular.module( 'finance', [] ).factory( 'currencyConverter', [ '$http', // 服务工厂需要注入此依赖 function( $http ) { var currencies = [ '美元', '欧元', '人民币' ]; var usdToForeignRates = {}; // 我们将从后端服务获得汇率列表 var convert = function( amount, inCurr, outCurr ) { return amount * usdToForeignRates[outCurr] / usdToForeignRates[inCurr]; }; //获取汇率 var refresh = function() { //通过jsonp执行跨域请求 return $http.jsonp( BACKEND_URL ).then( function( response ) { //回调函数 usdToForeignRates = response.data.query.results.rate; } ); }; refresh(); return { currencies : currencies, convert : convert }; } ] ); |
可以认为模块是多种应用程序组件——控制器、服务、过滤器、指令,等等——的容器。
传统的应用程序具有Main函数作为入口点,通过该入口点,可以实例化应用程序的其他组件。Angular没有这的入口点,它只是通过模块机制,声明式的指定应用程序该如何启动。这种方式具有以下优点:
- 声明性的过程简洁,易于理解
- 可以保持全局命名空间的清洁
- 可以通过模块组织可重用组件
- 单元测试中可以仅加载需要的代码(模块)
对于大规模的应用程序,你应该分割出多个模块:
- 每个特性(Feature)设置一个模块
- 每个可重用组件一个模块,特别是指令和过滤器
- 应用程序主模块,作为应用入口,依赖于上述模块,包含初始化代码
每个HTML页面可以对应一个AngularJS应用,通过指令 ng-app 可以指定该应用使用的Angular模块,此模块即主模块。
ng-app指令可以定义在任何HTML节点上,这意味着你可以让页面的一部分使用Angular实现,其它部分使用别的JavaScript框架。
在模块启动期间,Angular可以执行一系列配置、运行块:
- Config块:在Provider注册和配置阶段被执行。只有Provider、Constant可以被注入到Configuration块,这可以避免在服务尚未配置好之前意外的初始化它们。配置块调用方式为:
module.config(function(injectables){}); - Run块:在Injector创建好之后执行,用来kickstart应用程序。只有Instance、Constant可以被注入到Run块,这可以避免在应用程序运行期间再进行系统配置。运行快的调用方式为:
module.run(function(injectables){});
Angular提供了一系列的快捷方法,调用它们和执行Config块是等价的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
angular.module('app', []). value('a', 123). factory('a', function() { return 123; }). directive('directiveName', ...). filter('filterName', ...); //和下面的代码等价 angular.module('myModule', []). config(function($provide, $compileProvider, $filterProvider) { $provide.value('a', 123); $provide.factory('a', function() { return 123; }); $compileProvider.directive('directiveName', ...); $filterProvider.register('filterName', ...); }); |
上述代码向Angular(的Injector)注册若干组件(值、服务、指令、过滤器),确切的说,是注册组件的Provider。Provider告知了Angular如何创建这些组件,你可以简单的把Provider理解为工厂,本文后续章节会详细介绍Provider。
Run块是和Main函数最相似的组件,它在所有服务均已配置、Injector已经创建后执行。在Angular中,Run块通常存放一些难于单元测试的代码。
模块可以依赖于(require)其他模块,被依赖模块的Config块,总是先于依赖它的模块的Config块执行。此规则同样适合Run块。
每个模块只能被加载一次,即使很多模块都依赖于它。
语句: angular.module('myModule', []) 会重新定义myModule模块,被覆盖模块管理的所有组件均被销毁。
语句: angular.module('myModule') 则会找回已经存在的myModule模块的引用,如果myModule模块尚未定义,则抛出异常。
所谓模板就是添加了Angular特殊标记的HTML。这些特殊标记包括:指令、双括号标记、过滤器、表单控件(用于验证用户输入)。
在复杂的应用中,Angular支持在单个主页面下,使用多个模板片断(partial templates)来显示多个应用视图。所谓模板片断,就是分散在不同HTML文件中的模板片段。你可以使用 ngView 指令,基于传递给 $route 服务的配置来加载模板片断。
在Angular中,视图就是模型在模板上的投影。就好像你的影子会随着你走动一样,模型的任何变化也会体现到视图上。
在Angular中,控制器是一类特殊的服务,其增强了视图的功能:
- 控制器可以提供行为,供视图中事件监听指令调用
- 控制器可以方便的扩展$scope,添加模型属性
ng-controller 指令会导致一个新创建的控制器实例关联到对应的DOM元素。Angular会创建一个新的Child Scope,该Scope隐式的关联到控制器对象。 $scope 代表此Scope,可以作为控制器的构造函数参数注入。 控制器实例的变量名(as varName)也作为$scope的属性。
应该避免使用控制器来:
- 操控DOM。控制器应当仅仅包含业务逻辑,如果其中包含任何展现层逻辑,会严重的影响可测试性。使用数据绑定、指令可以封装大部分DOM操作
- 格式化输入。可以使用Angular表单控件来完成
- 过滤输出。可以使用Angular过滤器来完成
- 在控制器之间共享代码或状态。可以使用Angular Service完成
- 管理其它Angular组件的生命周期,例如创建Service实例
通过为$scope添加必要的属性来初始化之,这些属性全部可以在HTML子元素的标记中访问:
1 2 3 4 |
angular.module('myApp').controller('GreetingController', ['$scope', function($scope) { $scope.greeting = 'Hola!'; //为$scope添加状态 $scope.double = function(value) { return value * 2; }; //为$scope添加行为 }]); |
1 2 3 4 5 |
<!-- 子元素都可以访问$scope,可以直接访问属性、方法 --> <div ng-controller="GreetingController"> {{ greeting }} <input ng-model="num"> equals {{ double(num) }} </div> |
控制器关联的DOM元素可以具有嵌套关系,相应的,这些控制器对应的Scope也具有嵌套关系,这类似于块级作用域:
1 2 3 4 5 6 7 8 9 10 11 |
<div ng-controller="MainController"> <p>Hello, {{name}}!</p> <div ng-controller="ChildController"> <p>Hello,{{name}}!</p> <div ng-controller="GrandChildController"> <!-- GrandChildController的$如果定义了$scope.name,则使用,否则到外层scope中搜索 --> <p>Hello, {{name}}!</p> </div> </div> </div> |
Angular中的服务是通过DI组织在一起的一群可替换对象,用于在应用程序中组织和共享代码。服务具有以下特性:
- 延迟初始化:仅当某个组件依赖于它,服务才被创建
- 单例:所有依赖者都获得同一个服务实例的引用
Angular内置了若干重要的服务,例如$http。
开发人员可以定义自己的服务,并且通过一个服务名称、服务工厂函数,将其关联到某个模块。服务工厂函数可以返回一个对象/函数,此返回值(而非工厂函数本身)将被注入到依赖者中。
与所有Angular组件一样,服务也可以声明自己的依赖项。
可以在模块的config函数中,通过$provide服务注册新的服务:
1 2 3 4 5 |
angular.module( 'myModule', [] ).config( [ '$provide', function( $provide ) { $provide.factory( 'serviceId', function() { return new Object(); } ); } ] ); |
这种方式常用在单元测试中,仿冒被测试组件的依赖项。
Scope是应用程序数据模型(Data Model,简称Model)的容器,某些时候你可以认为它和数据模型是同义词。同时Scope也是表达式的执行上下文。Scope形成嵌套的、层次性结构,就像DOM结构本身一样。Scope能够用于监控表达式、传播事件。
Scope充当模板(视图)、模型、控制器之间的粘合剂,它让模型和视图解耦,却又能保持同步。
- Scope提供了 $watch 函数,用于观察模型的变化。利用此API你可以把模型变更通知给整个Angular应用甚至Angular领域之外
- Scope提供了 $apply 函数,应用来自Angular领域之外(控制器、服务、Angular事件处理器)的模型变化到视图组件
通过嵌套,可以限制Scope对应用组件属性的访问,同时允许Scope对共享模型属性的访问。内嵌的Scope分为两类:
- 子作用域(Child scopes):利用JavaScript原型机制,从父Scope继承属性
- 隔离作用域(Isolate scopes):不从父Scope继承属性
Scope的(自定义)属性,就是数据模型的属性。
Scope是控制器和视图之间的桥梁。在模板链接期间,指令在Scope上创建$watch表达式。$watch允许指令在属性变化时获得通知,指令因而能够更新DOM的渲染。
尽管控制器和视图都引用Scope,但是它们绝不互相引用。这确保了可测试性。视图应当仅仅和Scope相关,从中获得渲染所需数据。
每个Angular应用程序都包含唯一的根Scope,此根Scope下可以有多个Child Scope。你可以注入 $rootScope 以使用根Scope。某些指令会自动创建新的Child Scope,新创建的Scope会自动加入到Scope树中。Scope树与其节点关联的DOM树,呈并行对应关系。
当Angular去估算一个表达式 {{varName}} 时,它会从表达式所在DOM元素关联的Scope上寻找varName的定义,如果找不到,则到父Scope中寻找,直到根Scope。当一个DOM元素关联到某个Scope时,Angular自动为其添加样式类 ng-scope 。
下面的例子会创建两层、三个Scope:
1 2 |
<body ng-app ng-init="names = ['Alex','Wong']"> <!-- 根Scope --> <span ng-repeat="name in names">{{name}}</span> <!-- 重复指令,每次循环创建一个Scope --> |
上述模板生成的DOM结构如下:
1 2 3 4 5 6 7 8 9 |
<html ng-app="" ng-init="names = ['Alex','Wong']" class="ng-scope"> <body> <!-- ngRepeat: name in names --> <span ng-repeat="name in names" class="ng-binding ng-scope">Alex</span> <!-- end ngRepeat: name in names --> <span ng-repeat="name in names" class="ng-binding ng-scope">Wong</span> <!-- end ngRepeat: name in names --> </body> </html> |
你可以通过 angular.element(aDomElement).scope() 获得某个DOM元素关联的Scope。
Angular的事件系统是以Scope为中心的,Scope负责向外发出事件。尽管不同Scope可以通过共享变量进行通信,但是你应当更依赖事件机制,以便解耦不同应用组件。
Scope的事件传播方式与DOM事件类似:
- 向下广播( $broadcast(eventName,eventArgs) ):沿Scope链,向下传播到所有子代作用域。如果从$rootScope上调用,相当于全局广播
- 向上冒泡( $emit(eventName,eventArgs) ):沿作用域链,向上传播到所有祖先作用域
在可以传播事件的同时,Scope还支持注册事件监听器: $on(eventName,function listener(event, eventArgs)); ,该函数的返回值是一个“反注册函数”,调用它即可取消事件监听器:。从监听器里面抛出的异常,会全部传递到 $exceptionHandler 服务处理。监听器的 event 入参即事件对象,具有以下属性/方法:
属性/方法 | 说明 |
targetScope | 发出该事件的原始Scope对象 |
currentScope | 当前监听器所在的Scope对象 |
name | 事件的名称 |
stopPropagation() | 禁止冒泡事件进一步向上传播 |
preventDefault() | 设置 defaultPrevented 标记为true,这样作用域链上后续的监听器可以依据此标记来决定需要执行的动作 |
defaultPrevented | 一个标记位 |
下面是Scope事件传播的一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<script type="text/javascript"> angular.module( 'eventModule', [] ).controller( 'EventController', [ '$scope', function( $scope ) { $scope.count = 0; //注册事件监听器 $scope.$on( 'HelloEvent', function() { $scope.count++; } ); } ] ); </script> </head> <body ng-app="eventModule"> <div ng-controller="EventController"><!-- 每个ng-controller都会创建自己的实例 --> <!-- Root Scope --> Root scope <tt>HelloEvent</tt> count: {{count}} <ul> <li ng-repeat="i in [1]" ng-controller="EventController"> <button ng-click="$emit('MyEvent')">Emit</button> <!-- 触发事件,向上传播 --> <button ng-click="$broadcast('MyEvent')">Broadcast</button> <br> <!-- 触发事件,向下传播 --> Middle scope <tt>HelloEvent</tt> count: {{count}} <!-- Current Scope --> <ul> <!-- Child Scope --> <li ng-repeat="item in [1, 2]" ng-controller="EventController">Leaf scope <tt>HelloEvent</tt> count: {{count}} </li> </ul> </li> </ul> </div> </body> |
- 创建:$rootScope是在应用启动时,由$Injector自动创建的;在模板链接阶段,某些指令(ng-controller、ng-repeat等)会创建Child scope
- 观察者注册:在模板链接阶段,指令会通过 $watch(watchExpression, listener) 注册观察者(Watcher)。watchExpression可以是Scope的一个属性、一个表达式的文本、或者一个函数(函数的返回值作为需要监控的值),listener只会在watchExpression当前值与先前值不相等时执行。很多指令会隐式的注册观察者,例如ngModel、{{}}、ngBind等
- 修改模型:要保证模型的修改被正确的监控(observed)到,应当使用
scope.$apply() 来执行修改。Angular的API会自动调用$apply,因此在控制器中同步的修改模型或者通过$http、$timeout、$interval 等服务异步修改模型时,不需要显式调用该方法。该方法的规格如下:
12345/*** @param expr 字符串表达式或者回调函数,回调函数唯一入参是当前$scope* @returns 表达式或者回调函数的执行结果*/function $apply(expr){} - 监控模型的修改:在$apply调用的结尾,Angular会在$rootScope上直接执行一个 $digest() 循环,此操作会传播到所有Child scope上。所有被观察的表达式(watchExpression)被重新估算,以确认修改是否发生(脏值检查)。如果发生,则执行listener指定的逻辑
- 销毁 :当Child scope不再需要,Angular会调用 scope.$destroy() 以销毁之。销毁后,$digest操作不会在传播到当前Child scope,并且Child scope模型所占用的内存空间可以被回收
依据其与Scope交互的方式,可以把指令分为两类:
- 负责观察的指令(Observing directives):例如{{双括号表达式}},它们通过$watch注册一个监听函数,每当表达式的值发生变化,这类指令都会收到通知,以便更新视图
- 负责监听(用户操作)的指令:例如ng-click,它们注册DOM事件的监听函数,当DOM事件发生时,它们使用$apply()方法执行关联的表达式并更新视图
再次强调:使用不属于Angular领域的方式操控模型,必须通过$apply()方法完成,否则监听器可能无法正常的更新视图。
Scope与控制器的交互主要有以下几种情况:
- 控制器利用Scope向模板暴露方法
- 控制器定义可以改变模型(Scope的属性)的方法
- 控制器可以注册模型的观察者(Watcher)
在Scope上进行脏检查是很频繁的操作,因此$watch指定的脏检查表达式应当是高效的,不能包含针对DOM元素的操作,因为DOM操作相对于JavaScript属性访问,速度有数量级的差距。
此外,不要在控制器中显式调用作用域的$watch方法, 这导致可测试性降低。
脏检查可以依据三种策略完成:
- 基于引用的检查,通过 scope.$watch (exp, listener, false) 指定此策略。仅当表达式的返回值变为一个新的对象时,才认为数据已脏。也就是说,集合内部增减元素,不认为集合变脏
- 基于集合内容的检查,通过调用 scope.$watchCollection(exp,listener) 指定此策略。如果表达式返回值是一集合,当添加元素、删除元素、改变元素顺序时,也认为集合变脏
- 基于值的检查,通过调用 scope.$watch(exp,listener,true) 指定此策略。表达式内任何嵌套的对象发生变化时,都能检测为脏数据。这种方式最强大也最昂贵,在每次digest时,被观察表达式的整个对象结构需要被遍历
如下图,左侧显示的浏览器事件循环,右侧显示了AngularJS对浏览器事件循环的扩展:
浏览器事件循环:
- 首先需要明确一点,浏览器中的JavaScript脚本执行是“单线程”的,这意味着事件需要一件件处理,事件监听函数需要一个个执行
- 事件循环(Event-loop)等待一个事件的到达,这些事件可以是用户交互、定时器事件、网络事件(来自服务器的响应)
- 事件的回调函数被执行。此时进入JavaScript上下文。回调函数可能修改DOM的结构
- 回调执行完毕后,浏览器退出JavaScript上下文,并依据修改后的DOM结构重新渲染视图
Angular修改了上述第二步,把JavaScript上下文分割为:经典的JavaScript上下文、Angular执行上下文两个部分。只有在Angular上下文中执行的操作才能受益于Angular数据绑定、异常处理、属性监控。通过$apply可以进入Angular上下文。
Angular执行上下文的工作流流程如下:
- 调用scope.$apply(fn)进入Angular执行上下文。fn是你期望在Angular执行上下文中完成的操作
- Angular执行fn,通常会改变应用程序状态
- Angular进入Digest循环。该循环由两个更小的循环组成,分别处理$evalAsync队列、$watch队列。Digest循环会迭代执行,直到模型变得“稳定”——$evalAsync队列为空、$watch不包含任何脏数据。Digest循环之所以需要反复执行,是因为某个表达式的估算可能存在副作用——导致其它表达式的值发生改变
- $evalAsync 队列用于调度那些需要在当执行上下文外部完成的工作。例如:指令、控制器直接调用 $evalAsync(expr) 后,expr会在Angular操控DOM之后,浏览器渲染之前被执行
- $watch列表由前文介绍的 scope.$watch() 注册的表达式组成,这些表达式在上一次迭代后可能发生变化。如果检测到变化,则对应的监听函数被执行,典型操作是更新DOM
- 一旦Digest循环迭代完毕,退出Angular执行上下文、JavaScript上下文。随后浏览器渲染视图,反映出DOM更新
Angular的Injector子系统负责组件管理:创建组件、解析依赖、注入依赖。
依赖注入在Angular中广泛应用,你可以在定义一个组件时,以及调用模块的 run 、 config 方法时执行依赖注入:
- 通过工厂函数或者构造函数定义的服务、指令、过滤器、动画等组件,可以注入service、value类型的组件作为依赖
12345678910111213//通过工厂函数定义组件:angular.module('myModule', [])//第二个参数如果为数组,那么除了最后一个,都是需要注入的依赖.factory('serviceId', ['depService', function(depService) {//服务工厂函数}]).directive('directiveName', ['depService', function(depService) {//指令工厂函数}]).filter('filterName', ['depService', function(depService) {//过滤器工厂函数}]); - 通过构造函数定义的控制器,可以注入service、value类型的组件作为依赖。并且可以注入其它特殊的依赖,例如$scope:
1234someModule.controller('MyCtrl', ['$scope', 'dep1', 'dep2', function($scope, dep1, dep2) {$scope.aMethod = function() {}}]); - run方法支持一个回调函数作为入参,在回调中,你可以注入service、value、constant类型的组件
- config方法支持一个回调函数作为入参,在回调中,你可以注入provider、constant类型的组件。run、config方法的例子如下:
123456789angular.module( 'myModule', [] )//指定在模块配置阶段执行的函数.config( [ 'depProvider', function( depProvider ) {} ] )//指定在模块运行期间执行的函数.run( [ 'depService', function( depService ) {} ] );
有三种方式可以告知Injector,组件需要哪些依赖:
- 推荐的方式:内联数组记法,为模块的相应方法(controller、factory等)第二个参数传入数组,其最后一个元素是工厂/构造函数,其余元素都是字符串形式的依赖声明:
12module.controller( 'HelloCtrler', [ '$scope', 'greeter', function( $scope, greeter ) {} ] ); - 使用$inject属性执行依赖注入:
123var HelloCtrler = function( $scope, greeter ) {};HelloCtrler.$inject = [ '$scope', 'greeter' ];someModule.controller( 'HelloCtrler', HelloCtrler ); - 隐式注入。这种方式最简单,但是不支持压缩JavaScript:
123module.controller('HelloCtrler', function($scope, greeter) {//依据构造函数入参名称,自动寻找对应的依赖,不需要字符串显式说明依赖项的名称});使用 ng-strict-di 指令可以禁用隐式注入。手工启动Angular时,可以通过如下方式禁用:
123angular.bootstrap(document, ['app'], {strictDi: true});
Angular表达式类似于JavaScript表达式。表达式主要用于插值绑定,例如
<span title="attr-{{ attrBinding }}">{{ textBinding }}</span> ,以及指令属性,例如
ng-click="functionExpression()"
- 上下文:JavaScript表达式的上下文对象是Window;Angular表达式的上下文对象是一个scope对象
- null/undefined安全性:Angular对点号导航中出现null/undefined的表达式,解析为null/undefined;JavaScript则抛出ReferenceError/TypeError。在Angular中,表达式 {{a.b.c}} 和 {{((a||{}).b||{}).c}} 是等价的
- 控制流:Angular表达式不支持流程控制语句,例如条件分支、循环、异常处理。三元操作符 a ? b : c 是支持的
- 函数声明:Angular表达式不支持函数声明
- 正则式直接量:Angular表达式不支持声明正则式直接量
- 创建对象:Angular表达式不支持通过new创建新对象
- 其它:Angular不支持逗号操作符、void操作符
- 过滤器:Angular支持类似于Linux Shell管道的语法,在表达式中指定过滤器,以格式化输出
如果要手工估算一个Angular表达式的值,可以调用 $eval() 函数。
Angular在内部使用 $parse 服务,而不是eval()来估算表达式的值。Angular有意的禁止对window、location、document等全局对象的访问,防止引入缺陷或者降低可测试性。
你可以使用 $window 、 $location 等服务代替那些全局对象,这些服务支持仿冒(Mock),利于单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 |
<script type="text/javascript"> angular.module( 'app', [] ).controller( 'HelloCtrler', function( $window, $scope ) { $scope.name = 'World'; $scope.greet = function() { $window.alert( 'Hello ' + $scope.name ); }; } ); </script> </head> <body ng-app="app" ng-controller="HelloCtrler"> <button ng-click="greet()">Greet</button> </body> |
ngClick、ngFocus等指令在其值表达式的当前上下文中暴露了一个 $event 对象。如果你同时使用jQuery那么该对象是一个jQuery事件对象;否则,它是一个jqLite对象。
所谓一次性绑定是指,表达式仅仅在第一次digest时估算一次,如果其值是非undefined,则以后不再估算。使用语法 {{::expression}} 声明一次性绑定。
一次性绑定可以用于减少资源消耗,因为它不会在后续的digest循环中被重新估算。
注意,如果绑定表达式中包含对象直接量,那么只有对象直接量的所有属性均非undefined,才停止后续估算。
插值绑定用于为HTML属性、文本提供值,或者部分值。
在编译阶段,Angular编译器利用 $interpolate 服务检查文本节点、元素属性是否包含插值标记(即双括号标记)。如果包含,则为节点添加interpolate指令,并注册watcher。
对于值类型为布尔的HTML属性,例如disabled,浏览器可能仅仅检查属性的存在性,而不看它的值,这导致 disabled="false" 也会导致禁用。因此,你不能使用 disabled="{{isDisabled}}" 方式进行绑定。
Angular提供了若干特殊指令,例如 ng-disabled 、 ng-readonly 、 ng-selected 、 ng-checked 专门用于解决布尔值绑定问题。
某些浏览器会对属性值有效性进行验证,因此使用插值绑定可能导致错误,例如: <svg><circle cx="{{cx}}"></circle></svg> 。要解决此问题,可以使用 ng-attr- 前缀: <circle ng-attr-cx="{{cx}}"></circle> 。
对于驼峰式大小写属性,使用此前缀语法时,需要在大写字母前面加下划线,并把大写字母改为小写。例如绑定 viewBox 属性,需使用 ng-attr-view_box
应当避免这样的用法: <div ng-show="form{{$index}}.$invalid"></div> 。这增加了标记的复杂性,不一定对所有指令生效,而且影响性能。
可以通过依赖注入使用$interpolate,它是一个函数:
1 2 3 4 5 6 |
/** * @param text 包含{{}}标记的字符串 * @param mustHaveExpression 如果为true,则text不包含表达式时返回null * @param trustedContext 用于安全性控制 */ function $interpolate(text, mustHaveExpression, trustedContext){} |
通过$interpolateProvider可以改变插值表达式相关的配置,例如:
1 2 3 4 5 |
module.config( function( $interpolateProvider ) { //修改插值表达式的起始、终止符号 $interpolateProvider.startSymbol( '__' ); $interpolateProvider.endSymbol( '__' ); } ); |
在Angular中,过滤器用于格式化表达式,并显示给用户。你可以在模板、控制器、服务中使用过滤器。过滤器可能很方便的自定义,其对应的API是定义在ng模块的provider: $filterProvider 。
语法格式示例:
1 2 3 4 5 6 7 8 9 |
{{ expression | filter }} <!-- 过滤器的结果可以传递给其它过滤器 --> {{ expression | filter1 | filter2 | ... }} <!-- 过滤器支持入参 --> {{ expression | filter:argument1:argument2:... }} <!-- 格式化数字,保留2位小数,结果为1,234.00--> {{ 1234 | number:2 }} |
要在这些组件中使用过滤器,只需要注入 <filterName>Filter 即可。例如要使用number过滤器,则注入numberFilter。注入的对象是一个函数,调用时,第一个参数是被格式化的值,后续参数是过滤器入参。
定制过滤器很简单,只需要调用模块的 filter(filterName, filterFunction) 方法注册即可。filterFunction必须是一个函数,其第一个入参是被格式化的值,后续入参是过滤器参数。
通常filterFunction应当是“纯函数”——无状态且幂等。Angular依赖这一特征,仅在过滤器输入变化的时候,才执行过滤器。
尽管Angular建议把过滤器函数实现为无状态的,以便Angular执行性能优化,但是你还是可以使用下面的方式把过滤器声明为有状态的:
1 2 3 4 5 6 |
angular.module( 'app', [] ).filter( 'decorate', [ 'decoration', function( decoration ) { function decorateFilter( input ) {...} decorateFilter.$stateful = true; //声明为有状态过滤器 return decorateFilter; } ] ); |
有状态过滤器在每轮Digest循环中都被调用。
Angular包含了若干指令、CSS样式类,用于双向数据绑定和HTML表单的处理。
双向数据绑定的关键指令是ngModel。该指令不但提供了视图、模型之间的双向同步机制,而且提供了额外的API以增强功能。
下面是Angular表单的简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<script type="text/javascript"> angular.module( 'app', [] ).controller( 'Ctrler', [ '$scope', function( $scope ) { $scope.master = {}; $scope.update = function( user ) { $scope.master = angular.copy( user ); }; $scope.reset = function() { $scope.user = angular.copy( $scope.master ); }; $scope.reset(); } ] ); </script> </head> <body ng-app="app"> <div ng-controller="Ctrler"> <form novalidate> <!-- 禁用浏览器原生的表单验证功能 --> 姓名:<input type="text" ng-model="user.name" /><br /> <!-- Angular为HTML5的表单控件类型提供了基本的验证规则, 如果不通过验证,表单元素将不会同步到模型上,输入错误的电子邮件可以观察到这一点 --> 邮件:<input type="email" ng-model="user.email" /><br /> 性别:<input type="radio" ng-model="user.gender" value="male" />男 <input type="radio" ng-model="user.gender" value="female" />女<br /> <input type="button" ng-click="reset()" value="重置" /> <input type="submit" ng-click="update(user)" value="保存" /> </form> <pre>user = {{user | json}}</pre> <pre>master = {{master | json}}</pre> </div> </body> |
ngModel指令添加了若干CSS样式类,用以修饰处于不同状态的表单控件:
CSS样式类 | 说明 |
ng-valid | 对应模型字段有效时的样式 |
ng-invalid | 对应模型字段无效时的样式 |
ng-valid-[key] | 对应模型字段针对特定的验证器有效时的样式,通过$setValidity调用添加 |
ng-invalid-[key] | 对应模型字段针对特定的验证器无效时的样式,通过$setValidity调用添加 |
ng-pristine | 用户从未与之交互过(例如点击它)的控件的样式 |
ng-dirty | 用于已经交互过的控件的样式 |
ng-touched | 控件获得焦点时的样式 |
ng-untouched | 控件失去焦点时的样式 |
ng-pending | 当存在$asyncValidators尚未执行时的样式 |
在Angular中,表单对应了一个FormController的实例,而使用ngModel指令标注的表单控件则对应NgModelController的实例。表单、表单控件的HTML属性name的值,自动作为当前scope下的属性名——此属性就是FormController/NgModelController实例的引用。
上述特性意味着,可以使用Angular标准的绑定原语来访问表单、控件的内部状态。我们可以利用此特性来显示验证错误信息:
- 当用户和控件交互后($touched状态位被设置),显示验证错误信息
- 当前用户提交表单时($submitted状态位被设置),显示验证错误信息,即使用户没有和表单控件交互
下面的代码完善了上一个示例,添加了验证错误信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<script type="text/javascript"> angular.module('app', []) .controller('Ctrler', ['$scope', function($scope) { $scope.master = {}; $scope.update = function(user) { $scope.master = angular.copy(user); }; $scope.reset = function(form) { if (form) { form.$setPristine(); //设置为从未交互状态 form.$setUntouched(); //设置为失焦点状态 } $scope.user = angular.copy($scope.master); }; $scope.reset(); }]); </script> <body ng-app="app"> <div ng-controller="Ctrler" > <form name="form" novalidate> 姓名: <input type="text" ng-model="user.name" name="uName" required="" /> <!-- ng-show指令用于在表达式估算结果为true的时候显示标签。如果结果为false,Angular会给元素添加.ng-hide样式 --> <span ng-show="form.$submitted || form.uName.$touched"> <!-- 如果表单已提交,或uName控件已经被点击过 --> <span ng-show="form.uName.$error.required">请输入你的姓名</span><!-- 如果required验证器失败 --> </span> <br/> 邮件: <input type="email" ng-model="user.email" name="uEmail" required="" /> <span ng-show="form.$submitted || form.uEmail.$touched"> <span ng-show="form.uEmail.$error.required">请输入你的邮件地址</span><!-- 如果required验证器失败 --> <span ng-show="form.uEmail.$error.email">电子邮件格式错误</span><!-- 如果email验证器失败 --> </span> <br /> <input type="button" ng-click="reset(form)" value="重置" /> <input type="submit" ng-click="update(user)" value="保存" /><!-- 提交时,验证机制自动触发--> </form> <pre>user = {{user | json}}</pre> <pre>master = {{master | json}}</pre> </div> </body> |
默认的,表单元素值的任何改变都会立即触发模型更新、表单验证,你可以使用ngModelOptions指令覆盖此行为。例如:
ng-model-options="{ updateOn: 'blur' }"
仅在控件失去焦点时才验证并更新。如果需要在多个事件发生时触发验证,用空格分隔事件名,例如
updateOn: 'default blur' 。
你还可以让更新/验证延迟触发,例如 ng-model-options="{ debounce: 500 }" 会导致500ms后触发更新/验证。这个延迟会同时应用到parsers、validators、以及$dirty、$pristine等模型标记位。
延迟可以和触发事件联用:
1 2 |
<!-- 分别指定default、blur事件时的触发延迟 --> ng-model-options="{ updateOn: 'default blur', debounce: { default: 500, blur: 0 } }" |
Angular为HTML5的input类型:text, number, url, email, date, radio, checkbox提供了基本的验证实现。并且提供了required, pattern, minlength, maxlength, min, max等专用于验证的指令。
具体的验证工作由一个个单独的验证函数(验证器)负责完成。验证函数存放在 ngModelController.$validators 对象中。
验证函数必须满足如下规则:
- 接收modelValue、viewValue作为入参,这两个参数分别代表模型的值、用户输入的值
- 返回布尔值,表示验证是否通过
验证函数在以下时机调用:
- 当输入控件的值变化时,即 $setViewValue 被调用时
- 当模型发生变化时
注意:验证机制在成功运行 $parsers 和 $formatters 之后才会触发。
Angular依据验证函数的返回值,在内部调用 $setValidity 设置控件的有效性。失败的验证器,以其名字作为key,存放在 ngModelController.$error 对象中。
Angular还支持异步验证, ngModelController.$asyncValidators 包含了所有异步验证器。异步验证器主要用在需要通过$http进行后台验证的场景中。异步验证函数必须返回一个 promise 对象,并在验证成功时resolved,失败时rejected。正在进行的异步验证,以其名字作为key,存放在 ngModelController.$pending 对象中。
下面的例子中,定义了两个指令,分别在控件的控制器上注册同步、异步的验证器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<script type="text/javascript"> var app = angular.module( 'app', [] ); var INTEGER_REGEXP = /^\-?\d+$/; //定义新的指令 app.directive( 'integer', function() { return { require : 'ngModel', link : function( scope, elm, attrs, ctrl ) { //添加到验证器的集合中 ctrl.$validators.integer = function( modelValue, viewValue ) { if ( ctrl.$isEmpty( modelValue ) ) { return true; } if ( INTEGER_REGEXP.test( viewValue ) ) { return true; } return false; }; } }; } ); app.directive( 'username', function( $q, $timeout ) { return { require : 'ngModel', link : function( scope, elm, attrs, ctrl ) { var usernames = [ 'Jim', 'John', 'Jill', 'Jackie' ]; //添加到异步验证器的集合中 ctrl.$asyncValidators.username = function( modelValue, viewValue ) { if ( ctrl.$isEmpty( modelValue ) ) { //$q是ng模块中定义的一个服务,用于辅助你异步的调用函数 return $q.when(); //直接返回 } var def = $q.defer(); //创建一个Deferred对象,代表未来会完成的任务 //延迟两秒执行 $timeout( function() { // 仿冒一个延迟到达的后台响应 if ( usernames.indexOf( modelValue ) === -1 ) { def.resolve(); //解决期望的promise为指定的值 } else { def.reject(); //回绝期望的promise为指定的原因 } }, 2000 ); return def.promise; //返回Deferred关联的Promise对象 }; } }; } ); </script> <body ng-app="app"> <form name="form" class="css-form" novalidate> <div> 尺寸: <input type="number" ng-model="size" name="size" min="0" max="10" integer />{{size}} <span ng-show="form.size.$error.integer">尺寸必须是一个整数</span> <span ng-show="form.size.$error.min || form.size.$error.max">尺寸的范围必须在0到10之间!</span> </div> <div> 用户名: <input type="text" ng-model="name" name="name" username />{{name}} <span ng-show="form.name.$pending.username">正在检查用户名的可用性</span> <span ng-show="form.name.$error.username">用户名已经被占用!</span> </div> </form> |
由于验证器只是 ngModelController.$validators 中的属性,因此可以很容易的替换它们,内置验证器也不例外:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
app.directive( 'overwriteEmail', function() { var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@example\.com$/i; return { require : 'ngModel', restrict : '', link : function( scope, elm, attrs, ctrl ) { // 仅当控制器(即NgModelController)存在,且其验证器包含email时,才覆盖 if ( ctrl && ctrl.$validators.email ) { // 覆盖默认的电子邮件验证器 ctrl.$validators.email = function( modelValue ) { return ctrl.$isEmpty( modelValue ) || EMAIL_REGEXP.test( modelValue ); }; } } }; } ); |
上面基于ngShow指令来显示验证错误信息的方式,比较繁琐,Angular 1.3引入了 ngMessages 模块,可以简化验证信息的展示。要使用该模块,你必须下载其JS文件并声明对ngMessages模块的依赖。
1 2 3 4 5 6 7 8 9 10 11 |
<form name="formx"> <!-- 默认每次显示一条消息,ng-messages-multiple则允许显示多条信息 ng-messages-includ可以把子节点的HTML存放到额外的模板中 --> <div class="error" ng-messages="formx.name.$error" ng-messages-multiple> <div ng-message="required">当验证器required失败时显示的信息</div> <div ng-message="minlength">当验证器minlength失败时显示的信息</div> <div ng-message="maxlength">当验证器maxlength失败时显示的信息</div> </div> </form> |
Angular实现了所有基本的HTML表单控件(input、select、textarea),这些控件能够应付大部分的需求。如果你需要更多的灵活性,可以通过指令来编写自己的表单控件。
要使自定义表单控件能够与ngModel联用,并支持双向数据绑定,你必须:
- 为控件对应的ngModelController实现 $render 方法。该方法负责显示传递给 NgModelController.$formatters 的数据
- 每当用户和控件交互时,或者模型需要被更新时,调用 $setViewValue 方法
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<script type="text/javascript"> //声明一个指令,其名称借用HTML5的contenteditable属性 angular.module( 'app', [] ).directive( 'contenteditable', function() { return { require : 'ngModel', link : function( scope, elm, attrs, ctrl ) { // 失去焦点时,把视图的值设置到模型 elm.on( 'blur', function() { ctrl.$setViewValue( elm.html() ); } ); // 渲染函数:把模型反应到视图 ctrl.$render = function() { elm.html( ctrl.$viewValue ); }; // 从视图中初始化模型的值 ctrl.$setViewValue( elm.html() ); } }; } ); </script> <style type="text/css"> div[contentEditable] { border: 1px solid #B0B0B0; cursor: pointer; width: 150px; } </style> </head> <body ng-app="app"> <!-- contenteditable属性,表示内容可编辑的HTML元素,同时也是我们定义的指令 --> <div contenteditable="true" ng-model="content">Hello</div> <pre>model = {{content}}</pre> </body> |
概括的说,指令是DOM元素(属性、元素、注释、CSS类)上的标记。指令通知Angular的HTML编译器( $compile )为DOM元素添加特定的行为,或者对DOM子树进行转换。
Angular自带了很多指令,你也可以创建自己的指令。在启动(Bootstrap)阶段,Angular会遍历整个DOM树,依据DOM匹配的指令进行相应的处理。
指令在模块上通过API: directive(normalizedName, factoryFunc) 进行注册。工厂函数factoryFunc的返回值是一个对象,该对象告知$compile当指令匹配时,需要执行什么样的行为。工厂函数只会在Angular编译器匹配指令到元素时执行一次,你可以在工厂函数中包含初始化逻辑。
选择自己的指令名称时,应当注意避免使用ng前缀,不要选择未来可能成为HTML内置标签名的单词,比避免冲突。
指令可以具有属性,当指令通过DOM元素标记时,指令的属性体现为DOM属性。
当指令是DOM元素声明的一部分时,Angular认为指令匹配元素:
1 2 3 4 5 6 7 |
<!-- input元素匹配ngModel指令 --> <input ng-model="foo"> <input data-ng-model="foo"> <input x-ng_model="foo"> <!-- person元素匹配person指令 --> <person>{{name}}</person> |
指令作为JavaScript对象,我们通常使用它的正规化(normalized)名称:大小写敏感的、驼峰式大小写形式。例如: ngModel 。
但是HTML是大小写不敏感的,因此在DOM中我们使用纯小写来引用指令,并且通常使用短横线分隔风格。例如: ng-model 。
DOM中的指令名称,到正式的指令名称的转换规则如下:
- 去除 x- 或者 data- 前缀
- 把 : 、 - 、 _ 分隔的单词,改为驼峰式大小写
$compile可以基于DOM元素名称、属性、CSS类名、甚至注释来匹配指令,Angular提供的指令支持这全部四种节点。下面的例子示例了在模板的不同位置使用demoDir指令的方式:
1 2 3 4 5 6 7 8 9 10 11 |
<!-- 元素名称 --> <demo-dir></demo-div> <!-- 元素属性 --> <span demo-dir="exp"></span> <!-- 注释--> <!-- directive: demo-dir exp --> <!-- CSS类--> <span class="demo-dir: exp;"></span> |
最好只通过元素名称、 属性来引用指令,这样Angular更容易判断给定的元素匹配什么指令。
注释中携带指令,主要用于指令需要跨越多个兄弟DOM元素的情况。在Angular1.2中引入的 ng-repeat-start 和 ng-repeat-end 可以代替之。
可以使用restrict选项限制指令可以在哪些节点上引用:
1 2 3 4 5 6 7 |
app.directive('myCustomer', function() { return { restrict: 'AE' //默认值:AE表示可以在属性、元素名上引用指令 //A:属性;E:元素;C:CSS类名 }; }); |
决定通过元素名称还是属性来引用指令时,参考以下意见:
- 当为你的模板创建DSL(领域特定语言)时,考虑使用元素名称
- 当装饰既有HTML元素,以增加新功能时,考虑使用属性
如果你有一段反复使用到的HTML片段,你可以通过指令将其封装起来,以便重用,减少重复代码。显示一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<script type="text/javascript"> angular.module( 'app', [] ).controller( 'Ctrler', [ '$scope', function( $scope ) { $scope.user = { name : 'Alex Wong', address : 'No.20 Waterside Pearl Plaza' }; } ] ).directive( 'userInfo', function() { //指令工厂函数返回一个对象,对象的每一个属性称为“选项”(option) return { template : 'Name: {{user.name}} <br/>Address: {{user.address}}' }; } ); </script> <body ng-app="app" ng-controller="Ctrler"> <span user-info></span> </body> |
上面的例子中,我们通过 template 选项指定了模板的内容,如果模板内容很复杂,最好将其存放在独立的HTML文件中,通过 templateUrl 选项加载:
1 2 3 4 5 |
app.directive( 'userInfo', function() { return { templateUrl : '../userInfo.html' }; } ); |
templateUrl选项还可以指定一个返回URL或者HTML模板内容的函数,该函数接受两个参数:指令对应的HTML元素、一个关联到该HTML元素的attr对象。注意在该函数中你不能对Scope进行任何访问,因为模板加载时Scope尚未初始化。下面的例子示例了templateUrl的这种用法:
1 2 3 4 5 6 7 8 9 10 11 |
<script type="text/javascript"> app.directive( 'userInfo', function() { templateUrl: function(elem, attr){ return 'user-info-'+attr.type+'.html'; } } ); </script> <body ng-app="app" ng-controller="Ctrler"> <div user-info type="name"></div> <div user-info type="address"></div> </body> |
上面的例子有个严重的缺陷,注意模板内容: 'Name: {{user.name}} <br/>Address: {{user.address}}' ,它假设了当前Scope存在user属性,这将导致指令难以重用。
Angular允许为指令指定 scope 选项,允许为指令创建一个隔离作用域( isolate scope)。在scope对象内部,你可以使用&、@、=等限定符,指明隔离作用域和外部作用域的映射关系(绑定策略)。
=用于进行双向绑定。建立关联后,在隔离作用域修改变量,该变量在外部作用域中映射的变量同时改变,反之依然。
下面使用隔离作用域,对上一个例子进行改造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<script type="text/javascript"> angular.module( 'app', [] ).controller( 'Ctrler', function( $scope ) { //在当前Scope下定义两个属性 $scope.alex = { name : 'Alex Wong', address : 'No.20 Waterside Pearl Plaza' }; $scope.meng = { name : 'Meng Lee', address : 'No.11 Jinxiu Homeland' }; } ).directive( 'userInfo', function() { return { restrict : 'E', //隔离作用域,创建自己的scope scope : { //=uInfo表示通过HTML属性u-info来指定隔离Scope中info对应外部Scope的哪个属性 info : '=uInfo' // '='等价于'=info'。该属性值必须使用正规化的驼峰式大小写 }, //模板文本或HTML文件,使用指令的隔离作用域 template : 'Name: {{info.name}} Address: {{info.address}}' }; } ); </script> <body ng-app="app" ng-controller="Ctrler"> <!-- info的值,对应当前Scope下的属性名 --> <user-info u-info="alex"></user-info><br/><user-info u-info="meng"></user-info> </body> |
一旦指定了scope选项,那么外部作用域的任何属性,在指令的template中默认都不可用,除非在scope选项中声明。
&允许指令的隔离作用域向外部作用域单向传递变量,用于估算定义在指令属性中的表达式的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<script type="text/javascript"> angular.module( 'app', [] ).controller( 'Ctrler', function( $scope, $timeout ) { $scope.name = 'Alex'; $scope.message = ''; $scope.hideDialog = function( message ) { $scope.message = message; $scope.dialogIsHidden = true; }; } ).directive( 'myDialog', function() { return { scope : { //隔离作用域的close属性,由指令匹配元素的on-close属性指定 //&绑定,允许指令在原始的作用域下执行任意表达式估算,而不是像=绑定那样哪些仅仅执行属性映射 'close' : '&onClose' }, template : '<div class="alert"> ' + //虽然close在隔离作用域下执行,但是onClose指向的表达式在原始作用域$scope下估算 //使用&绑定,可以通过一个映射,传递解析表达式需要的上下文变量,例如这里传递了一个message变量 ' <a href class="close" ng-click="close({message: \'Closed\'})">×</a> ' + ' </div>' }; } ); </script> <body ng-app="app" ng-controller="Ctrler"> {{message}} <my-dialog ng-hide="dialogIsHidden" on-close="hideDialog(message)"></my-dialog> </body> |
设置指令的 transclude 选项为true,则指令内部的任意子节点,访问到的是指令外部(Outside)作用域,而非指令内部作用域。指令的子节点会自动附加为模板中标记了 ng-transclude 元素的内部。注意下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<script type="text/javascript"> angular.module( 'app', [] ).controller( 'Ctrler', [ '$scope', function( $scope ) { $scope.name = 'Alex Wong'; } ] ).directive( 'transcludeDirective', function() { return { restrict : 'AE', transclude : true, scope : {}, //创建该指令自己的作用域 link : function( scope, element ) { //这里的Scope引用新创建的隔离作用域,而非Ctrler创建的作用域 scope.name = 'Meng Lee'; }, //子模板中的绑定: //如果存在隔离作用域,则绑定到隔离作用域中的属性,当然你可以把隔离作用域映射到外部作用域 //如果不存在隔离作用域,则自动绑定到外部作用域 //子模板中的ng-transclude元素,不应有任何内容,会被指令实例的内容所覆盖 //每出现一个ng-transclude,就把指令实例的内容合并为子元素 template : '<div>{{name}}</div><div ng-transclude>Content will be removed</div><p ng-transclude></p>' }; } ); </script> <body ng-app="app" ng-controller="Ctrler"> <!-- 指令实例子节点中的绑定,默认访问的是指令内部作用域,而使用了transclude后,反而访问指令外部的作用域 --> <transclude-directive><div>My name is {{name}}</div></transclude-directive> </body> |
其生成的HTML内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<body ng-app="app" ng-controller="Ctrler" class="ng-scope"> <transclude-directive class="ng-isolate-scope"> <div class="ng-binding">Meng Lee</div> <!-- 绑定到隔离作用域 --> <!-- 下面两个则绑定到外部作用域--> <div ng-transclude=""> <!-- 如果transclude-directive指令内部没有div元素,仅仅是文本节点,那么Angular自动创建一个span包裹文本节点--> <div class="ng-binding ng-scope">My name is Alex Wong</div> </div> <p ng-transclude=""> <div class="ng-binding ng-scope">My name is Alex Wong</div> </p> </transclude-directive> </body> |
Angular的这种特性,可以让指令包裹任意一段可以访问外部作用域的HTML内容,并通过
ng-transclude 将这段HTML嵌入到子模板内部(也就是说,允许通用的模板wrap任意的内容),并且与此同时,允许子模板使用隔离的作用域。
从指令使用者的角度来说,这种特性是合理的:
- 指令使用者并不知道隔离作用域的细节,不应该让他使用隔离作用域
- 子模板工作在隔离的作用域中,可以增强可复用性——不需要对“外部作用域”中存在哪些变量做假设。这使得子模板使用隔离作用域是必要的
- 而用户在使用指令时,会很自然的认为自己在使用“外部作用域”
这种指令开发者、使用者视角的差异,导致了transclude机制的出现。
如果需要让指令能够操控DOM、注册DOM监听器,通常需要指定 link 选项。link选项的值是一个函数,其签名如下:
1 2 3 4 5 6 7 8 9 |
/** * @param scope 一个Angular作用域对象,如果指令创建了隔离作用域,该参数是此隔离作用域,否则是上级作用域 * @param element jqLite元素对象,当前指令匹配的元素 * @param attrs 规范化的元素属性名到其值的映射 * @param controller 指令所在的控制器实例。如果指令本身创建了自己的控制器,则该参数指向这个新的控制器 * @param transcludeFn */ function link( scope, element, attrs, controller, transcludeFn ) { } |
下面的例子中,我们创建一个指令,它依据用户指定的格式,每秒更新当前时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<script type="text/javascript"> angular.module( 'app', [] ).controller( 'Ctrler', function( $scope ) { $scope.format = 'yyyy-mm-dd HH:mm:ss'; } ).directive( 'currentTime', function( $interval, dateFilter ) { //注入一个服务、一个过滤器作为依赖 function link( scope, element, attrs ) { var format, timeoutId; //下面几个闭包的共享状态 function updateTime() { element.text( dateFilter( new Date(), format ) ); //format可以使用scope.format代替 } //对attrs.currentTime指定的表达式——format进行监控,也就是监控当前作用域下的format属性 scope.$watch( attrs.currentTime, function( value ) { //等价于scope.$watch('format',...) format = value; //一旦发生变化,则触发一次更新操作 updateTime(); } ); //开发新指令时,一定要注意执行必要的销毁操作 //元素被销毁时的钩子,当由Angular编译器导出的DOM节点被销毁时,$destroy事件被触发 //类似的,作用域被销毁时,也触发这一事件,你可以 scope.$on('$destroy', ...) element.on( '$destroy', function() { $interval.cancel( timeoutId ); //取消定时器 } ); //你也可以使用element.on添加若干DOM事件监听器 // 开始循环处理,把句柄保存为共享闭包变量 timeoutId = $interval( function() { updateTime(); }, 1000 ); } return { link : link }; } ); </script> <body ng-app="app" ng-controller="Ctrler"> <!-- format仅仅是5个字母的文本,还是另有其义,完全取决于指令如何解释它 下面的两个指令都把它作为表达式看待 --> 时间格式:<input ng-model="format"> <br/> 当前时间:<span current-time="format"></span> </body> |
template、templateUrl相当于指令的View,Angular还允许给指令添加 controller 选项, 这样指令的模板就附带一个控制器了。
下面是一个页签的例子,注意父子指令之间的通信:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
<script type="text/javascript"> angular.module( 'app', [] ).controller('Ctrler',function($scope){ $scope.title1 = 'Title1'; }).directive( 'myTabs', function() { return { transclude : true, scope : {}, //指令可以使用该选项,声明其模板关联的控制器,就像ngController关联控制器到模板一样 controller : [ '$scope', function( $scope ) { var panes = $scope.panes = []; $scope.select = function( pane ) { angular.forEach( panes, function( pane ) { pane.selected = false; } ); pane.selected = true; }; this.addPane = function( pane ) { if ( panes.length === 0 ) { $scope.select( pane ); } panes.push( pane ); }; } ], template : ' <div> ' + ' <span ng-repeat="pane in panes" style="margin-right:5px"> ' + ' <a href="" ng-click="select(pane)">{{pane.title}}</a> ' + ' </span> ' + //指令的内容,全部纳入到下面的div内部 ' <div ng-transclude></div> ' + ' </div> ' }; } ).directive( 'myPane', function() { return { //指令的require选项,表示当前指令要求某个控制器的存在。如果找不到,$compile会抛出错误 //前缀^表示,指令在其上级元素上寻找目标控制器。如果没有该前缀,仅在指令自己的元素上寻找 require : '^myTabs', restrict : 'E', transclude : true, scope : { title : '@' // 即@title,把指令title属性的值传入隔离作用域 }, //注意第四个参数tabsCtrl,它就是require来的控制器。该参数可以和require同时指定为数组 link : function( scope, element, attrs, tabsCtrl ) { tabsCtrl.addPane( scope ); }, template : '<div style="margin-top: 24px" ng-show="selected" ng-transclude></div>' }; } ); </script> <body ng-app="app" ng-controller="Ctrler"> <my-tabs> <my-pane title="{{title1}}"> 面板一内容 </my-pane> <my-pane title="面板二"> 面板二内容 </my-pane> </my-tabs> </body> |
该例子使用到了@限定符, 它允许从外部作用域单向的传递值到隔离作用域,传递的媒介仍然是指令属性,支持 attr="text" 和 attr="text{{expr}}" 两种形式。
选项 | 说明 |
restrict | 指令在DOM中可以何种形式被声明,支持E、A、C、M,可以混合使用 |
priority | 优先级。同一元素如果匹配多个指令,那么优先级数值越大的指令,越先执行。大部分指令忽略此参数 ngRepeat在内置指令中具有最高的优先级 |
terminal | 默认值false,停止运行当前元素上比本指令优先级低的指令 |
template | 指令使用的模板 |
templateUrl | 指令使用的模板URL |
replace | 默认值false,如果设置为true,模板将会替换指令匹配的元素,而不是作为其子元素(默认值) |
scope | 可以设置为true或者一个对象,默认false。 如果设置为true,则从父Scope继承并创建一个新的作用域 如果设置为对象,则创建隔离作用域,可以通过多种绑定策略在在隔离作用域和外部作用域之间进行映射或数据交换 |
transclude | 允许指令内部嵌入任意HTML代码,这些代码访问外部作用域 |
从1.3版本开始Angular为ngRepeat、ngSwitch、ngView等指令提供了动画钩子。
Angular中的动画完全基于CSS样式类,只要你为HTML元素添加了CSS类,就可以应用动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<style> <!-- .sample-show-hide { padding: 10px; border: 1px dashed black; background: #C0C0C0; width: 100px; /* 这里声明CSS变换规则 */ -webkit-transition: all linear 0.5s; transition: all linear 0.5s; } /* 这里指定了隐藏时的样式,每当Angular隐藏div时,会自动添加ng-hide样式 */ .sample-show-hide.ng-hide { opacity: 0; } --> </style> <script type="text/javascript" src="angular.js"></script> <script type="text/javascript" src="angular-animate.js"></script> <!-- 必须引入额外的JS库 --> <script type="text/javascript"> angular.module('app', ['ngAnimate']); //要使用动画,必须依赖ngAnimate模块 </script> </head> <body ng-app="app"><!-- 也可以直接使用ngAnimate模块 --> <div ng-init="checked=true"> <input type="checkbox" ng-model="checked" style="float: left; margin-right: 10px;">隐藏/显示 <div class="sample-show-hide" ng-show="checked" style="clear: both;"></div> </div> </body> |
Angular可以监控通过ngClass添加、移除CSS样式类的动作,并执行对应的动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<style> <!-- .css-class-add, .css-class-remove { -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 5s; -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 5s; -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 5s; } /* 添加样式类后,最终需要变成大红字 */ .css-class, /* 在添加样式类的过程中,*-add、*-add-active类自动添加。它们的动画结果也必须时大红字,以便和css-class一致 */ .css-class-add.css-class-add-active { color: #FF0055; font-size:2em; } .css-class-remove.css-class-remove-active { /* 类似,变换结果是默认元素样式 */ font-size:1em; color: #000000; } --> </style> <p> <input type="button" value="设置" ng-click="myCssVar='css-class'"> <input type="button" value="清空" ng-click="myCssVar=''"> <br> <span ng-class="myCssVar">CSS动画文本</span> </p> |
指令 | 支持的动画 |
ngRepeat | 进入、离开、移动 |
ngView | 进入、离开 |
ngInclude | 进入、离开 |
ngSwitch | 进入、离开 |
ngIf | 进入、离开 |
ngClass | 添加、移除 |
ngShow / ngHide | 添加、移除 |
可以调用注入的 $animate 服务,并执行动画:
1 2 3 4 5 6 7 8 9 10 11 12 |
module.directive( 'directive', function( $animate ) { return function( scope, element, attrs ) { element.on( 'click', function() { if ( element.hasClass( 'clicked' ) ) { $animate.removeClass( element, 'clicked' ); } else { $animate.addClass( element, 'clicked' ); } } ); }; }); |
指令可以扩展既有HTML元素或者属性的行为,甚至创建具有新行为的HTML或属性。这样,在利用到HTML这一“声明式”语言的优势的基础上,我们可以对它进行无限扩展。
Angular的 HTML编译器,本质上是一个Angular服务,它的关键工作就是遍历整个DOM树,处理各种各样的指令。编译过程分为两个阶段:
- 编译(Compile):遍历DOM树,收集到所有指令。本阶段的产出是一个链接(Linking)函数。在此阶段,每个指令的compile函数被调用,任何可以跨指令实例的操作都应该放在compile中,而不是link中,以增强性能
- 链接(Link):通过把指令与特定Scope联合,来产生一个活动的视图——Scope上发生的任何变化,都会立即反应到视图上;用户与视图的任何交互,也会立即反应到Scope的模型字段上。在此阶段,每个指令的link函数被调用
这种编译、链接分离的设计,有助于提高性能。例如ngRepeat指令需要为循环中的每一个条目复制DOM元素一次,它只需要复制编译后的模板,而链接多次(编译一次)。
- $compile遍历DOM,并匹配指令。如果一个指令匹配到元素,它会被加到元素的指令列表中,一个元素可以匹配多个指令
- 当元素匹配的所有指令都被识别出来后,$compile依据指令的priority对它们进行排序
- 每个指令的compile函数被执行,该函数可以修改DOM结构,compile函数返回一个link函数
- 每个指令返回的link函数被串联为组合的LINK函数。LINK依次调用link
- $compile调用LINK,让Scope与每个指令链接。在这一步,$compile会注册元素的监听器、设置$watch表达式
链接的结果是一个活动视图,具有双向绑定特性。下面的伪代码可以帮助理解编译过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var $compile = ...; // 被注入的编译器 var scope = ...; var parent = ...; // 编译后的模板的父元素 var html = '<div ng-bind="exp"></div>'; //模板 // 第一步:解析HTML文本为DOM元素 var template = angular.element(html); // 第二步:编译模板。在此指令可以修改DOM结构,但是很少有指令具有compile函数 var linkFn = $compile(template); // 第三步:链接编译后的模板到Scope。克隆一份编译后的副本,与scope链接 var element = linkFn(scope); // 第四步:添加链接后的活动视图到父元素 parent.appendChild(element); |
Angular应用程序启动时,会创建一个Injector服务,该服务负责创建各种对象(包括来自ng模块的、来自应用主模块的、来自直接/间接依赖模块的),并把他们组织在一起(依赖注入)。Injector创建的对象可以分为两类:
- 普通服务(Services ):其API由开发者依据业务需要设计
- 特殊对象(Specialized objects):用于扩展Angular框架,遵从Angular框架的API规范。特殊对象包括:控制器、指令、过滤器、动画
在Angular中,你可以向Injector注册“Recipe”(菜谱),来告知Injector如何创建上述两类对象。Recipe包含两个要素:
- 该Recipe要创建的对象的标识符
- 关于如何创建对象的说明
Recipe分为5种类型,要注册这些Recipe,需要分别调用Angular模块的五种API:
- Provider:这是最通用 的Recipe,其余四种,本质上是基于Provider的语法糖。Provider只是一个实现了
$get 方法的JavaScript对象,这个$get是此Provider所创建对象的工厂函数。Angular内置了很多Provider,供你在配置阶段修改相应Angular服务的行为。只有当你需要在配置阶段(应用尚未启动)时暴露API,才需要使用Provider:
123456789101112var app = angular.module( 'app', [] );//注册提供者app.provider( 'accountServiceProvider', function() {this.debugMode = false;this.$get = [ 'dao', function accountServiceFactory( dao ) {return this.debugMode ? new DebugAccountService() : new AccountService( dao );} ]} );//在应用程序配置阶段,修改提供者的配置app.config( function( accountServiceProvider ) {accountServiceProvider.debugMode = true;} ); - Value:提供一个简单的“值”,可以注入到其它组件中:
123var app = angular.module('app', []);app.value('accountService', new AccountService());app.controller('Ctrler', function (accountService) { this.accountService= accountService; }); //使用服务 - Factory:能够使用其它组件(依赖注入)、支持初始化、延迟初始化,比Value灵活的多。该类型的Recipe给出一个函数,调用的返回值就是要创建的服务的实例。注意Angular中所有服务都是单例的:
123app.factory('accountService', function AccountServiceFactory() {return new AccountService(); //单例。根据业务规则,这里可能有复杂的代码}); - Service:与Factory类似,但是通过调用既有的构造函数创建服务实例:
123456function AccountService(dao){this.dao = dao;...}//第二个参数数组:前面的元素都是依赖,最后是构造函数app.service('accountService',["accountDao", AccountService]); - Constant:类似于Value,但是可以在Config阶段使用,不需要依赖或者初始化的简单对象:
1app.constant('INTERFACE_VER', '1.0');
由于实质上所有Recipe都是Provider,因此Injector执行组件创建、依赖注入的唯一信息来源就是Provider。
在Angular中,路由功能用于管理应用程序的多个视图(Multiple views),并在这些视图之间切换。路由功能位于 ngRoute 模块中,该模块和Angular核心分离,定义在独立的文件 angular-route.js 中。
该服务和路由功能密切相关,它提供了修改URL路径和处理各种形式导航的能力。该服务没有刷新整个页面的能力,可以使用 $window.location 代替。
$location服务提供以下接口:
函数 | 说明 | ||
path() | 获取页面当前的路径,或者跳转到目标URL:
该函数直接和HTML5的历史API交互,所以用户可以点击浏览器后退按钮,返回上一个视图 |
||
replace() | 可以禁止把URL添加到历史记录中:
|
||
absUrl() | 获取编码后的完整URL | ||
hash() | 获取URL中的hash片段 | ||
host() | 获取URL中的主机 | ||
port() | 获取URL中的端口号 | ||
protocol() | 获取URL中的协议 | ||
search() | 获取或者修改URL中的查询串:
|
||
url() | 获取或者设置当前页面的URL:
|
在多视图应用中,所谓布局模板是指包含了所有视图的公共内容的模板。在当前路由(Route)改变时,相应的模板片断(partial templates,或叫视图模板)会被包含到布局模板中。模板片断代表了“当前视图”。
路由的规则通过 $routeProvider 来配置,这是 $route 服务的提供者。通过$route服务,可以轻易的把控制器、模板、浏览器当前URL绑定在一起,这样你就可以实现深链(deep linking)—— 将应用视图关联到URL,并利用浏览器的浏览历史。
$rouet服务通常与 ngView 指令联用,ngView的职责是把当前路由关联的视图模板包含到布局模板中。该指令会创建新的作用域,视图模板使用这一新作用域。该指令优先级为1000,并且禁止同一元素上低优先级的指令。
ngView遵守以下规则:
- 每当发生 $routeChangeSuccess 事件,视图都会更新
- 如果当前路由关联到某个模板:
- 创建一个新的作用域
- 移除上一个视图,同时上一个作用域也会被清除
- 将新的作用域同当前模板关联在一起
- 把路由定义中对应的控制器(如果有的话)和当前作用域关联起来
- 触发 $viewContentLoaded 事件
- 如果提供了 onload 属性,调用该属性所指定的函数
调用$routeProvider的 when(path, route) 函数,可以编写路由规则。其中path匹配 $location.path ,route是一个对象,可以提供以下属性:
属性 | 说明 | ||
controller | 和新的作用域、视图模板关联的控制器。如果指定一个函数,则从该函数创建控制器;如果指定一个字符串,则从模块中查找既有控制器 | ||
template | 模块或者模板URL,Angular会把模板渲染到ng-view指定的元素内部 | ||
templateUrl | |||
resolve | 为控制器注入一系列依赖,如果某个依赖是promise,它会在$routeChangeSuccess触发前被resolve为有效值。举例:
|
||
redirectTo | 执行重定向,可以指定字符串或者函数,函数签名为:
|
||
reloadOnSearch | 默认true,当
location.search() 变化时重新加载路由;如果设置为false,URL查询串那部分的变化不会引起重新加载路由。 该属性对路由嵌套和原地分页等需求非常有用 |
选择不同的路由模式,则URL的风格也不同:
- 标签模式(hashbang mode):
$location 服务默认使用的模式,URL路径以
# 号开头,例如
http://gmem.cc/#!/index 。标签模式不需要重写
<a> 元素的href属性,也不需要任何服务器端的支持。下面的代码手工配置当前应用为标签模式:
1234angular.module( 'app', [ 'ngRoute' ] ).config( function( $locationProvider ) {$locationProvider.html5Mode( false );$locationProvider.hashPrefix( '!' );} ); - HTML5 模式:更加RESTful的URL风格,例如 http://gmem.cc/index 。如果浏览器不支持HTML5,Angular会自动退化为标签模式
HTML5 模式需要后端服务器支持URL重写,使用HTML5模式时,永远不要使用相对路径,否则可能导致Angular无法正确处理路由。
$route 服务在路由过程中的每个阶段都会触发不同的事件,可以为这些不同的路由事件设置监听器并做出响应:
事件 | 说明 | ||
$routeChangeStart | 在路由变化之前会发布该事件,监听函数的规格:
|
||
$routeChangeSuccess | 在路由的依赖被成功加载后发布该事件,监听函数的规格:
|
||
$routeChangeError | 在任何一个promise被拒绝或者失败时发布该事件,监听函数的规格:
|
||
$routeUpdate | 在reloadOnSearch属性被设置为false的情况下,重新使用某个控制器的实例时,发布该事件 |
这里是一个用户管理的简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>User Management App</title> <script type="text/javascript" src="angular.js"></script> <script type="text/javascript" src="angular-route.js"></script> <script type="text/javascript" src="index.js"></script> <style> <!-- body { font-family: Consolas; } table { table-layout: fixed; border: 0; border-spacing: 0; border-collapse: collapse; } td { background-color: #E5E5E5; border: 1px solid #fff; color: #333333; min-width: 100px; padding: 5px 0 0 5px; } thead td,.thead { text-align: center; font-weight: bold; } --> </style> </head> <body ng-app="app"> <div>User Management</div> <div ng-view></div> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<table> <thead> <tr> <td>ID</td> <td>Name</td> </tr> </thead> <tbody> <tr ng-repeat="user in users"> <td>{{user.id}}</td> <td><a href="#/users/{{user.id}}">{{user.name}}</a></td> </tr> </tbody> </table> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<table> <tr> <td class="thead">ID</td> <td>{{user.id}}</td> </tr> <tr> <td class="thead">Name</td> <td>{{user.name}}</td> </tr> <tr> <td class="thead">Age</td> <td>{{user.age}}</td> </tr> <tr> <td class="thead">Address</td> <td>{{user.addr}}</td> </tr> <tr> <td class="thead">E-mail</td> <td>{{user.email}}</td> </tr> </table> <div> <a href="#/users">Back</a> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
'use strict'; angular.module( 'app', [ 'ngRoute' ] ).config( function( $routeProvider ) { // 需要在配置阶段,对路由提供者进行配置 // when用于定义路由,即URL和模板+控制器的对应关系 $routeProvider.when( '/users', { // URL的HASH部分为#/users时 templateUrl : 'partials/users.html', controller : 'UsersCtrler' } ).when( '/users/:id', { // URL的HASH部分为#/users/:id时,:开头表示URL中的路径变量 templateUrl : 'partials/user-detail.html', controller : 'UsersDetailCtrler' } ).otherwise( { // 不匹配已声明的路由规则时的行为 redirectTo : '/users' } ); } ).value( 'usersData', [ { // 简单的静态数据 id : 10000, name : 'Alex Wong', age : 29, addr : 'No.20 Waterside Pearl Plaza', email : 'alex@gmem.cc' }, { id : 10002, name : 'Meng Lee', age : 26, addr : 'No.11 Jinxiu Homeland', email : 'meng@gmem.cc' }, { id : 10003, name : 'Wen Jun', age : 26, addr : 'No.10 YanHuang International', email : 'wenjun@gmem.cc' } ] ).controller( 'UsersCtrler', function( $scope, usersData ) { // 每次切换到视图,控制器都创建一次,并执行这里的代码 $scope.users = usersData; } ).controller( 'UsersDetailCtrler', function( $scope, $routeParams, usersData ) { for ( var i = 0; i < usersData.length; i++ ) { var user = usersData[i]; if ( user.id == $routeParams.id ) $scope.user = user; } } ); // 注意我们把所有Angular组件都定义在这一个模块中了,如果应用规模变大,应当注意职责分离 |
该服务对浏览器原生的XMLHttpRequest进行了简单封装。它实质上是一个函数,其返回值是promise对象,其入参是一个配置对象,你可以使用以下配置项:
配置项 | 说明 | ||
method | HTTP方法,支持:GET、DELETE、HEAD、JSONP、POST、PUT | ||
url | 绝对或相对的URL,请求的目标 | ||
params | 查询参数,字符串或者对象,例如:
|
||
data | 请求体,字符串或者对象,通常在发送POST请求时使用 从1.3开始,支持发送二进制数据:
|
||
headers | 列表,每一个元素都是一个函数,它会返回代表随请求发送的HTTP头 | ||
transformRequest | 一个函数或函数数组,用来对HTTP请求的请求体和头信息进行转换: function(data,headers) {} | ||
transformResponse | 一个函数或函数数组,用来对HTTP响应的响应体和头信息进行转换: function(data,headers) {} | ||
cache | 布尔型或缓存对象。如果设置为true,则Angular使用默认的$http缓存来对GET请求进行缓存。如果设置为 $cacheFactory 的实例,那么该实例被用于对GET请求进行缓存 | ||
timeout | 如果设置为数值,那么请求延迟指定毫秒后再发送 如果设置为promise,那么此promise被resolvehou请求中止 |
||
withCredentials | 默认的CORS请求不会发送cookie,withCredential为true则添加请求头Access-Control-Allow-Credentials,这样目标域的Cookie被包含在请求中 | ||
responseType |
设置XMLHttpRequestResponseType属性,支持:blob、document、json、text等值 |
基本的用法:
1 2 3 4 5 6 7 8 9 |
$http( { //配置选项 method : 'GET', url : '/api/users.json' } ).success( function( data, status, headers, config ) { // 当相应准备就绪时调用 } ).error( function( data, status, headers, config ) { // 当响应以错误状态返回时调用 } ); |
或者:
1 2 3 4 5 6 7 8 9 10 11 |
// $http方法返回一个HttpPromise对象,我们可以调用它的then()、success()和error()方法 var promise = $http( { method : 'GET', url : '/api/users.json' } ); //then会接收到完整的响应对象,而而success()和error()则会对响应进行解析 promise.then( function( resp ) { // resp是一个响应对象 }, function( resp ) { // 带有错误信息的resp } ); |
Angular传递给HttpPromise的 then() 方法的响应对象,包含以下属性:
属性 | 说明 |
data | 转换过后的响应体,如果定义了transformResponse的话 |
status | HTTP状态码 |
headers | 函数,接收请求头名称作为入参,返回请求头的值 |
config | 原始请求的配置选项 |
statusText | HTTP状态文本 |
$http服务提供了一些快捷方法:
方法 | 说明 |
get() | 发送GET请求: $http.get(url,config); ,返回HttpPromise |
delete() | 发送DELETE请求: $http.delete(url,config); ,返回HttpPromise |
head() | 发送HEAD请求: $http.head(url,config); ,返回HttpPromise |
jsonp() | 发送JsonP请求: $http.jsonp(url,config); ,返回HttpPromise。url必须包含指定回调为 JSON_CALLBACK ,例如 /users.json?callback=JSON_CALLBACK |
post() | 发送POST请求: $http.post(url,data,config); ,返回HttpPromise |
put() | 发送PUT请求: $http.put(url,data,config); ,返回HttpPromise |
Angular支持缓存GET请求的结果,可以设置配置选项cache来使用缓存,或者进行全局的缓存配置:
1 2 3 4 5 |
angular.module( 'app', [] ).config( function( $httpProvider, $cacheFactory ) { $httpProvider.defaults.cache = $cacheFactory( 'lru', { //基于LRU算法 capacity : 20 //允许的缓存条目 } ); } ); |
拦截器用于为HTTP请求处理提供全局性的功能,例如身份验证、错误处理等。拦截器在在请求发送前、响应到达后进行执行特定逻辑。
每个拦截器本质上是一个服务,该服务提供四个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
angular.module( 'app', [] ).factory( 'testInterceptor', function( $q ) { var interceptor = { /** * 拦截待发送的请求,可以修改甚至替换配置对象 * * @param config * $http的配置选项 * @returns 配置选项或者配置选项的promise */ 'request' : function( config ) { return config; // 或者 $q.when(config) }, /** * 拦截响应内容,可以修改甚至替换响应 * * @param response * 响应 * @returns 响应或者响应的promise */ 'response' : function( response ) { return response; // 或者 $q.when(config); }, /** * Angular在上一个请求拦截器抛出错误,或者promise被reject时调用此拦截器 */ 'requestError' : function( rejection ) { }, /** * AngularJS在上一个响应拦截器抛出错误,或者promise被reject时调用此拦截器 */ 'responseError' : function( rejection ) { } }; return interceptor; } ); |
可以通过 $httpProvider.interceptors 添加新的拦截器:
1 2 3 |
angular.module( 'app', [] ).config( function( $httpProvider ) { $httpProvider.interceptors.push( 'testInterceptor' ); } ); |
默认的请求头保存在 $httpProvider.defaults.headers对象中,其中common存储通用默认请求头,post、put则分别存储POST、PUT请求的默认请求头。可以在配置阶段修改这些对象。
一个“域”由协议(HTTP/HTTPS)、域名(IP)、端口唯一确定。默认的,浏览器限制一个域中的脚本只能读写本域的资源,此即同源策略。
同源策略限制了Angular应用向其它后端服务器发送XHR请求的能力,如果要和第三方服务器交互,必须绕过同源策略。
尽管浏览器不允许向异域发送Ajax请求,但是它允许加载异域的脚本文件。利用这一特性,我们可以传递一个回调函数的名称给异域,而异域依据此回调函数动态生成脚本,把需要交换的数据作为入参,传递给回调。例如,脚本:
1 |
<script type="text/javascript" src="http://3rd.com/userdata?callback=JSON_CALLBACK"></script> |
的内容可以是:
1 2 3 |
JSON_CALLBACK({ name : 'Alex' }); |
这样,脚本加载完毕后,JSON_CALLBACK函数自动被调用。这就实现了与异域进行数据交换的能力 。
Angular的$http服务已经内置了JsonP的支持。
近年来,W3C制定了跨域资源共享(CORS)来通过标准的方式取代JSONP。
要通过Angular使用CORS,必须配置$httpProvider:
1 2 3 4 |
angular.module( 'app', [] ).config( function( $httpProvider ) { $httpProvider.defaults.useXDomain = true; //添加此设置 delete $httpProvider.defaults.headers.common['X-Requested-With']; } ); |
异域的后端服务器也必须进行适当配置,并返回特殊的响应头:
响应头 | 说明 |
Access-Control-Allow-Origin | 该响应头控制哪些域可以向本服务器发送跨站请求,设置为*表示任何域都可以 |
Access-Control-Allow-Credentials | 默认情况下,CORS请求不发送Cookie。如果异域服务器启用该响应头,那么你可以启用$http服务的withCredentials选项,这样Cookie会一并发送给异域 |
也可以在服务器端放置代理,所有对异域的请求都通过服务器转发,这样就不存在异域问题了。
进行服务器端身份验证的两种基本方式为:
- 基于Cookie的身份验证:这是最为广泛接受的方式,每个请求都附带一个Cookie,该Cookie关联了服务器端的一个用户会话
- 基于令牌(Token)的身份验证:这是最近流行的新方式,每个请求附带一个“令牌”,令牌可以存放在请求头中,令牌由服务器授予客户端
基于令牌的身份验证,具有如下优势:
- CORS友好:基于Cookie的身份验证在跨域时运作的不是很好,而基于令牌的验证,可以让你方便的向任何服务器发送Ajax请求,因为身份信息就存放在请求头中
- 无状态:Cookie机制要求服务器端维持一个用户会话对象,并把Cookie和用户会话关联。而令牌不需要服务器端维持任何状态
- 解耦:不需要被某种验证机制/框架捆绑,客户端不需要理解服务器如何生成令牌
- 移动设备友好:在iOS、Android等Native移动平台上工作时,Cookie可能正常使用
本节主要介绍AngularJS的令牌身份验证。
当未验证或者身份验证失败时,应当返回 401 状态码。当身份验证成功后,应当返回令牌,例如 { token: '令牌字符串' } 。
1 2 3 4 5 6 7 8 |
//假设当前作用域的user对象存放用户名、密码等信息 $http.post( '/authenticate', $scope.user ).success( function( data, status, headers, config ) { // 存放到sessionStorage,浏览器关闭后实现,类似还有localStorage等地方可以存储 $window.sessionStorage.token = data.token; } ).error( function( data, status, headers, config ) { // 验证失败,删除令牌 delete $window.sessionStorage.token; } ); |
我们可以通过Angular的拦截器机制,来把令牌自动附加到每个请求中去,并对错误的响应进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
app.factory( 'authInterceptor', function( $rootScope, $q, $window ) { return { request : function( config ) { config.headers = config.headers || {}; if ( $window.sessionStorage.token ) { // 自动设置Authorization请求头 config.headers.Authorization = $window.sessionStorage.token; } return config; }, response : function( rejection ) { // 错误处理 switch ( rejection.status ) { case 401: if ( rejection.config.url !== 'api/login' ) { // 如果当前不是在登录页面,广播事件。监听器可以导航应用到登录视图 $rootScope.$broadcast( 'auth:loginRequired' ); } break; case 403: $rootScope.$broadcast( 'auth:forbidden' ); break; case 404: $rootScope.$broadcast( 'page:notFound' ); break; case 500: $rootScope.$broadcast( 'server:error' ); break; } return $q.reject( rejection ); } }; } ); app.config( function( $httpProvider ) { $httpProvider.interceptors.push( 'authInterceptor' ); } ); |
promise是一个对象,它代表了一个函数调用最终可能的返回值或者抛出的异常。promise用于异步的处理数据,你可以将其看做远程对象的代理,promise和Java5引入的Future功能类似。
JavaScript中传统的异步处理方式是回调函数,但是,如果回调函数中又涉及其它异步处理,会让代码结构失控:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
keyService.get( config, { // 异步请求1 success : function( key ) { roomService.open( key, { // 异步请求2 success : function( room ) { roomService.cleanUp( room, {// 异步请求3 success : function() { } } ); }, failure : function() { // 失控,你需要编写多个错误处理代码 } } ); }, failure : function() { } } ); |
当使用promise来设计API时,上述代码看起来像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
angular.module( 'app', [] ).factory( 'keyService', function( $q, $timeout ) { return { get : function( id ) { var deferred = $q.defer(); // 代表一个延迟完成的任务 console.log( 'Finding key by id ' + id ); $timeout( function() { // 可以在未来某个时刻,延迟的resolve或者reject promise if ( id < 100 ) deferred.resolve( 'KEY' + id ); else deferred.reject( 'Invalid key' ); }, 1000 ); return deferred.promise; // 必须返回一个promise对象,才能支持promise链 } }; } ).factory( 'roomService', function( $q, $timeout ) { return { open : function( key ) { var deferred = $q.defer(); console.log( 'Opening room with key ' + key ); $timeout( function() { if ( key == 'KEY0' ) deferred.resolve( 'ROOM0' ); else deferred.reject( 'Do not Disturb' ); }, 3000 ); return deferred.promise; }, cleanUp : function( room ) { var deferred = $q.defer(); console.log( 'Cleaning up room ' + room ); $timeout( function() { deferred.resolve( room + ' CLEANED UP' ); }, 6000 ); return deferred.promise; }, }; } ).run( function( keyService, roomService, $q ) { //promise chain keyService.get( 99 ) .then( function( value ) { return roomService.open( value ); // 为了支持后续的链式调用,必须返回promise }, function( reason ) { // 记录原因,再创建一个拒绝的promise,这类似于记录日志然后重新抛出异常 console.log( 'Failed to get key : ' + reason ); // 明确创建一个因为reason而拒绝的promise // 返回如下的一个promise,否则链条上后续的任务还会继续执行 return $q.reject( reason ); } ) .then( function( value ) { return roomService.cleanUp( value ); }, $q.reject ) // 可以直接把第二个入参设置为$q.reject,这类似于直接重新抛出异常 .then( function( value ) { console.log( 'Result: ' + value ); }, function( reason ) { console.log( 'Failed to clean up room : ' + reason ); return $q.reject( reason ); } ); } ); |
可以看到,使用promise的代码(run块)更加简洁,可读性更高,逃离了回调的地狱。
某些Angular API,调用后返回Promise对象,例如 $http 、 $timeout 。
注入 $q 服务,可以在任何代码中使用Promise:
1 2 3 4 |
angular.module( 'app', [] ).run( function( $q ) { var d = $q.defer(); //生成一个Deferred对象 return d.promise; } ); |
一个Promise要么被resolve,要么被reject:
- resolve被调用时,带有一个履行值
- reject被调用时要带一个拒绝原因
可以调用 Deferred 提供的接口,操控promise:
方法 | 说明 | ||
resolve( value ) | 解析deferred关联的promise | ||
reject( reason ) | 拒绝deferred关联的promise:
|
||
notify( value ) | 报告执行状态,例如执行的进度,在运行长时间执行的任务时有用 |
promise对象本身提供了以下几个接口:
方法 | 说明 |
then() | 签名:
then(successFn, errFn, notifyFn) 无论promise被resolve还是reject,当结果可用后,Angular会立即调用该函数指定的successFn、errFn回调 在promise结果可用前,notifyFn可能被调用0-N次 |
catch(errFn) | 等价于 promise.then(null, errorCallback) |
finally(callback) | 可以当promise结果可用时,执行一定的逻辑,该逻辑不论resolve还是reject,都需要执行——例如资源清理代码 |
该服务用于异步的执行函数,提供了对deferred、promise的支持。$q提供以下接口:
方法 | 说明 |
defer() | 创建一个Deferred对象,表示在未来会完成的一个任务 |
reject(reason) | 创建一个已经被reason拒绝的promise对象,可以用于在promise chain中传递rejection,这和异常传播很类似 |
when | 签名:
when(value, [successCallback], [errorCallback], [progressCallback]); 可以装饰一个简单对象、或者第三方then-able的promise为$q的promise实现。 resolve是该函数的别名 |
all(promises) | 联合多个promise,只有所有成员resolved,该联合才resolved |
应当在Cordova的deviceready事件之后来启动Angular应用:
1 2 3 4 |
var onDeviceReady = function() { angular.bootstrap( document, [ 'app' ] ); }; document.addEventListener( 'deviceready', onDeviceReady ); |
Leave a Reply