@Belinda
2015-05-15T03:15:48.000000Z
字数 13584
阅读 1658
学习笔记
AngularJS提供了一些功能的封装,但是当你试图通过全局对象angular去访问这些 功能时,却发现与以往遇到的库大不相同。
比如,在jQuery中,我们知道它的API通过一个全局对象:
AngularJS也暴露了一个全局对象:angular,也对ajax调用进行封装提供了一个
仔细地查阅
事实上,AngularJS把所有的功能组件都以依赖注入的方式组织起来,这导致了你 必须通过一个中介才能获得某个组件的实例对象:
var injector = angular.injector(['ng']);
injector.invoke(function($http){
//do sth. with $http
});
这个中介,就是依赖注入模式中的容器,在AngularJS中,被称为注入器
注入器是AngularJS框架的关键,这是一个DI/IoC容器的实现。
AngularJS将功能分成了不同类型的组件分别实现,这些组件有一个统称: 供给者/provider。组件之间不可以互相直接调用,一个组件必须通过注入器才 可以调用另一个组件。这样的好处是组件之间相互解耦,脏活留给注入器。
注入器实现了两个重要的功能:
集中存储所有provider的配方(名称+实例化方法),就是说,它知道整个系统 都有哪些功能组件。
按需提供功能组件的实例。
在AngularJS中,从injector的角度看,组件就是一个功能提供者,因此被称为供给者/Provider。
很显然地,每个组件需要在injector中注册自己,这需要两部分信息:
标识符:用来区别自己
创建方法:用来告诉injector如何实例化自己
标识符通常使用一个字符串标识,比如"
创建方法通常是一个具有指定接口的构造函数,injector通过调用该函数,就可以实例化组件。
标识符和创建方法的组合信息,被称为配方。injector中将维护一个集中的配方库,用来按需创建 不同的组件。
要使用AngularJS的功能,必须首先获取注入器。有两种方法取得注入器。
1.创建一个新的注入器
可以使用angular.injector()创建一个新的注入器
2.使用已经创建的注入器
使用angular.element().injector()获得已经创建的注入器。
使用注入器的invoke()方法,可以直接调用一个用户自定义的函数体,并通过函数参数 注入所以来的对象:
angular.injector(['ng'])
.invoke(function($http){
//do sth. with $http
});
也可以使用注入器的get()方法,获得指定名称的服务实例:
var my$http = angular.injector(['ng']).get('$http');
//do sth. with my$http
当采用invoke方式调用组件服务时,AngularJS通过检查函数的参数名确定需要注 入什么对象,比如:
angular.injector(['ng'])
.invoke(function($http){
//do sth. with $http
});
AngularJS在执行invoke时,检查函数的参数表,发现并注入$http对象。
这样有一个问题,就是当我们对JavaScript代码进行压缩处理时,$http可能会被 变更成其他名称,这将导致注入失败。
AngularJS采用依赖项数组的方法解决代码压缩混淆产生的问题:
angular.injector(['ng'])
.invoke(["$http","$compile",function($http,$compile){
//do sth. with $http,$compile
}]);
这是传入invoke的是一个数组,数组的最后一项是实际要执行的函数,数组的其他项 则指明需要向该函数注入的对象。invoke将按照数组中的顺序,依次向函数注入依赖 对象。
和jQuery不同,AngularJS是一个框架。
lib vs. framework
jQuery是一个库,库总是被动的,就像工具,应用的开发逻辑是你的,在 某一点上需要用一下工具,就用好了。
框架则非常不同,这意味着AngularJS为应用已经搭起了一个架子,约定了 一些组成部分,并且实现了这些部分的拼装运行。换句话说,应用的开发 逻辑是AngularJS的,你得跟着它走。
所以,AngularJS难学一些,因为它有一个架子在那,你不了解这个架子, 基本没法下手。
在右边的示例中,我们定义了一个指令ez-duang, 它应该会展开成一个动画 显示出来。
但是,看起来没有什么动画显示出来。AngularJS似乎没有工作,为什么?
有点像操作系统,AngularJs也有一个启动引导的概念。
当你在HTML文件中引入angular.min.js时,AngularJS只是建立了一个全局的 angular对象,这个对象有一些方法可供开发者调用,但应用的框架还没有建立。 在这个阶段,AngularJS还只是一个库。
只有通过启动引导,那些指令才能够生效,比如,我们的ez-duang。
像下面这样,试着给html元素增加一个ng-app指令,再重新运行!
<html ng-app>
....
</html>
ng-app="ezstuff"
如果HTML文件中有某个标签有ng-app属性,那么当DOM树建立成功后,AngularJS 就会自动进入引导过程,启动整个框架:
在大多数情况下,我们都使用ng-app指令来进行自动引导启动,但是如果一个HTML文件中 有多个ng-app,AngularJS只会自动引导它找到的第一个ng-app应用,这是需要手工引导 的一个应用场景。
我们可以利用angular.bootstrap()进行手动引导:
angular.bootstrap(element, [modules], [config]);
bootstrap方法有三个参数:
element : 一个DOM元素,以这个元素为Angular应用的根。就是自动引导时ng-app所在 的元素,这个参数是必须的。比如:document、document.body等。
modules : 引导时需要载入的模块数组。比如:[]、["ezstuff"]等。由于我们的HTML中引用 了ezstuff模块中定义的ez-duang指令,所以,我们需要指定载入ezstuff模块。
config :引导配置项,可选。我们先忽略。
最终,我们使用如下的形式进行手动引导:
angular.bootstrap(document,["ezstuff"]);
引导过程使AngularJS从库转变成了一个框架。
回忆我们之前提到,AngularJS深入骨髓地使用着依赖注入,那么,在引导过程之初,首先创建一个 注入器就毫不奇怪了。
注入器是通向AngularJS所有功能的入口,而AngularJS的功能实现,是通过模块的方式组织的。所以, 在创建注入器的时候,需要告诉AngularJS载入哪些模块(ng模块是内置载入的,不需要显式指定)。
在自动启动引导的场景下,可以给ng-app赋值以指定一个需要载入的模块,比如:
ng-app = "ezstuff"
在手动启动引导的场景下,通过bootstrap方法的第二个参数指定需要载入的模块,比如:
angular.bootstrap(document,["ezstuff"]);
scope对象是AngularJS实现数据绑定的重要服务,所以,在引导启动建立了注入器之后, AngularJS马上在应用的根节点上创建一个根作用域:$rootScope对象。
$rootScope
如果是自动引导启动,那么ng-app所在的DOM节点对应着根作用域。如果是手工引导启动, 那么在bootstrap方法中指定的第一个参数就对应着根作用域。
无论哪一种情况,一旦$rootScope对象创建成功,AngularJS就将这个对象存储到根节点 的data中,我们可以使用如下的方法查看这个对象:
angular.element(approot).data("$rootScope");
以ng-app所在DOM节点为根节点,对这棵DOM子树进行编译。
编译过程通常借助于指令,完成这几种操作:
在DOM对象上挂接事件监听。
在DOM对象对应的scope对象上挂接数据监听。
编译过程是AngularJS相当有特点的一个存在,我们将在下一节继续深入。
编译的目的:实现声明式前端开发
HTML可以声明式地格式化静态文档。例如,如果你需要居中,那么你不需要明确地告诉 浏览器将窗口尺寸二分以获得中心点,并将该中心点与文本中心点对齐。简单地加一个 "align=center"属性就可以获得期望的效果。这就是声明式的威力。
但是,声明式语言也有其局限性。例如,没有简单的办法将文本对器到1/3的位置。我们 需要一个办法教会浏览器新的语法。
AngularJS的HTML编译器使开发者可以创建新的HTML语法。可以将AngularJS的编译器 理解为浏览器引擎的前级解释器,开发者定制的指令,首先被AngularJS的编译器解释为 浏览器引擎能够理解的指令,再送往浏览器引擎进行渲染生成。
笼统地说,指令是DOM元素(例如属性、元素、CSS类等)上的标识符,用来告诉AngularJS的HTML编译器 ($compile服务)将特定的行为绑定到DOM元素,或者改变DOM元素及其子元素。
指令可以放置在元素名、属性、CSS类名称及备注中。下面是一些等效的触发"ng-bind"指令的格式:
<span ng-bind="exp"></span>
<span class="ng-bind: exp;"></span>
<ng-bind></ng-bind>
<!-- directive: ng-bind exp -->
指令本质上就是一个函数,当编译器在DOM中匹配它时,AngularJS将执行这个函数。
问题是,HTML中的ez-duang,怎么就匹配到了JavaScript中的ezDuang?
AngularJS在进行匹配检测之前,首先对HTML元素的标签和属性名转化成规范的驼峰式字符串:
去除名称前缀的x-和data-
以: , - 或 _ 为分割符,将字符串切分成单词,除第一个单词外,其余单词首字母大写
重新拼接各单词
例如,下面的写法都等效地匹配ngBind指令:
<span ng-bind="name"></span> <br>
<span ng:bind="name"></span> <br>
<span ng_bind="name"></span> <br>
<span data-ng-bind="name"></span> <br>
<span x-ng-bind="name"></span> <br>
所以,在前面的课程中,我们在HTML中使用的ez-duang指令,将被规范为ezDuang,编译器使用 这个规范化的名称与注册的指令进行匹配。
编译器是一个AngularJS的内置服务,它负责遍历DOM树来查找匹配指令,并根据指令进行 处理。
HTML编译包括3个步骤:
匹配指令。
为何将compile和link分开?
简单说,当模型变化会导致DOM结构变化时,需要compile和link分开。例如,ng-repeat指令 需要为集合中的每个成员克隆DOM元素。将编译和链接过程分开可以有效地提高性能,因为 模板克隆仅需要编译一次,但链接则发生在每个克隆的DOM元素上。
指令很少需要compile函数,这是很少见的,因为大多数指令考虑的是作用于特定的DOM元素实例, 而不是改变DOM的结构。
指令通常需要link函数,link函数允许指令在DOM元素上注册监听器,
我们知道,在AngularJS中,实现数据绑定的核心是scope对象。那么控制器又有什么用呢?
简单地说,没有控制器,我们没有地方定义业务模型。
回忆下ng-init指令,我们可以使用ng-init指令在scope对象上定义数据,比如:
<div ng-init="sb={name:'somebody',gender:'male',age:28}">
</div>
但是,ng-init的值是一个AngularJS表达式,在这个表达式里,没有办法定义方法。
控制器/controller让我们有机会在scope上定义我们的业务逻辑,具体说,可以使用控制器:
对scope对象上的数据模型进行初始化
向$scope对象上的数据模型添加方法
在一个HTML元素上使用ng-controller指令,就可以引入一个控制器对象:
<div ng-controller="myController">...</div>
名为myController的控制器实际上就是一个JavaScript的构造函数:
//控制器类定义
var myControllerClass = function($scope){
//模型属性定义
$scope.text = "...";
//模型方法定义
$scope.do = function(){...};
};
//在模块中注册控制器
angular.module('someModule',[])
.controller("myController",myControllerClass);
控制器构造函数仅在AngularJS对HTML文档进行编译时被执行一次,从这个角度看,就更容易理解 为何将控制器称为对scope对象的增强:一旦控制器创建完毕,就意味着scope对象上的业务模型 构造完毕,此后就不再需要控制器了- scope对象接管了一切。
ng-controller指令将在该DOM对象上创建一个新的scope对象,这个scope对象的原型就是父scope。 在下图中,我们看到:
ng-app指令导致
通常在应用启动时,需要初始化scope对象上的数据模型。我们之前曾使用ng-init指令进行初始化, 而使用控制器则是更为规范的做法。
右边的示例定义了一个ezController,利用这个控制器,我们对业务模型进行了初始化赋值:
请注意,控制器仅仅负责在编译时在scope对象上建立视图对象vm,视图对象和模板的绑定则是由 scope负责管理的。
业务模型是动态的,在数据之外,我们需要给业务模型添加动作。
在之前建立的业务模型上,我们增加一个随机挑选的方法:shuffle,这个方法负责 从一个小型的名人库中随机的选择一个名人来更新模型的sb属性:
通过在button上使用ng-click指令,我们将模型的shuffle方法绑定到了鼠标点击 事件上。试着用鼠标点击【shuffle】按钮,我们的模型将从库里随机的选出一个 名人,显示在视图里。
控制器的设计出发点是封装单个视图的业务逻辑,因此,不要进行以下操作:
DOM操作
应当将DOM操作使用指令directive进行封装。
变换输出形式
应当使用过滤器/filter对输出显示进行转化。
跨控制器共享代码
对于需要复用的基础代码,应当使用服务/service进行封装
在AngularJS中创建一个服务组件很简单,只需要定义一个具有$get方法的构造函数, 然后使用模块的provider方法进行登记:
//定义构造函数
var myServiceProvider = function(){
this.$get = function(){
return ....
};
};
//在模块中登记
angular.module("myModule",[])
.provider("myService",myServiceProvider);
使用模块的provider方法定义服务组件,在有些场景下显得有些笨重。AngularJS友好 地提供了一些简化的定义方法,这些方法只是对provider方法的封装,分别适用于 不同的场景。
我们简单介绍其中的factory方法和service方法:
factory方法
factory方法要求提供一个类工厂,调用该类工厂将返回服务实例。
var myServiceFactory = function(){
return ...
};
angular.module("myModule",[])
.factory("myService",myServiceFactory);
service方法
service方法要求提供一个构造函数,AngularJS使用这个构造函数创建服务实
var myServiceClass = function(){
this.method1 = function(){...}
};
angular.module("myModule",[])
.service("myService",myServiceClass);
指令也是一种服务,只是这种服务的定义有几个特殊要求:
factory方法返回的对象必须返回一个指令定义对象
//定义指令的类工厂
var directiveFactory = function(injectables){
//指令定义对象
var directiveDefinationObject = {
...
};
return directiveDefinationObject;
};
//在模块上注册指令
angular.module("someModule",[])
.directive("directiveName",directiveFactory);
右边的示例定义一个简单的指令ez-hoverable,这个指令被限制只能 出现在属性的位置,每个具有这个指令的HTML元素,将在鼠标移入 时以虚线边框突出显示。
每个指令定义的工厂函数,需要返回一个指令定义对象,编译器/$compile 在编译时就根据这个定义对象对指令进行展开。
指令定义对象的常用属性如下:
link函数负责实现DOM和scope的数据绑定,通常在link里执行DOM事件监听和数据变化监听。 link函数在template执行后被调用。link是最常用的属性,一个指令的逻辑通常在link函数 里实现。
link函数的形式如下:
function link(scope,iElement,iAttrs,controller,transcludeFn){...}
可以是EACM这四个字母的任意组合,用来限定指令的应用场景。如果不指定这个属性, 默认情况下,指令将仅允许被用作元素名和属性名:
* E - 元素名,例如:<my-directive></my-directive>
* A - 属性名,例如:<div my-directive="exp"></div>
* C - 类,例如:<div class="my-directive: exp;"></div>
* M - 注释,例如:<!-- directive:my-directive exp -->
template是一个HTML片段,可以用来:
* 替换指令的内容。这是默认的行为,可以使用replace属性更改。
* 替换指令本身(如果replace属性设为TRUE的话)。
* 包裹指令的内容(如果transclue属性设为TRUE的话)。
指明是否使用template替换指令元素。
* true - 编译时,将使用template替换指令元素
* false - 编译时,将使用template替换指令元素的内容
template是一个HTML片段,可以用来:
替换指令的内容。这是默认的行为,可以使用replace属性更改。
替换指令本身(如果replace属性设为TRUE的话)。
包裹指令的内容(如果transclue属性设为TRUE的话)。
最简单的指令只需要进行模板替换就可以实现,右边的示例实现了一个ezCustomer 指令,这个指令只是简单的使用template指定的模板替换ez-customer的内容。
可以使用restrict属性,限定指令允许出现的位置:
'A' : 只匹配属性
'E' : 只匹配元素
'C' : 只匹配CSS类
angular.module('demo',[])
.directive('myCustomer', function() {
return {
restrict: 'E',
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};
这时,以下的使用方法就不行了:
<div ng-controller="Controller">
<div my-customer=""></div>
</div>
这样是合法的:
<div ng-controller="Controller">
<my-customer></my-customer>
</div>
默认情况下,指令没有自己的作用域,当然,可以使用一个控制器构造独立 的作用域,但更好的方法,是让指令有自己隔离的作用域。
这通过增加scope选项获得:
angular.module('docsIsolationExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
$scope.vojta = { name: 'Vojta', address: '3456 Somewhere Else' };
}])
.directive('myCustomer', function() {
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};
});
使用:
<div ng-controller="Controller">
<my-customer info="naomi"></my-customer>
<my-customer info="vojta"></my-customer>
</div>
scope选项指定了每一个隔离作用域绑定的属性:
customerInfo对应隔离作用域中的属性
=info 用来告诉$compile绑定到info属性上
如果需要在指令中操作DOM,我们可以在选项中使用link,link需要指定一个函数, AngularJS将在编译时调用这个函数,并传递scope、element和attrs这几个参数进去。
scope 是SCOPE对象
element 是jq对象
attrs 是规范化后的属性名/值哈希表
angular.module('docsTimeDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.format = 'M/d/yy h:mm:ss a';
}])
.directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) {
function link(scope, element, attrs) {
var format,
timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
element.on('$destroy', function() {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function() {
updateTime(); // update DOM
}, 1000);
}
return {
link: link
};
}]);
使用:
<div ng-controller="Controller">
Date format: <input ng-model="format"> <hr>
Current time is: <span my-current-time="format"></span>
</div>
有些指令需要能够包含其他未知的元素,比如一个对话框,我们不知道会有什么元素 需要放在对话框里。如果要设计一个对话框指令,我们需要使用transclude。
angular.module('docsTransclusionDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
templateUrl: <div ng-transclude=""></div>
};
});
使用:
<div ng-controller="Controller">
<my-dialog>Check out the contents, {{name}}!</my-dialog>
</div>
angular.module('dragModule', [])
.directive('myDraggable', ['$document', function($document) {
return function(scope, element, attr) {
var startX = 0, startY = 0, x = 0, y = 0;
element.css({
position: 'relative',
border: '1px solid red',
backgroundColor: 'lightgrey',
cursor: 'pointer'
});
element.on('mousedown', function(event) {
// Prevent default dragging of selected content
event.preventDefault();
startX = event.pageX - x;
startY = event.pageY - y;
$document.on('mousemove', mousemove);
$document.on('mouseup', mouseup);
});
function mousemove(event) {
y = event.pageY - startY;
x = event.pageX - startX;
element.css({
top: y + 'px',
left: x + 'px'
});
}
function mouseup() {
$document.off('mousemove', mousemove);
$document.off('mouseup', mouseup);
}
};
}]);
过滤器也是一种服务,负责对输入的内容进行处理转换,以便更好地向用户显示。
过滤器可以在模板中的{{}}标记中使用:
{{ expression | filter:arg1:arg2}}
预置的过滤器
AngularJS的ng模块实现了一些预置的过滤器,如:currency、number等等,可以直接 使用。例如下面的示例将对数字12使用currency过滤器,结果将是"$12.00":
{{12|currency}}
带参数的过滤器
过滤器也可以有参数,例如下面的示例将数字格式化为"1,234.00":
{{1234|number:2}}
过滤器流水线
过滤器可以应用于另一个过滤器的输出,可称之为链式调用,语法如下:
{{expression|filter1|filter2|...}}
别忘了过滤器也是一种服务,所以你可以将它注入你的代码中。
和普通的服务不同,过滤器在注入器中注册时,名称被加了一个后缀:Filter。例如, number过滤器的服务名称是:numberFilter,而currency过滤器的服务名称是: currencyFilter。
通常我们的代码被封装在三个地方:控制器、服务、指令。这些地方都支持服务的直接 注入,例如:
angular.module('myModule',[])
.controller(function($scope,numberFilter){
//...
})
有时你需要显式的通过注入器调用过滤器,那么使用注入器的invoke方法:
angular.injector(['ng'])
.invoke(function(numberFilter){
//...
})
总之,记住过滤器是一种服务,除了名字需要追加Filter后缀,和其他服务的调用方法没 什么区别
过滤器也是一种特殊的服务,与创建一个普通的服务相比较:
必须使用模块的filter()接口定义
必须提供factory方法
factory方法必须返回一个过滤器函数,其第一个参数为输入变量
//定义过滤器类工厂
var filterFactory = function(){
//定义过滤器函数
var filter = function(input){
//process input and generate output
return output
}
};
//在模块上注册过滤器
angular.module("someModule",[])
.filter("filterName",filterFactory);
右边的示例定义了一个将字符串格式化为大写的过滤器。
过滤器的行为可以通过额外的参数来调整。比如,我们希望改进上一节的示例,使其可以 支持仅大写每个单词的首字母。
实现阶段
通过在过滤器类工厂返回的过滤器函数中传入额外的参数,就可以实现这个功能。
var filter = function(input,argument1,argument2){...}
调用阶段
在使用过滤器时,额外的参数通过前缀:引入,比如
{{expression|filter:argument1:argument2}}