AngularJS进阶学习
参考:http://www.hubwiz.com/class/54f3ba65e564e50cfccbad4b
1. AJAX:Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)可以与服务器交换数据并更新部分网页,而无需重新加载整个页面。
2. JQuery:浏览器里原生的JavaScript有点像汇编语言,不同的浏览器就像不同的CPU架构,汇编语言各有千秋,这让前端开发者很恼火。聪明人很快发现了这个痛点,于是,抹平浏览器差异的jQuery库出现了。
3. AngularJS:jQuery看成库的话,AngularJS则是一个框架,它以一种特殊的方式向jQuery表达了敬意:内置精简版的jQuery - jqLite。如果某种原因你不愿意使用jqLite,也可以在AngularJS之前引入jQuery库。 AngularJS自动地将jqLite升级成jQuery。
指令directive:类似ng-app,ez-clock这样的非标准HTML标签叫做指令(JavaScript文件)
模板:包含AngularJS标记的html文件被称为模板
编译:将指令解析成为浏览器可以理解的html元素和脚本的过程叫做编译
视图:通过编译器解析处理模板文件,生成的结果就是视图
一般过程:构造声明式界面模板,来提出需求,继而抽取并实现自定义指令。
1. 作用域/Scope
在HTML模板中,我们用了两个内置指令:
- ng-app指令会在启动引导时创建一个$rootScope对象。
- ng-init指令用来在作用域上初始化变量,这个指令将在$rootScope上建立sb对象。
1.1 层级的作用域
在默认情况下,一个DOM子元素不会创建新的作用域,也就是说,这个子元素所对应的 scope对象,其实就是它的最近一级的祖先对象对应的scope对象。
有些指令会导致创建新的作用域,比如ng-controller。如果在一个DOM对象上创建了新 的作用域,那么这个scope对象的原型是其最近一级的组件对象的scope对象。
1.2 监听Scope数据的变化
编译仅仅在启动引导时执行一次,这意味着我们的link函数只会被调用一次,所以需要监听数据变化来使得界面和数据一致。
- $watch(watchExpression, listener, [objectEquality]);
$watch方法要求传入三个参数:
- watchExpression - 要监听的表达式,比如:"sb"
- listener - 变化发生时的回调函数,AngularJS将向这个函数传入新值和旧值
- objectEquality - 如果监听表达式的值是一个对象,应当将这个参数置为true。
以上便建立了从 数据到界面的单向绑定。
1.3 修改Scope上的数据
挂接监听函数(示例中使用keyup事件), 在监听函数中实现对sb变量的修改。
以上便建立了从 界面到数据的单向绑定。
angular.module("ezstuff",[]) .directive("ezNamecardEditor",function(){ return { restrict : "E", template : "<ul class='nceditor'></ul>", replace : true, link : function(scope,element,attrs){ //获得变量名称 var model = attrs.data; //展开HTML模板,使用field属性标记对应字段 element.append("<li>name : <input type='text' field='name'></li>") .append("<li>gender : <input type='text' field='gender'></li>") .append("<li>age : <input type='text' field='age'></li>"); //监听DOM事件,变化时修改变量值 element.find("input").on("keyup",function(ev){ var field = ev.target.getAttribute("field"); scope[model][field] = ev.target.value; //将对scope的修改进行传播 scope.$apply(""); }); } }; }) .directive("ezLogger",function(){ return { restrict : "A", link : function(scope,element,attrs){ var model = attrs.data; scope.$watch(model,function(nv){ var cnt = JSON.stringify(nv,null," "); element.html("<pre>"+cnt+"</pre "); },true); } }; });
1.4 数据变化的传播
数据绑定有两个方向:
- 数据 → 界面:我们使用scope对象的$watch()方法监听数据的变化,来更新界面。
- 界面 → 数据:我们在界面的DOM对象上监听变化事件,来更新数据,并通过$apply()方法传播变化。
- $watch()
每个scope对象都维护了一个私有的监听队列,每次当我们在scope上执行一次$watch方法,就相当于 向这个监听队列里塞入一个监听函数。
- $apply()
为了捕捉对数据的修改,AngularJS要求开发者使用scope对象的$apply方法对数据进行修改, $apply方法内部会自动地调用监听队列里的监听函数
//方法1:直接修改sb对象. 不会自动触发监听函数 scope.sb.name = 'Tonny'; //方法2:使用scope的$apply方法,在数据修改后会自动触发监听函数 scope.$apply("sb.name = 'Tonny'"); //方法3:直接修改sb对象,然后调用$apply方法来传播变化。 scope.sb.name = 'Tonny'; scope.$apply("");
2. 依赖注入:注入器
在依赖注入的模式下,所有的组件必须通过容器才能相互访问,这导致了在AngularJS中, 你必须通过一个中介才能获得某个组件的实例对象:
var injector = angular.injector(['ng']); injector.invoke(function($http){ //do sth. with $http });
这个中介,就是依赖注入模式中的容器,在AngularJS中,被称为:注入器
AngularJS的组件之间不可以互相直接调用,一个组件必须通过注入器才 可以调用另一个组件。这些组件有一个统称 - 供给者/provider。
配方其实就是:名称+类构造函数。AngularJS启动时,这些provider首先使用其配方在注入器内注册。比如,http请求服务组件封装在$httpProvider类内,它通过"$http"这个名字在注入器内注册。其他组件,比如一个用户的控制器,如果需要使用http功能,使用"$http"这个名字 向注入器请求,就可以获得一个http服务实例了。
2.1 注册服务组件
从injector的角度看,组件就是一个功能提供者,因此被称为供给者/Provider。 在AngularJS中,provider以JavaScript类(构造函数)的形式封装。
Provider类要求提供一个$get函数(类工厂),injector通过调用该函数, 就可以获得服务组件的实例。
名称和类函数的组合信息,被称为配方。injector中维护一个集中的配方库, 用来按需创建不同的组件。这个配方库,其实就是一个Hash对象,key就是服务名称,value 就是类定义。
2.2 调用组件服务:获得注入器对象,而后通过注入器调用API
获取注入器的方法有两个:
- 创建一个新的注入器
可以使用angular.injector()创建一个新的注入器:
angular.injector(modules, [strictDi]);
- 获取已经创建的注入器
如果AngularJS框架已经启动,那么可以使用DOM对象的injector()方法获 得已经创建的注入器:
var element = angular.element(dom_element); var injector = element.injector();
注入器有两个方法可供进行API调用:invoke()和get()。
- invoke()
使用注入器的invoke()方法,可以直接调用一个用户自定义的函数体,并通过函数参数 注入所依赖的服务对象,这是AngularJS推荐和惯例的用法:
angular.element(document).ready(function(){ angular.injector(["ng","ezstuff"]).invoke(function(ezHello){ //将ezHello实例对象转成字符串显示出来 var e = document.querySelector("#logger"); angular.element(e).text(ezHello); }); });
- get()
也可以使用注入器的get()方法,获得指定名称的服务实例:
angular.element(document).ready(function(){ //直接通过注入器获取ezHello实例对象 var myHello = angular.injector(["ng","ezstuff"]).get("ezHello"); //将ezHello实例对象转成字符串显示出来 var e = document.querySelector("#logger"); angular.element(e).text(myHello); });
2.3 注入的方式和原理
有两种方法告知注入器需要注入的服务对象:参数名注入和依赖数组注入。
- 参数名注入
AngularJS在执行invoke()函数时,将待注入函数定义转化为字符串,通过 正则表达式检查其参数表,从而发现并注入所所依赖的服务对象:
//myfunc通过参数表声明这个函数依赖于"$http"服务 var myfunc = function($http){ //do sth. with $http
}; injector.invoke(myfunc); //myfunc的定义将被转化为字符串进行参数名检查
example:
//在ezstuff模块上登记一个服务ezHello angular.module("ezstuff",[]) .provider("ezHello",function(){ //$get方法是一个类工厂,返回服务的实例 this.$get = function(){ return "hello,world!"; }; }); angular.element(document).ready(function(){ var myfunc = function(ezHello){ //将ezHello实例对象转成字符串显示出来 var e = document.querySelector("#logger"); angular.element(e).text(ezHello); }; angular.injector(["ng","ezstuff"]).invoke(myfunc); });
这样有一个问题,就是当我们对JavaScript代码进行压缩处理时,$http可能会被 变更成其他名称,这将导致注入失败。
- 依赖数组注入
AngularJS采用依赖项数组的方法解决代码压缩混淆产生的问题。这时传入invoke()的 是一个数组,数组的最后一项是实际要执行的函数,其他项则指明需要向该函数注入 的服务名称。注入器将按照数组中的顺序,依次向函数注入依赖对象。
采用这种方法,待注入函数的参数表的名称就无关紧要了:
//myfunc依赖于"$http"和"$compile"服务 var myfunc = ["$http","$compile",function(p1,p2){ //do sth. with p1($http),p2($compile)
}];
injector.invoke(myfunc);
example:
//在ezstuff模块上登记一个服务ezHello angular.module("ezstuff",[]) .provider("ezHello",function(){ //$get方法是一个类工厂,返回服务的实例 this.$get = function(){ return "hello,world!"; }; }); angular.element(document).ready(function(){ var myfunc = ["ezHello",function(hhh){ //将ezHello实例对象转成字符串显示出来 var e = document.querySelector("#logger"); angular.element(e).text(hhh); }]; angular.injector(["ng","ezstuff"]).invoke(myfunc); });
3. 启动引导
当你在HTML文件中引入angular.min.js时,AngularJS只是建立了一个全局的 angular对象,这个对象有一些方法可供开发者调用,但应用的框架还没有建立。
在这个阶段,AngularJS还只是一个库,和jQuery类似,你可以使用angular.element() 操作DOM,也可以使用angular.injector()创建注入器... 但是,你定义的指令,你 创建的控制器,你封装的服务,你开发的模板...所有这些组件,还静静地躺在那里, 没有被整合在一起。
我们说,框架还没有运转起来,现在还是库阶段。
只有通过启动引导,AngularJS框架才开始将那些组件拼接在一起,应用才真正 开始运转。
3.1 自动引导启动框架
就像你看到的那样,如果HTML模板中有某个标签有ng-app属性,那么当DOM树建立成功后, AngularJS就会自动进入引导过程,启动整个框架:
3.2 手工引导启动框架
在大多数情况下,我们都使用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.element(document).ready(function(){ var e = document.querySelector("#bootstrap"); angular.element(e) .on("click",function(){ angular.bootstrap(document,["ezstuff"]); }) }) ; angular.module("ezstuff",[]) .directive("ezDuang",function(){ return { restrict : "E", template : "<img src='http://ww4.sinaimg.cn/bmiddle/757eb2ffjw1eptcr4qobjg209205dthh.gif'>" }; });
3.3 引导的一般过程
3.3.1 引导第1步:创建注入器
引导过程使AngularJS从库转变成了一个框架。AngularJS深入骨髓地使用着依赖注入,那么,在引导过程 之初,首先需要创建一个注入器就毫不奇怪了。注入器是通向AngularJS所有功能的入口,而AngularJS的功能实现,是通过模块的方式组织的。所以, 在创建注入器的时候,需要告诉AngularJS载入哪些模块(ng模块是内置载入的,不需要显式指定)。
在自动启动引导的场景下,可以给ng-app赋值以指定一个需要载入的模块,比如:
- ng-app = "ezstuff"
在手动启动引导的场景下,通过bootstrap方法的第二个参数指定需要载入的模块,比如:
- angular.bootstrap(document,["ezstuff"]);
INSIDE:无论自动启动还是手工启动,最终都是调用angular对象上的injector()方法创建了一个 注入器,然后把这个注入器存入了根对象的data里:
- var injector = angular.injector(["ng","ezstuff"]);
- angular.element(document).data("$injector",injector);
3.3.2 引导第2步:创建根作用域
scope对象是AngularJS实现数据绑定的重要服务,所以,在引导启动建立了注入器之后, AngularJS马上在应用的根节点上创建一个根作用域:$rootScope对象。
如果是自动引导启动,那么ng-app所在的DOM节点对应着根作用域。如果是手工引导启动, 那么在bootstrap方法中指定的第一个参数就对应着根作用域。
无论哪一种情况,一旦$rootScope对象创建成功,AngularJS就将这个对象存储到根节点 的data中:
- var rootScope = injector.get("$rootScope");
angular.element(document).data("$rootScope",rootScope);
我们可以使用如下的方法查看这个对象:
- angular.element(approot).data("$rootScope");
3.3.3 引导第3步:编译DOM子树
引导过程的最后一步,是以ng-app所在DOM节点为根节点,对这棵DOM子树进行编译。
编译过程通常借助于指令,完成这几种操作:
- 对DOM对象进行变换。
- 在DOM对象上挂接事件监听。
- 在DOM对象对应的scope对象上挂接数据监听。
手动引导需要如下:
- var compile = injector.get("$compile")
compile(document)(rootScope);
3.4 编译器/$compile
编译器$compile是一个AngularJS的内置服务,它负责遍历DOM树来查找匹配指令, 并调用指令的实现代码进行处理。
HTML编译包括3个步骤:
- 匹配指令
$compile遍历DOM树,如果发现有元素匹配了某个指令,那么这个指令将被加入 该DOM元素的指令列表中。一个DOM元素可能匹配多个指令。
- 执行指令的编译函数
当一个DOM元素的所有指令都找齐后,编译器根据指令的优先级/priority指令进行排序。 每个指令的compile函数被依次执行。每个compile执行的结果产生一个link函数,这些 link函数合并成一个复合link函数。
- 执行生成的链接函数
$compile通过执行指令的link函数,将模板和scope链接起来。结果就是一个DOM视图和scope对象模型 之间的动态数据绑定。
3.5 指令/directive
指令可以放置在元素名、属性、CSS类名称及备注中。指令的实现本质上就是一个类工厂,它返回一个指令定义对象,编译器根据这个指令定义对象进行操作。
指令的规范化
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>
4. 控制器
在AngularJS中,实现数据绑定的核心是scope对象。没有控制器/controller,我们没有地方定义业务模型。
控制器让我们有机会在scope上定义我们的业务逻辑,具体说,可以使用控制器:
- 对scope对象进行初始化
- 向scope对象添加方法
4.1 在模板中声明控制器
在一个HTML元素上使用ng-controller指令,就可以引入一个控制器对象:
- <div ng-controller="myController">...</div>
4.2 控制器的实现
控制器实际上就是一个JavaScript的类/构造函数:
- //控制器类定义
- var myControllerClass = function($scope){
- //模型属性定义
- $scope.text = "...";
- //模型方法定义
- $scope.do = function(){...};
- };
- //在模块中注册控制器
- angular.module('someModule',[])
- .controller("myController",myControllerClass);
4.3 控制器的一次性
控制器构造函数仅在AngularJS对HTML文档进行编译时被执行一次。从这个角度看, 就更容易理解为何将控制器称为对scope对象的增强:一旦控制器创建完毕,就意味着scope对 象上的业务模型构造完毕,此后就不再需要控制器了- scope对象接管了一切。
4.4 控制器对scope的影响
4.4.1 控制器总是会创建新的scope对象
ng-controller指令总是创建一个新的scope对象:
在图中,我们看到:
- ng-app指令引发$rootScope对象的创建。开始时,它是一个空对象。
- body元素对应的scope对象还是$rootScope。ng-init指令将sb对象挂在了$rootScope上。
- div元素通过ng-controller指令创建了一个新的scope对象,这个对象的原型是$rootScope。
- 因为原型继承的关系,在do函数中对sb的引用指向$rootScope.sb。
4.4.2 初始化$scope对象
通常在应用启动时,需要初始化scope对象上的数据模型。我们之前曾使用ng-init指令进行初始化, 而使用控制器则是更为规范的做法。
右边的示例定义了一个ezController,利用这个控制器,我们对业务模型进行了初始化赋值:
请注意,控制器仅仅负责在编译时在scope对象上建立视图对象vm,视图对象和模板的绑定则是由 scope负责管理的。
4.4.2 向cope对象添加方法
业务模型是动态的,在数据之外,我们需要给业务模型添加动作。
在之前建立的业务模型上,我们增加一个随机挑选的方法:shuffle,这个方法负责 从一个小型的名人库中随机的选择一个名人来更新模型的sb属性:
通过在button上使用ng-click指令,我们将模型的shuffle方法绑定到了鼠标点击 事件上。试着用鼠标点击【shuffle】按钮,我们的模型将从库里随机的选出一个 名人,显示在视图里。
控制器的设计出发点是封装单个视图的业务逻辑,因此,不要进行以下操作:
- DOM操作
应当将DOM操作使用指令/directive进行封装。
- 变换输出形式
应当使用过滤器/filter对输出显示进行转化。
- 跨控制器共享代码
对于需要复用的基础代码,应当使用服务/service进行封装
5. 封装服务
5.1 创建服务组件
在AngularJS中创建一个服务组件很简单,只需要定义一个具有$get方法的构造函数, 然后使用模块的provider方法进行登记:
- //定义构造函数
- var myServiceProvider = function(){
- this.$get = function(){
- return ....
- };
- };
- //在模块中登记
- angular.module("myModule",[])
- .provider("myService",myServiceProvider);
5.2 可配置的服务
有时我们希望服务在不同的场景下可以有不同的行为,这意味着服务可以进行配置。
比如,我们希望小计算器可以根据不同的本地化区域,给计算结果追加货币符号前缀, 那么需要在这个服务创建之前,首先配置本地化区域的值,然后在具体的计算中, 根据这个值选择合适的货币符号。
AngularJS使用模块的config()方法对服务进行配置,需要将实例化的服务提供者 (而不是服务实例)注入到配置函数中:
- angular.module("myModule",[])
- .config(["myServiceProvider",function(myServiceProvider){
- //do some configuration.
- }]);
注意:服务提供者provider对象在注入器中的登记名称是:服务名+Provider。 例如: $http的服务提供者实例名称是"$httpProvider"。
function doCalc(){ var injector = angular.injector(["ezstuff"]), mycalculator = injector.get("ezCalculator"), ret = mycalculator.add(3,4); document.querySelector("#result").innerText = ret; } angular.module("ezstuff",[]) .provider("ezCalculator",function(){ var currency = "$"; this.setLocal = function(l){ var repo = { "CN":"¥", "US":"$", "JP":"¥", "EN":"€" }; if(repo[l]) currency = repo[l]; }; this.$get = function(){ return { add : function(a,b){return currency + (a+b);}, subtract : function(a,b){return currency + (a-b);}, multiply : function(a,b){return currency + (a*b);}, divide: function(a,b){return currency + (a/b);} } }; }) .config(function(ezCalculatorProvider){ ezCalculatorProvider.setLocal("CN"); });
5.3 服务定义语法糖
使用模块的provider方法定义服务组件,在有些场景下显得有些笨重。AngularJS友好 地提供了一些简化的定义方法,这些方法通常只是对provider方法的封装, 分别适用于不同的应用场景:
- factory
使用一个对象工厂函数定义服务,调用该工厂函数将返回服务实例。
- service
使用一个类构造函数定义服务,通过new操作符将创建服务实例。
- value
使用一个值定义服务,这个值就是服务实例。
- constant
使用一个常量定义服务,这个常量就是服务实例。
5.3.1 factory方法
factory方法要求提供一个对象工厂,调用该类工厂将返回服务实例。
- var myServiceFactory = function(){
- return ...
- };
- angular.module("myModule",[])
- .factory("myService",myServiceFactory);
INSIDE:AngularJS会将factory方法封装为provider,上面的示例 等同于:
- var myServiceFactory = function(){
- return ...
- };
- angular.module("myModule",[])
- .provider("myService",function(){
- this.$get = myServiceFactory;
- });
如下:
angular.module("ezstuff",[]) .factory("ezCalculator",function(){ return { add : function(a,b){return a+b;}, subtract : function(a,b){return a-b;}, multiply : function(a,b){return a*b;}, divide: function(a,b){return a/b;} } })
5.3.2 service方法
service方法要求提供一个构造函数,AngularJS使用这个构造函数创建服务实例:
- var myServiceClass = function(){
- this.method1 = function(){...}
- };
- angular.module("myModule",[])
- .service("myService",myServiceClass);
INSIDE:AngularJS会将service方法封装为provider,上面的示例 等同于:
- var myServiceClass = function(){
- //class definition.
- };
- angular.module("myModule",[])
- .provider("myService",function(){
- this.$get = function(){
- return new myServiceClass();
- };
- });
如下:
var ezCalculatorClass = function(){ this.add = function(a,b){return a+b;}; this.subtract = function(a,b){return a-b;}; this.multiply = function(a,b){return a*b;}; this.divide = function(a,b){return a/b;}; }; angular.module("ezstuff",[]) .service("ezCalculator",ezCalculatorClass);
5.3.3 value方法
有时我们需要在不同的组件之间共享一个变量,可以将这种情况视为一种服务: provider返回的总是变量的值。
value方法提供了对这种情况的简化封装:
- angular.module("myModule",[])
- .value("myValueService","cx129800123");
INSIDE:AngularJS会将value方法封装为provider,上面的示例 等同于:
- angular.module("myModule",[])
- .provider("myService",function(){
- this.$get = function(){
- return "cx129800123";
- };
- });
5.3.4 constant方法
有时我们需要在不同的组件之间共享一个常量,可以将这种情况视为一种服务: provider返回的总是常量的值。
constant方法提供了对这种情况的简化封装:
- angular.module("myModule",[])
- .constant("myConstantService","Great Wall");
和value方法不同,AngularJS并没有将constant方法封装成一个provider,而仅仅 是在内部登记这个值。这使得常量在AngularJS的启动配置阶段就可以使用(创建任何 服务之前):你可以将常量注入到模块的config()方法中。
如下,将constant换成value则不能运行:
angular.module("ezstuff",[]) .constant("ezCurrency","CN") .provider("ezCalculator",function(){ var currency = "$"; this.setLocal = function(l){ var repo = { "CN":"¥", "US":"$", "JP":"¥", "EN":"€" }; if(repo[l]) currency = repo[l]; }; this.$get = function(){ return { add : function(a,b){return currency + (a+b);}, subtract : function(a,b){return currency + (a-b);}, multiply : function(a,b){return currency + (a*b);}, divide: function(a,b){return currency + (a/b);} } }; }) .config(function(ezCurrency,ezCalculatorProvider){ ezCalculatorProvider.setLocal(ezCurrency); });
6. 封装指令
6.1 创建指令
指令也是一种服务,只是这种服务的定义有几个特殊要求:
- 必须使用模块的directive()方法注册服务
- 必须以对象工厂/factory()方法定义服务实现
- 对象工厂必须返回一个指令定义对象
- //定义指令的类工厂
- var directiveFactory = function(injectables){
- //指令定义对象
- var directiveDefinationObject = {
- ...
- };
- return directiveDefinationObject;
- };
- //在模块上注册指令
- angular.module("someModule",[])
- .directive("directiveName",directiveFactory);
INSIDE:指令在注入器中的登记名称是:指令名+Directive。 例如,ng-app指令的服务名称是:"ngAppDirective"。
6.2 指令定义对象
每个指令定义的工厂函数,需要返回一个指令定义对象。指令定义对象就是 一个具有约定属性的JavaScript对象,编译器/$compile在编译时就根据这 个定义对象对指令进行展开。
指令定义对象的常用属性如下:
- template : string
使用template指定的HTML标记替换指令内容(如果replace = true,那么用HTML片段替换指令本身。如果transclue属性为true,则为包裹指令的内容。)
- restrict : string
用来限定指令在HTML模板中出现的位置。restict属性可以是EACM这四个字母的任意组合,用来限定指令的应用场景。 如果不指定这个属性,默认情况下,指令将仅允许被用作元素名和属性名:
- E - 指令可以作为HTML元素使用
- A - 指令可以作为HTML属性使用
- C - 指令可以作为CSS类使用
- M - 指令可以在HTML注释中使用
- replace : true|false
使用这个属性指明template的替换方式。作图为false,有图为true
<body> <div ng-controller="ezCtrl"> <ez-customer></ez-customer> </div> </body>
当然,因为replace为true时,要求模板有 一个根节点,所以定义如下:
.directive("ezCustomer", function() { return { restrict:"E", replace:true, template: "<div>Name: {{customer.name}} Address: {{customer.address}}</div>" }; });
- scope : true|false|{...}
scope属性为指令创建私有的作用域,这在创建可复用的Widget时非常有用。通过设置scope属性,指令的每个实例都将获得一个隔离的本地作用域:
scope上有两种约定符号:@表示DOM值变则Scope值变,=表示DOM值和Scope值双向影响。如下图的理解:
angular.module("ezstuff",[]) .controller("ezCtrl", ["$scope", function($scope) { $scope.Emmy = { name: "Emmy", address: "1600 Amphitheatre" }; $scope.Edison = { name: "Edison", address: "2500 Amphitheatre" }; }]) .directive("ezCustomer", function() { return { restrict:"E", replace:true, scope:{ name:"@name", address: "=address" }, template: "<div>Name: <input field='name' value={{name}}></input> Address: <input field='address' value={{address}}></input></div>", link : function(scope,element,attrs){ element.find("input").on("keyup",function(ev){ var field = ev.target.getAttribute("field"); scope[field] = ev.target.value; //将对scope的修改进行传播 scope.$apply(""); }); } }; }) .directive("ezLogger",function(){ return { restrict : "A", link : function(scope,element,attrs){ var model = attrs.data; scope.$watch(model,function(nv){ var cnt = JSON.stringify(nv,null," "); element.html("<pre>"+cnt+"</pre "); },true); } }; });
<html ng-app="ezstuff"> <head> <script src="angular.min.js"></script> </head> <body> <div ng-controller="ezCtrl"> <ez-customer name="{{Emmy.name}}" address="Emmy.address"></ez-customer> <ez-customer name="{{Edison.name}}" address="Edison.address"></ez-customer> <div ez-logger data="Emmy"></div> <div ez-logger data="Edison"></div> </div> </body> </html>
- link : function(..){...}
link属性是一个函数,用来在指令中操作DOM树、实现数据绑定。
- scope:指令对应的scope对象。如果指令没有定义自己的本地作用域,那么传入的就是外部的 作用域对象。
- iElement:指令所在DOM对象的jqLite封装。如果使用了template属性,那么iElement对应 变换后的DOM对象的jqLite封装。
- iAttrs:指令所在DOM对象的属性集。这是一个Hash对象,每个键是驼峰规范化后的属性名。
- transclude : true|false|'element'
允许指令包含其他HTML元素,这通常用于实现一个容器类型的Widget。
有些指令需要能够包含其他未知的元素。比如我们定义一个指令ez-dialog,用来 封装对话框的样式和行为,它应当允许在使用期(也就是在界面模板文件里)才指 定其内容:
- <ez-dialog>
- <p>对话框的内容在我们开发ez-dialog指令的时候是无法预计的。这部分内容需要
- 被转移到展开的DOM树中适当的位置。</p>
- </ez-dialog>
transclude属性可以告诉编译器,利用所在DOM元素的内容,替换template中包含 ng-transclude指令的元素的内容:
从上图中可以看到,使用transclude有两个要点:
- 需要首先声明transclude属性值为true,这将告诉编译器,使用我们这个指令的 DOM元素,其内容需要被复制并插入到编译后的DOM树的某个点。
- 需要在template属性值中使用ng-transclude指明插入点。
7. 过滤器
7.1 在视图模板中使用过滤器
过滤器也是一种服务,负责对输入的内容进行处理转换,以便更好地向用户显示。
过滤器可以在模板中的{{}}标记中使用:
- {{ expression | filter:arg1:arg2}}
- 预置的过滤器
AngularJS的ng模块实现了一些预置的过滤器,如:currency、number等等,可以直接 使用。例如下面的示例将对数字12使用currency过滤器,结果将是"$12.00":
- {{12|currency}}
- 带参数的过滤器
过滤器也可以有参数,例如下面的示例将数字格式化为"1,234.00":
- {{1234|number:2}}
- 过滤器流水线
过滤器可以应用于另一个过滤器的输出,就像流水线,语法如下:
- {{expression|filter1|filter2|...}}
7.2 在代码中使用过滤器
别忘了过滤器也是一种服务,所以你可以将它注入你的代码中。
和普通的服务不同,过滤器在注入器中注册时,名称被加了一个后缀:Filter。 例如,number过滤器的服务名称是:numberFilter,而currency过滤器的服务名称是: currencyFilter。
通常我们的代码被封装在三个地方:控制器、服务、指令。这些地方都支持服务的直接 注入,例如:
- angular.module('myModule',[])
- .controller(function($scope,numberFilter){
- //...
- })
有时你需要显式的通过注入器调用过滤器,那么使用注入器的invoke()方法:
- angular.injector(['ng'])
- .invoke(function(numberFilter){
- //...
- })
总之,记住过滤器是一种服务,除了名字需要追加Filter后缀,和其他服务的调用方法没 什么区别。
右边的示例在控制器中注入了number和currency过滤器,实现对total的格式化。
7.3 创建过滤器
和指令类似,过滤器也是一种特殊的服务,与创建一个普通的服务相比较:
- 必须使用模块的filter()接口注册服务
- 必须提供对象工厂/factory方法
- 对象工程必须返回一个过滤器函数,其第一个参数为输入变量
- //定义过滤器类工厂
- var filterFactory = function(){
- //定义过滤器函数
- var filter = function(input,extra_args){
- //process input and generate output
- return output
- }
- };
- //在模块上注册过滤器
- angular.module("someModule",[])
- .filter("filterName",filterFactory);
右边的示例定义了一个将字符串格式化为大写的过滤器。
7.4 为过滤器增加参数
过滤器的行为可以通过额外的参数来调整。比如,我们希望改进上一节的示例,使其可以 支持仅大写每个单词的首字母。
- 实现
通过在过滤器类工厂返回的过滤器函数中传入额外的参数,就可以实现这个功能。
- var filter = function(input,argument1,argument2){...}
- 使用
在使用过滤器时,额外的参数通过前缀:引入,比如
- {{expression|filter:argument1:argument2}}
右边的示例实现了支持参数的过滤器ezUC,试着去掉HTML模板中过滤器ezUC的参数, 看看显示输出的区别!