学会使用AngularJS

这是用前端框架AngularJS构建一个简单应用的部分代码,首页配制一个路由。当然,AngularJS路由功能是一个纯前端的解决方案,与我们熟悉的后台路由不太一样。前端路由需要提前对指定 的(ng-app),定义路由规则(routeProvider),然后通过不同的URL,告诉(ng-app)加载哪个页面(HTML),再渲染到(ng-app)视图(ng-view)中。虽然URL输入不一样,页面展示不一样,其实完成的是单页(ng-app)视图(ng-view)的局部刷新。

由上图可以知道:

控制器:"courseCtrl"是我们定义的一个控制器,在定义这个控制器的时候我们注入了一个$scope和helloAngular。当一个应用中两个控制有相同内容时,我们就定义一个公共函数来存放,这个公共函数在Angular中就叫服务,这个例子中就是helloAngular。

指令:上图中diretive函数就是定义一个指令。名称为ezNamecard。然而指令有自定义的,也有内置的指令,如ng-repeat就是一个内置指令。

那么问题来了,我们该如何定义一个控制器,定义一个服务,定义一个指令?注入又是一个什么情况?Angular又是如何启动的?关键是AngularJS到底又是怎么样解析这些浏览器根本不认识的指令的了?

或许这些都是你的疑惑,又或许这些都是你想知道的。因此,就有了下面这篇文章。

为了让整篇文章读起来更容易点的,我罗列了一个大概的目录:

第一部分:AngularJS开发方式

托互联网日新月异发展的福,浏览器变成了人们接入互联网的入口,而JavaScript这个超级丑小鸭,在21世纪第一个十年后期,随着google一系列WEB应用的流行,终于成功地站到了舞台的中央,唤起了开发者对JavaScript的兴趣。

jQuery同一小撮对浏览器极其熟稔的极客负责抹平不同浏览器的差异,其他开发者只需要基于jQuery进行开发,可以更好地关注业务实现,而不是把时间花在适配不同的浏览器上。

试着用jQuery实现一个简单的时钟页面,实现思路很简单。

  1. 引入jQuery库
  2. 在DOM文档就绪后,启动一个1秒1次的定时器
  3. 在定时器每次触发时,取当前时间值,更新div#clock的文本
1 <body>
2     <div id="clock"></div>
3 </body>
1 $(function(){
2     setInterval(function(){
3         var d = new Date();
4         $("#clock").text(d.toString());
5     },1000)
6 })

世界上永远有人在追求进步,jQuery有点像C语言,威力很大,不过要弄出点像样的前端界面,还得花不少功夫处理琐碎的事情。

还能再简单些吗?Misko Hevery认为可以。于是,AngularJS诞生了。

AngularJS引入了以下三个主要的概念,期望让前端开发更系统化一些。

方式一:声明式界面开发

AngularJS最大的卖点是静态的HTML文档,就可以定义具有动态的页面。

改写时钟例子:

1 <body ng-app="ezstuff">
2     <ez-clock></ez-clock>
3 </body> 
 1 angular.module("ezstuff",[])
 2 .directive("ezClock",function(){
 3     return {
 4         restrict : "E",
 5         template : "<div></div>",
 6                 replace  : true,
 7         link : function(scope,element,attrs){
 8             setInterval(function(){
 9                 var d = new Date();
10                 element.text(d.toString());
11             },1000);
12         }
13     }
14 })

对比分析一下上面列出来的这个时钟例子:

诠释DOM结构

在Angular中,这个HTML文件被称为模板,而ng-app称之为指令。指令可以为HTML元素添加额外的行为(让HTML动起来)。在这个例子中,我们使用了一个ng-app指令,这个指令用来通知Angular自动引导应用;这个例子中还使用了一个自定义的ez-clock指令,这个指令是我们自己实现的,用来产生那个小时钟。tips:AngularJS中指令可分为内置指令和自定义指令,可以提前思考一下内置指令是如何使用的、自定义指令是如何定义的,以上代码段replace=true加和不加有什么区别,还有指令中link函数的各个参数分别起什么作用......。这里只负责诠释结构,所有的问题文章后面有详细分析。

思考代码执行过程

我们在模板中指定了一个自定义的标签ez-clock,而它变成了一个会动的时钟。这中间发生了什么?

当然,浏览器不会理解ez-clock这个标签,是脚本做了这个工作。

angular.min.js引入了基本的angularJS框架,它会在浏览器载入HTML文档并且建立好DOM树后,执行以下操作:

  1. 找到有ng-app属性的DOM节点
  2. 以这个节点为根节点,重新解释DOM树,具体说就是子树
  3. 在解释过程中,发现ez-clock这个指令
  4. 调用ez-clock指令的实现进行展开

ez-clock的展开操作如下:

  1. 使用一个div元素替换这个自定义标签
  2. 创建一个定时器,在定时器触发时刷新DIV元素的innerText

ez-clock这个自定义的标签,在AngularJS中被称为指令/directive,意思是看到这个指令,AngularJS基础框架需要对其进行解释,以便展开成浏览器可以理解的东西(HTML元素和脚本),而这个解释过程,有一个特定的名称:指令编译。

可见,要写的代码一点也不会少,只是,代码被一种新的方式重新组织了。

思考有什么好处

首先在开发过程中,便于分工与代码复用。

在小的项目中也可以应用AngularJS,这样你可以得到思维的锻炼。但是真正发生威力,是在一个团队中,可以有专人负责实现指令(比如:ez-clock),其他人只需要利用这些指令编写模板,这样的成本更低。

当然,从编写界面HTML模板的角度看,ez-clock比div更具有语义性,使模板更容易维护,使指令的实现升级不影响模板,这也是不小的好处了。

指令算是新型的API,与我们所熟悉的对象,函数这类接口完全不同,它提供了在静态化的HTML文件中,指定动态行为的能力。

总之,在使用AngularJS进行前端开发时,始终应该从构造声明式模板开始,如果现成的指令不够用,那么就定义自己的指令、实现自己的指令,这是一个迭代的过程。

方式二:数据的双向绑定

数据怎么显示

因为不能像jQuery一样将DOM操作混在模板里,声明式模板的范式很快让我们变得束手束脚。

一个典型的问题:在声明式模板里怎么显示数据?

假设我们有某人的基本信息,保存在一个json对象里:

 1 var sb = { 2  name : ‘somebody’, 3  gender : ‘male’, 4 age : 28 5 } 

我们定义一个指令ez-namecard,希望它经过编译后会展开成这样:

1 <div>
2     <div>name : somebody</div>
3     <div>gender : male</div>
4     <div>age : 28</div>
5 </div>

那么,怎么把sb这个json对象指定给ez-namecard这个指令?

一个很容易想到的定义方法,就是给指令ez-namecard指令增加一个属性,用这个属性的值指明数据对象,这相当于,用属性向指令(解释器)传递参数:

 1 <ez-namecard data="window.sb"></ez-namecard> 

这样的话,ez-namecard的解释器只要检查data属性,然后执行一个eval()就可以获得sb值,从而将其内容填充到展开的div片段中。

 1 var sb = {
 2     name : “somebody”,
 3     gender : “male”,
 4     age : 28
 5 };
 6 angular.module(“ezstuff”,[])
 7 .directive(“ezNamecard”,function(){
 8     return {
 9         restrict : “E”,
10         template : “<div></div>”,
11         link : function(scope,element,attrs){
12             var sb = eval(attrs.data);
13             var _html = "<div>gender : " + sb.gender + “</div>"
14                 + "<div>name : " + sb.name + “</div>"
15                 + "<div>age : " + sb.age + “</div>”;
16             element.append(_html);
17         }
18     }
19 })

作用域(Scope)

从上个实例中看到,前面定义的sb变量,默认是挂在window对象上的,即window.sb。如果所有的数据都挂在window上,说不定哪天就会出现变量的命名冲突,所以,AngularJS引入了一个自用的命名空间,也就是$rootScope对象,这样sb就可以挂在$rootScope上了,即$rootScope.sb。tips:注意一下下面标红指令名称的可命名规范,指令名称可以这样写的原因文章后面讲指令的时候会有详细说明。

1 <body ng-app="ezstuff" ng-init="sb ={name:'somebody',gender:'male',age:28}">
2     <ez-namecard data="sb"></ez-namecard>
3 </body>
 1 angular.module("ezstuff",[])
 2 .directive("ezNamecard",function($rootScope){
 3     return {
 4         restrict : "E",
 5         template : "<div></div>",
 6         link : function(scope,element,attrs){
 7             var sb = $rootScope.$eval(attrs.data);
 8             var _html = "<div>name : " + sb.name + “</div>"
 9             + "<div>gender : " + sb.gender + “</div>"
10             + "<div>age : " + sb.age + “</div>”;
11             element.append(_html);
12         }
13     };
14 });

看一下这个代码,在模板页面和控制页面找一下变化:

HTML:ng-app指令会指示AngularJS基础框架在启动引导时创建一个$rootScope对象,ng-init指令用来在作用域上初始化变量,在我们的例子中,这个指令将sb变量建立在$rootScope上。

JavaScript:与之前使用eval函数进行表达式估值不同,这里我们直接使用$rootScope的$eval方法获得在$rootScope上sb变量的值。

再仔细看一下,$rootScope是我们在定义ezNamecard这个指令,通过函数参数表的方式注入的,而link函数,也有一个参数scope。而这两个有什么区别了?下面我们来理解清楚层级作用域的概念。

层级的作用域

在AngularJS中,ng-app开始的DOM子树上,每个DOM对象都有一个对应的scope对象。

比如:在上面示例中,body对象对应一个scope对象(因为body元素有ng-app属性,所以这个scope就是$rootScope对象),ez-namecard对象也对应一个scope对象…。

当然,这里必须清楚两点。

第一:在默认情况下,一个DOM子元素不会创建新的作用域,也就是说,这个子元素所对应的scope对象,其实就是它的最近一级的祖先对象对应的scope对象。

比如:在我们的例子中,我们没有在ez-namecard创建新的作用域,那么它对应的scope对象,就是它的父对象即body对象的scope对象,恰好也就是$rootScope对象;而ez-namecard有3个div元素,这三个div对象由于也没有创建新的作用域,他们对应的scope对象,则向上一直找,最终也是对应到body对象所对应的$rootScope对象。

第二:有些指令会导致创建新的作用域,像ng-controller(控制器),如果在一个DOM对象上创建了新的作用域,那么这个scope对象的原型是其最近一级的组件对象。

比如:在我们的例子中,如果在ez-namecard上使用ng-controller指令,那么ez-namecard对应的scope对象就不再是body对应的$roootScope对象,但是由于原型继承,所以通过这个scope依然可以访问到sb变量。

在谈数据

数据的操作非常重要,可以更深入一点分析一下,也就是说在AngularJS这个框架中是如何玩转数据的呢?

一、监听数据的变化

我们已经实现了将数据显示到界面上,不过这还不够。由于编译仅仅在启动引导时执行一次,这意味着我们的link函数只会被调用一次,那么,如果数据变化了,在界面上将不会有任何反馈,即界面和数据将变得不同步了。

这就需要持续监听数据的变化。

好在AngularJS的scope对象直接支持对数据变化的监听。$watch方法要求传入两个参数:

  1. 要监听的表达式,比如:sb
  2. 变化时的回调函数,AngularJS将向这个函数传入新值和旧值
 1 angular.module("ezstuff",[])
 2 .directive("ezNamecard",function($rootScope){
 3     return {
 4         restrict : "E",
 5         template : "<div></div>",
 6         link : function(scope,element,attrs){
 7             element.append("<div>name : <span class='name'></span></div>")
 8                 .append("<div>gender : <span field='gender'></span></div>")
 9                 .append("<div>age : <span field='age'></span></div>")
10             scope.$watch(attrs.data,function(nv,ov){
11                 var fields = element.find("span");
12                 fields[0].innerText = nv.name;
13                 fields[1].innerText = nv.gender;
14                 fields[2].innerText = nv.age;
15             },true)
16         }
17     };
18 });

经过改进后的代码,当数据被改变时,界面会自动得到更新。这时,我们称,建立了从数据到界面的单向绑定。

二、如何修改数据?

一旦在指令解释器中可以访问模型,那么使用声明模板实现数据修改非常简单了。

这里我们定义一个新的指令:ez-namecard-editor,意图让其展开成这样:

1 <div>
2     <div>name : <input type="text"> </div>
3     <div>gender : <input type="text"> </div>
4     <div>age : <input type="text"></div>
5 </div>

先看一下HTML的内容。使用ez-namecard-editor指令代替了ez-namecard,其他都一样。

1 <body ng-app="ezstuff" ng-init="sb = {name:'somebody',gender:'male',age:28}">
2     <ez-namecard-editor data="sb"></ez-namecard-editor>
3 </body>

再看JavaScript代码。在ez-namecard-editor的指令实现中,为了用input中的值自动更新sb值,我们需要在解释器中给input对象挂接上监听函数(这里使用keyup事件),然后在事件中修改sb变量的对应属性就可以了。

 1 angular.module("ezstuff",[])
 2 .directive("ezNamecardEditor",function($rootScope){
 3         return {
 4         restrict : "E",
 5         template : "<div></div>",
 6         link : function(scope,element,attrs){
 7         var model = attrs.data;
 8         element.append("<div>name:<input type='text' field='name'></div>")
 9         .append("<div>gender:<input type='text' field='gender'></div>")
10         .append("<div>age:<input type='text' field='age'></div>");
11         element.find("input").on("keyup",function(ev){
12             var field = ev.target.getAttribute("field");
13             console.log([model,field]);
14             scope[model][field] = ev.target.value;
15         })
16         }
17     };
18 });

最终的效果是,用户在界面上进行的操作,自动地同步到了我们的数据。这时,我们称,已经建立了从界面到数据的单向绑定。

三、数据变化的传播

然而,我们已经分别实现了两个方向的绑定:

数据——>界面:我们使用scope对象的$watch方法监听数据的变化,来更新界面。

界面——>数据:我们在界面的DOM对象上监听变化事件,来更新数据。

这时,我们就有了一个推测:

如果我们把ez-namecard和ez-namecard-editor都绑定到同一个sb对象上,那么在ez-namecard-editor上进行编辑,将导致sb对象发生变化,由于ez-namecard监听了这个变化,所以,ez-namecard的显示也应该变化。

四、变化传播的实现原理

scope维护了一个内部的监听队列,每次当我们在scope上执行一次$watch方法,就相当于向这个监听队列里塞入一个监听函数。

为了捕捉对数据的修改,AngularJS要求开发者使用scope对象的$apply方法对数据进行修改,$apply方法对数据进行修改,$apply方法内部会自动地调用监听队列里的监听函数:

 1 /* 修改scope上的sb对象的name属性 */
 2 
 3 //方法1:直接修改sb对象. 不会自动触发监听函数
 4 scope.sb.name = 'Tonny';
 5  
 6 //方法2:使用scope的$apply方法,在数据修改后会自动触发监听函数
 7 scope.$apply("sb.name = 'Tonny'");
 8  
 9 //方法3:直接修改sb对象,然后调用$apply方法来传播变化。
10 scope.sb.name = 'Tonny';
11 scope.$apply("");

在有些情况下,AngularJS会自动调用$apply方法,比如在初次编译的时候。但无论哪种情况,希望了解,对数据的变化监听,总是需要通过$apply方法的调用而被激活,如果AngularJS没有获得一个机会来调用$apply,就需要你手工的调用它。

1 <body ng-app="ezstuff" ng-init="sb = {name:'somebody',gender:'male',age:28}">
2     <ez-namecard data="sb"></ez-namecard>
3     <ez-namecard-editor data="sb"></ez-namecard-editor>
4 </body>
 1 angular.module("ezstuff",[])
 2 .directive("ezNamecard",function($rootScope){
 3     return {
 4         restrict : "E",
 5         template : "<div></div>",
 6         link : function(scope,element,attrs){
 7         element.append("<div>name:<span class='name'></span></div>")
 8         .append("<div>gender:<span field='gender'></span></div>")
 9         .append("<div>age:<span field='age'></span></div>")
10         scope.$watch(attrs.data,function(nv,ov){
11             var fields = element.find("span");
12             fields[0].innerText = nv.name;
13             fields[1].innerText = nv.gender;
14             fields[2].innerText = nv.age;
15         },true)
16         }
17     };
18 })
19 .directive("ezNamecardEditor",function($rootScope){
20     return {
21         restrict : "E",
22         template : "<div></div>",
23         link : function(scope,element,attrs){
24         var model = attrs.data;
25         element.append("<div>name:<input type='text' field='name'></div>")
26         .append("<div>gender:<input type='text' field='gender'></div>")
27         .append("<div>age:<input type='text' field='age'></div>");
28         element.find("input").on("keyup",function(ev){
29             var field = ev.target.getAttribute("field");
30             scope[model][field] = ev.target.value;
31             scope.$apply("")
32         })
33         }
34     };
35 });

五、数据和模板解耦

在AngularJS中,界面是运转起来的声明式模板,称为视图/View,真实的数据模型/Model通常不完全适用于某个特定应用场景的视图,所以用于视图绑定的模型,称为视图模型/ViewModel,这些结合起来,被称为M.V.VM,即MVVM模式。

在使用AngularJS进行前端开发,在建立声明式模式之后,在作用域上定义适应模板的数据模型,然后,指令的解释器会把数据和模板关联起来,这被称为数据绑定。

数据绑定包含两个方向:

  • 数据的变化会自动更新界面
  • 界面的交互会自动更新数据

方式三:用依赖注入解耦

什么是注入器/injector

AngularJS提供了一些功能的封装,但是当你试图通过全局对象angular去访问这些功能时,却发现与以往遇到的库大不相同。

比如,在jQuery中,我们知道它的API通过一个全局对象:$暴露出来,当你需要进行ajax调用时,使用$.ajax()就可以了。API很符合思维的预期。

AngularJS也暴露了一个全局对象:angular,也对ajax调用进行封装提供了一个$http对象,但是当你试图沿用旧经验访问angular.$http时,发现在不是那么回事。

仔细地查阅$http的文档,也找不到一点点的线索,从哪里可以把这个$http拿到。

事实上,AngularJS把所有的功能组件都以依赖注入的方式组织起来,这导致了你必须通过一个中介才能获得某个组件的实例对象:

1 var injector = angular.injector(['ng']);
2 injector.invoke(function($http){
3     // do sth. with $http
4 });

这个中介,就是依赖注入模式中的容器,在AngularJS中,被称为注入器。

注入器是AngularJS框架的关键,这是一个DI/IoC容器的实现。

DI/IoC:依赖注入和控制反转,框架都会有这个特性。

AngularJS将功能分成了不同类型的组件分别实现,这些组件有一个统称:供给者/provider。组件之间不可以互相直接调用,一个组件必须通过注入器才可以调用另一个组件。这样的好处是组件之间相互解耦,脏活留给注入器。

注入器实现了两个重要的功能:

  1. 集中存储所有provider的配方(名称+实例化方法),就是说,它知道整个系统都有哪些功能组件。
  2. 按需要提供功能组件的实例。

一、注册服务组件

在AngularJS中,从injector的角度看,组件就是一个功能提供者,因此被称为供给者/Provider。

很显然地,每个组件需要在injector中注册自己,这需要两部分信息:

  • 标识符:用来区别自己
  • 创建方法:用来告诉injector如何实例化自己

标识符通常使用一个字符串标识,比如“$http”代表http调用服务、“$scope”代表作用域对象、“$compile”代表编译服务...

创建方法通常是一个具有指定接口的构造函数,injector通过调用该函数,就可以实例化组件。

标识符和创建方法的组合信息,被称为配方。injector中将维护一些集中的配方库,用来按需创建不同的组件。

二、获得注入器对象

要使用AngularJS的功能,必须首先获取注入器。有两种方法取得注入器。

  • 创建一个新的注入器

可以使用angular.injector()创建一个新的注入器。

  • 使用已经创建的注入器

使用angular.element().injector()获得已经创建的注入器。

三、调用AngularJS的API

使用注入器的invoke()方法,可以直接调用一个用户自定义的函数体,并通过函数参数注入进来的对象:

1 angular.injector(['ng'])
2 .invoke(function($http){
3     // do sth. with $http
4 });

也可以使用注入器的get()方法,获得指定名称的服务实例:

 1 var my$http = angular.injector(['ng']).get('$http'); 2 // do sth. with my$http  

四、怎么确定注入什么?

当采用invoke方式调用组件服务时,AngularJS通过检查函数的参数名确定需要注入什么对象,比如:

1 angular.injector(['ng'])
2 .invoke(function($http){
3     // do sth. with $http
4 });

AngularJS执行invoke时,检查函数的参数表,发现并注入$http对象。

这样有一个问题,就是当我们对JavaScript代码进行压缩处理时,$http可能会被变更成其他名称,这将导致注入失败。

五、显示指定注入项

AngularJS采用依赖项数数组方法解决代码压缩混淆注入的问题:

1 angular.injector(['ng'])
2 .invoke(["$http","$compile",function($http,$compile){
3     // do sth. with $http,$compile
4 }]);

这是传入invoke的是一个数组,数组的最后一项是实际要执行的函数,数组的其他项 则指明需要向该函数注入的对象。invoke将按照数组中的顺序,依次向函数注入依赖对象。也就是说上面代码中绿色部分$http,$compile是可以为任意名称的。

实际项目中怎么使用注入

$injector是基础,在实际项目中直接使用$injector是很少见的。然而,在angularJS中有三种注入方式:推断式注入,标注式注入,内联式注入。

下面分别举个例子说明一下三种注入方式的使用规则:

推断式注入:

1 var myModule = angular.module(“MyModule”,[]);
2 var MyCtrl = function($scope){
3     $scope.gameName = “wpzheng”;
4 }
5 myModule.controller(“MyCtrl”,MyCtrl);

这种注入方式无需写注入对象,有一个不好的地方是函数参数名称必须和被注入的对象相同,这样如果发布后是压缩代码的话就可能出现在错误。 

标注式注入:

1 var myModule = angular.module(“MyModule”,[]);
2 var MyCtrl = function(thisIsMyName){
3     thisIsMyName.gameName = “wpzheng”;
4 }
5 MyCtrl.$inject = [‘$scope’];
6 
7 myModule.controller(‘MyCtrl’,MyCtrl); 

标注式注入也称声明式注入,这种注入方式是需要写注入对象(如:$scope),而函数参数名称可以与被注入的对象名称不同,这样话就解决了压缩后代码所带来的错误。

内联式注入

1 var myModule = angular.module(“MyModule”,[]);
2 myModule.controller(‘MyCtrl’,[‘$scope’,
3     function($scope1){
4         $scope1.gameName = “wpzheng”
5     }
6 ])

这种注入方式是我们最常用的方式,因为这无需单独使用$inject来注入名称,哦,对了,不是不需要使用inject,而是controller调用的时候已经自动使用inject帮我们注入了对应的服务。而且函数名称可以和被注入的对象名称不同。最主要的是书写方便,看着也爽。 

小结

好了,写到这个地方,这里暂且对上面的内容做个小结,这样更有意于后面的内容能够连接上。以上总的来说就阐述了一个问题:AngularJS的开发方式。以一个时钟实现为测试用例阐述的流程是:先说明jQuery是怎么实现这个例子中的功能的,通过对比,引出AngularJS的三个编程方式。

方式一:声明式界面开发:用AngularJS模板方式将时钟例子从新实现一次,用此诠释了三个问题,1、AngularJS中是如果利用指令等构造声明式模板的。2、AngularJS模板中指令的执行过程以及AngularJS基础框架是如何编译模板的。3、较jQuery,在代码量并没有减少的情况下使用AngularJS有什么好处。

方式二:数据的双向绑定:在AngularJS中每一个DOM对象都拥有一个自己的Scope对象,而ng-app启动指令对应DOM的是对象是$rootScope。但是要注意两点,1、在默认情况下,一个DOM子元素不会创建新的作用域,这个子元素所对应的scope对象,其实就是它的最近一级的祖先对象对应的scope对象。2、有些指令会导致创建新的作用域,像ng-controller(控制器),如果在一个DOM对象上创建了新的作用域,那么这个scope对象的原型是其最近一级的组件对象。然后分别以实例的方式讲了怎么实现数据到界面和界面到数据的原理,即双向数据绑定,这样就达到了数据和模板解耦的好处。

方式三:用依赖注入解耦:注入器负责从我们创建的服务中创建注入的实例。可以使用注入器的invoke()方法将服务注入到函数中。同样也可以通过调用get函数来获得任何一个已经被定义过的服务的实例。同时,AngularJS采用依赖项数数组方法解决代码压缩后变更注入名所引起错误的问题。其目的是为了让组件之间进行一个解耦。最后还列出了AngularJS的三种注入方式。而我们在项目中最常用的方式是内联注入。

第二部分:AngularJS启动和编译

一、AngularJS简介

AngularJS是Google开源的一款JavaScript MVC框架,弥补了HTML的构建应用方面的不足,其通过使用指令(directives)结构来扩展HTML词汇,使开发者可以使用HTML来声明动态内容,从而使得Web开发和测试工作变得更加容易。

jQuery不同,AngularJS是一个框架。jQuery是一个库,库总是被动的,就像工具,应用的开发逻辑是你的,在某一点上需要用一下工具,就用好了。

框架则非常不同,这意味着AngularJS为应用已经搭起了一个架子,约定了一些组成部分,并且实现了这些部分的拼装运行。换句话说,应用的开发逻辑是AngularJS的,你得跟着它走。

所以,AngularJS难学一些,因为它有一个架子在那,你不了解这个架子,基本没法下手。

二、AngularJS启动方式

明白AngularJS框架的启动方式更有易于有整体上理解它的运行机制。AngularJS总共有三种启动方式:

方式一:自动启动

Angular什么自动的找到ng-app,将它作为启动点,自动启动。

 1 <html ng-app=”myModule”>
 2 
 3 <head>
 4     <title>New Page</title>
 5     <meta charset="utf-8" />
 6     <script type="text/javascript" src="angular/angular.min.js"></script>
 7  </head>
 8 
 9 <body>
10     <div ng-controller="MyCtrl">
11         <span>{{Name}}</span>
12     </div>
13 </body>
14 </html>
1 var myModule = angular.module("myModule", []);
2 myModule.controller('MyCtrl', ['$scope',
3     function($scope) {
4         $scope.Name = "Puppet";
5     }
6 ]);

方式二:手动启动

在没有ng-app情况下,只需要在JS中添加一段注册代码即可

1 <body>
2     <div ng-controller="MyCtrl">
3         <span>{{Name}}</span>
4     </div>
5 </body>
1 var myModule = angular.module("myModule", []);
2 myModule.controller('MyCtrl', ['$scope',
3     function($scope) {
4         $scope.Name = "Puppet";
5     }
6 ]);
1 /**
2  * 这里要用ready函数等待文档初始化完成
3  */
4 angular.element(document).ready(function() {
5     angular.bootstrap(document, ['myModule']);
6 });

方式三:多个ng-app

在ng中,Angular的ng-app是无法嵌套使用的,在不嵌套的情况下有多个ng-app,他默认只会启动第一个ng-app,第二个第三个需要手动启动(注意:不要手动启动第一个,虽然可以运行,但会抛异常) 

 1 <body>
 2     <div id="app1" ng-app="myModule1">
 3         <div ng-controller="MyCtrl">
 4             <span>{{Name}}</span>
 5         </div>
 6     </div>
 7     <div id="app2" ng-app="myModule2">
 8         <div ng-controller="MyCtrl">
 9             <span>{{Name}}</span>
10         </div>
11     </div>
12 </body>
 1 var myModule1 = angular.module("myModule1", []);
 2 myModule1.controller('MyCtrl', ['$scope',
 3     function($scope) {
 4         $scope.Name = "Puppet";
 5     }
 6 ]);
 7 
 8 var myModule2 = angular.module("myModule2", []);
 9 myModule2.controller('MyCtrl', ['$scope',
10     function($scope) {
11         $scope.Name = "Vincent";
12     }
13 ]);
14 angular.element(document).ready(function() {
15     angular.bootstrap(app2, ['myModule2']);
16 });

三、AngularJS指令编译

指令/directive:笼统地说,指令是DOM元素(例如属性、元素、CSS类等)上的标识符,用来告诉AngularJS的HTML编译器($compile服务)将特定的行为绑定到DOM元素,或者改变DOM元素及其子元素。

指令本质上就是一个函数,当编译器在DOM中匹配它时,AngularJS将执行这个函数。

指令运行的三个阶段:

加载阶段:加载angular.js,找到ng-app指令,确定应用的边界。

编译阶段:遍历DOM,找到所有的指令缓存到它的内部的一个缓存中,然后在根据指令代码中的template,replace,transclude转换DOM结构。

链接阶段:对每一个指令运行link函数。

小结

通过和jQuery库进行一个对比,说明要先对AngularJS框架了解怎么使用后才能应用它。然后列出了Angular的三种启动,而我们最常用的应该只有自启动一种方式。最后说明了一下指令在Angular中是怎么被编译的。从而也就知道了指令的运行机制。

第三部分:AngularJS开发利器【控制器、服务器、指令、过滤器】

简介:AngularJS中控制器一旦定义,就意味着一个新的scope对象建立了,如何在这个scope对象上定义属性和方法以及控制器应该尽量避免做哪些行为。

一、使用控制器封装业务逻辑

理解控制器

我们知道,在AngularJS中,实现数据绑定的核心是scope对象。那么控制器又有什么用呢?

简单地说,没有控制器,我们就没法定义业务模型。

回忆下ng-init指令,我们可以使用ng-init指令在scope对象上定义数据,比如:

1 <div ng-init="sb={name:'somebody',gender:'male',age:28}">
2 </div>

但是,ng-init的值是一个AngularJS表达式,在这个表达式里,没有办法定义方法。

控制器/controller让我们有机会在scope上定义我们的业务逻辑,具体说,可以使用控制器:

  1. scope对象上的数据模型进行初始化
  2. scope对象上的数据横型添加方法

在一HTML元素上使用ng-controller指令,就可以引入一个控制器对象:

 1 <div ng-controller=“myController">...</div> 

名为myController控制器实际上就是一个JavaScript的构造函数:

 1 // 控制器类定义
 2 var myControllerClass = function($scope){
 3 // 模型属性定义
 4 $scope.text = "...";
 5 // 模型方法定义
 6 $scope.do = function(){...};
 7 };
 8 // 在模块中注册控制器
 9 angular.module('someModule',[])
10 .controller(“myController",myControllerClass);

控制器构造函数仅在AngularJS对HTML文档进行编译时被执行一次,从这个角度看,就更容易理解为何将控制器称为对scope对象的增强:一旦控制器创建完毕,就意味着scope对象上的业务模型构造完毕,以后不在需要控制器了,然而,scope对象接管了一切。

控制器对scope的影响

ng-controller指令将在该DOM对象上创建一个新的scope对象,这个scope对象的原型就是父scope,在下图中可以看到:

1、ng-app指令导致$rootScope对象的创建,开始时,它是一个空对象。

2、body元素对应的scope对象还是$rootscope,此时,通过ng-init指令,将SB对象挂在了$rootScope上。

3、div元素通过ng-controller指令定义了一个新的scope对象,这个对象的原型是$rootScope,在DO函数中对SB的引用是有效的,在当前SCOPE中没有SB的定义,因此这个引用指向原型对象的SB,最终$rootScope上的sb得到了修改。

初始化$scope对象

通常在应用启动时,需要初始化scope对象上的数据模型。我们之前曾使用ng-init指令进行初始化,而使用控制器则是更为规范的做法,下面定义了一个ezController,利用这个控制器,我们对业务模型进行了初始化赋值。

1 <div ng-controller="ezController">
2     <div>name : {{vm.sb.name}}</div>
3     <div>gender : {{vm.sb.gender}}</div>
4     <div>age : {{vm.sb.age}}</div>
5     <div>career : {{vm.sb.career}}</div>
6     <div><img ng-src="{{vm.sb.photo}}"></div>
7 </div>
 1 var ezControllerClass = function($scope){
 2     $scope.vm = {
 3         sb : {
 4             name : "Jason Stantham",
 5             gender : "male",
 6             age : 48,
 7             career : "actor",
 8             photo : "http://5c9eae4c45250d739b6003af3b34a.jpg"
 9         }
10     };
11 };
12 angular.module("ezstuff",[])
13 .controller("ezController",ezControllerClass);

请注意,控制器仅仅负责在编译时在scope对象上建立视图对象vm,视图对象和模板的绑定则是由scope负责管理的。  

向scope对象添加方法

业务模型是动态的,在数据之外,我们需要给业务模型添加动作。

在之前建立的业务模型上,我们增加一个随机挑选的方法;shuffle,这个方法负责从一个小型的名人库中随机的选择一个名人来更新模型的sb属性:

通过在button上使用ng-click指令,我们将模型的shuffle方法绑定到了鼠标点击事件上。然而,从运行结果来看,有同学可以就会想问,为什么我点击后,绑定在$scope上的变量值变化了,界面上显示的值也就会根着变化?回答这个问题其实很简单,Angular实现双向数据绑定,使用的是脏值检查,你只要弄清楚脏值策略就可以。一个网上搜一下就有一大把。呵呵!!!

 1 $scope.vm = {
 2     sb : {
 3         name : "Jason Stantham",
 4         gender : "male",
 5         age : 48,
 6         career : "actor",
 7         photo : "http://af3b34a.jpg"
 8     },
 9     shuffle : function(){
10         var repo = [
11         {name:,gender:"male",age:48,career:"actor",photo:""},
12         {name:"Jessica Alba",gender:"female",age:32,career:"actress",photo:""},
13         {name:"Nicolas Cage",gender:"male",age:53,career:"actor",photo:""},
14         {name:"ff",gender:"male",age:48,career:"independent",photo:""},
15         {name:"Sheetal Sheth",gender:"female",age:36,career:"actress",photo:""},
16         {name:"Barack Obama",gender:"male",age:58,career:"president",photo:""},
17         {name:"ww",gender:"male",age:63,career:"president",photo:""}
18         ];
19         var idx = Math.floor(Math.random()*repo.length);
20         $scope.vm.sb = repo[idx];
21     }
22 };

一些应避免的行为

控制器的设计出发点是封装单个视图的业务逻辑,因此,不要进行以下操作:

1、DOM操作

应当将DOM操作使用指令directive进行封装。

2、变换输出形式

应当使用过滤器/filter对输出显示进行转化。

3、跨控制器共享代码

对于需要复用的基础代码,应当使用服务/service进行封装。

二、使用服务器封装可复用的代码

简介:详细总结了AngularJS定义一个服务的三个方法:provider、factory、service。基础方法是provider。

创建服务组件

AngularJS中创建一个服务组件很简单,只需要定义一个具有$get方法的构造函数,然后使用模块的provider方法进行登记。

1 // 定义构造函数
2 var myServiceProvider = function(){
3     this.$get = function(){
4         return ....
5     };
6 };
7 // 在模块中登记
8 angular.module("myModule",[])
9 .provider("myService",myServiceProvider);

举个例子:

1 <button onclick="doCalc();">3+4=?</button>
2 <div id="result"></div>
 1 function doCalc(){
 2     var injector = angular.injector(["ezstuff"]),
 3         mycalculator = injector.get("ezCalculator"),
 4         ret = mycalculator.add(3,4);
 5 
 6     document.querySelector("#result").innerText = ret;
 7 }
 8 
 9 angular.module("ezstuff",[])
10     .provider("ezCalculator",function(){
11         this.$get = function(){
12             return {
13                 add : function(a,b){return a+b;},
14                 subtract : function(a,b){return a-b;},
15                 multiply : function(a,b){return a*b;},
16                 divide: function(a,b){return a/b;}
17             }
18         };
19     })

服务的定义方式

使用provider方法定义服务组件,在有些场景下显示得有些笨重。AngularJS友好地提供了一些简化的定义方法,这些方法只是对provider方法的封装,分别适用于不同的场景。

包括factory方法和service方法

1、factory方法

factory方法要求提供一个类工厂,调用该工厂将返回服务实例。

1 var myServiceFactory = function(){
2     return ...
3 };
4 angular.module("myModule",[])
5 .factory("myService",myServiceFactory);

改写一下上面的例子

 1 function doCalc(){
 2     var injector = angular.injector(["ezstuff"]),
 3         mycalculator = injector.get("ezCalculator"),
 4         ret = mycalculator.add(3,4);
 5 
 6     document.querySelector("#result").innerText = ret;
 7 }
 8 
 9 angular.module("ezstuff",[])
10     .factory("ezCalculator",function(){
11         return {
12             add : function(a,b){return a+b;},
13             subtract : function(a,b){return a-b;},
14             multiply : function(a,b){return a*b;},
15             divide: function(a,b){return a/b;}
16         }
17     })

2、service方法

service方法要求提供一个构造函数,AngularJS使用这个构造函数创建服务实例。

1 var myServiceClass = function(){
2     this.method1 = function(){...}
3 };
4 angular.module("myModule",[])
5 .service("myService",myServiceClass);

在改写一下上面的例子

 1 function doCalc(){
 2     var injector = angular.injector(["ezstuff"]),
 3         mycalculator = injector.get("ezCalculator"),
 4         ret = mycalculator.add(3,4);
 5 
 6     document.querySelector("#result").innerText = ret;
 7 }
 8 
 9 angular.module("ezstuff",[])
10     .service("ezCalculator",function(){
11         this.method1 =  {
12             add : function(a,b){return a+b;},
13             subtract : function(a,b){return a-b;},
14             multiply : function(a,b){return a*b;},
15             divide: function(a,b){return a/b;}
16         }
17                 this.getMethod = function(){
18                        return this.method1;
19                 }
20     })

好了,Angular中其实还有两个也比较常用的服务,value和constant,它们是用来定义一个常量的,也可以说是全局变量。定义模式非常简单,如果展开总结的话,应该叫angularJS如何定义全局变量。呵呵。关于这个回头我在单独写个总结。 

三、使用指令封装DOM操作

简介:AngularJS中指令分为内置指令和自定义指令,下面从三个主题总结:1、如何创建自定义指令(这会详细说明每个属性的使用)。2、自定义指令中如何操作DOM的。3、介绍一些常用指令的用法。

如何创建指令

指令也是一种服务,只是这种服务的定义有几个特殊要求

  • 必须使用模块的directive方法定义
  • 必须提供factory方法
  • factory方法返回的对象必须返回一个指令定义对象
 1 // 定义指令的类工厂
 2 var directiveFactory = function(injectables){
 3     // 指令定义对象
 4     var directiveDefinationObject = {
 5         ...
 6     };
 7     return directiveDefinationObject;
 8 };
 9 // 在模块上注册指令
10 angular.module("someModule",[])
11 .directive("directiveName",directiveFactory);
12 var ezHoverableFactory = function(){
13     return {
14         restrict : "A",
15                 replace  : true,
16                 template :"<p>测试内容</p>",
17         link : function(scope,element,attrs){
18             element.on("mouseover", function(){
19                 element.css({outline:"#ff0000 dotted thick"});
20             })
21             element.on("mouseout", function(){
22                 element.css({outline:"none"});
23             })
24         }
25     };
26 };
27 angular.module("ezstuff",[])
28 .directive("ezHoverable",ezHoverableFactory);
1 <span ez-hoverable>我是SPAN</span>
2 <button ez-hoverable>我是BUTTON</button>
3 <div ez-hoverable>我是DIV</div>
4 <textarea ez-hoverable>我是TEXTAREA</textarea>
5 <ez-hoverable></ez-hoverable>

每个指令定义的工厂函数,需要返回一个指令定义对象,编译器/$compile在编译时就根据这个定义对象对指令进行展开。

指令定义对象的常用属性如下: 

 1、link&compile

link函数负责实现DOM和scope的数据绑定,通常在link里执行DOM事件监听和数据变化监听。link函数在template执行后被调用。link是最常用的属性,一个指令的逻辑通常在link函数里实现。指令的执行可分为两个阶段compile和link。然而,compile的作用是什么,它和link函数有什么区别等一系列问题,我们通过运行两个实例来总结结论。

link实例:

 1 <hello></hello> 
1 return{

2     retrict:’E’,
3     template:’<div>hi everyone!</div>’,
4     link:function(scope,element,attrs){
5         element.on(“mouseenter”,function(){
6             console.log(“测试代码”);
7         })
8     }
9 }

说明:scope:这个指令所处的作用域。element:这指令所替换的当前元素。attrs:可以理解为是AngularJS提供的一个获取当前元素属性的方法。

compile实例:

1 <div alotofhello=“5”>
2     <p>你好,吃饭,再见!</p>
3 </div>
 1 return{
 2     restrict:’A’,
 3     compile: function(element,attrs,transclude){
 4        // 这里开始  对标签元素自身进行一些变换
 5        console.log(“指令编译…”);    
 6        var tpl = element.children().clone();
 7        console.log(tpl);
 8        for(var i= 0;i<attrs.alotofhello-1;i++){

 9           element.append(tpl.clone());
10        } 
11            // compile必须指定这么一个返回的函数 绑定链接 作为link函数
12        return function(scope,element,attrs,controller){
13           console.log(“指令链接…”);
14        }
15          }
16     
17          // 如果同时提供link函数的定义 结果会怎么呢? 如果注释compile在运行
18          link:function(){
19         console.log(“我自己的link函数…”);
20          } 
21 }

说明:在compile实例中红色标的代码不会被运行,因为它会被compile函数中return返回的link函数替换运行。当compile函数注释后,link函数又能正常运行。这行代码的输出结果是在页面中输出5行p元素。由此可以得出结论:

  1. compile一般很少用,一般不会和link同时用。
  2. compile函数的作用是对指令的模板进行转换。
  3. link函数的作用是在模型和视图之间建立关联,包括在元素上注册事件监听。
  4. 一般情况下我们只要编写link函数就够了。

2、restrict

可以是EACM这四个字母的做任意组合,用来限定指令的应用场景。如果不指定这个属性,默认情况下,指令将仅允许被用作元素和属性名。

  • E - 元素名,例如:<my-directive></my-directive>
  • A - 属性名,例如:<div my-directive="exp"></div>
  • C - 类,例如:<div class="my-directive: exp;"></div>
  • M - 注释,例如:<!-- directive:my-directive exp -->

也就是说,如果指令定义中restrict:'E',那么在模板页中就只能使用<my-customer></my-customer>才算合法。上面四行代码就是是例子,这里就不举例了。节省篇幅

3、template

template是一个HTML片段,可以用来:

  • 替换指令的内容。这是默认的行为,可以使用replace属性更改。
  • 替换指令本身(如果replace属性设为TRUE的话)。
  • 包裹指令的内容(如果transclue属性设为TRUE的话)。
 1 angular.module("ezstuff",[])
 2 .controller("ezCtrl",["$scope", function($scope){
 3     $scope.customer = {
 4         name: "Naomi",
 5         address: "1600 Amphitheatre"
 6     };
 7 }])
 8 .directive("ezCustomer", function(){
 9     return {
10         restrict:"AE",
11                 template:"<div>Name:{{customer.name}}Address:{{customer.address}}</div>",
12                 replace:true
13     }
14 })
1 <body ng-app="ezstuff" ng-controller="ezCtrl">
2     <ez-customer></ez-customer>
3 </body>

同等作用的属性还有templateUrl、$templateCache。templateUrl好理解一点,其意义就是引入一个页面片,现在举个$templateCache的实例,主要目的是说明它是怎么使用的。

 1 angular.module("ezstuff",[])
 2     .run(function($templateCache){
 3         $templateCache.put('hello.html','<div>HI everynone</div>');
 4     })
 5     .directive("hello",function(){
 6         return{
 7             restrict:'E',
 8             template:$templateCache.get('hello.html'),
 9             replace:true
10         }
11     })

这个例子中绿色的hello.html那个位置其实放的是一个函数$templateCache第二个参数的引用。 run方法只会被运行一次。

4、replace

指明是否使用template替换指令元素

  • true - 编译时,将使用template替换指令元素
  • false - 编译时,将使用template替换指令元素的内容

这个属性是比较好理解的,这里就不举实例了。用个例子运行一下,对照一下结果,区别就显而易见。呵呵 这里我得偷一下懒,后面难理解的我就弄个例子说一下。

5、transclude

有些指令需要能够包含其他未知的元素,比如一个对话框,我们不知道会有什么元素需要放在对话框里。如果要设计一个对话框指令,我们需要使用transclude。

 1 angular.module('docsTransclusionDirective', [])
 2     .controller('Controller', ['$scope', function($scope) {
 3       $scope.name = 'Tobias';
 4     }])
 5     .directive('myDialog', function() {
 6       return {
 7         restrict: 'E',
 8         transclude: true,
 9         template: '<div ng-transclude=""></div>'
10       };
11     });
1 <body ng-app="docsTransclusionDirective">
2     <div ng-controller="Controller">
3          <my-dialog>Check out the contents, {{name}}!</my-dialog>
4     </div>
5 </body> 

6、scope -建立独立的作用域

默认情况下,指令没有自己的作用域,当然,可以使用一个控制器构造独立的作用域,但更好的方法,是让指令有自己独立的作用域。通过独立的作用域的不同绑定,可以实现更具适应性的自定义标签,借由不同的绑定规则属性,从而定义出符合更多应用场景的标签。关于独立作用域搞弄清三个问题:

为何需要独立作用域

为了说清楚这个问题,先列出一个实例:

1 <div>
2    <xingoo></xingoo>
3    <xingoo></xingoo>
4    <xingoo></xingoo>
5 </div>
1 .directive("xingoo",function(){
2      return {
3          restrict:'AE',
4          template:'<div><input type="text" ng-model="username"/>{{username}}</div><br>',
5          repalce:true
6      }
7 })

运行的结果如下:

可以看到,在script中,创建了一个指令,该指令实现了一个自定义的标签。标签<xingoo></xingoo>的作用域是替换成一个输入框和一个数据显示。

然而,当我们自己创建某个指令时,这个指令肯定不可能只使用一次,是要重复多次使用的,有的在一个页面或者一个控制器内需要使用多次。类似于上面的这种场景,在任何一个输入框内改变数据,都会导致其他的标签内的数据一同发生改变,这显然不是我们想要的。这个时候就需要独立作用域了。

如何实现独立作用域

同样先给一个例子,然后在来分析它的运行结果。

1 .directive("xingoo",function(){
2      return {
3           restrict:'AE',
4           scope:{},
5           template:'<div><input type="text" ng-model="username"/>{{username}}</div><br>',
6           repalce:true
7      }
8 })

运行结果如下:

由此可见,只需要在定义指令时,添加scope:{}这个属性,就可以使标签拥有自己的作用域,仅仅是添加这一行代码而已,就实现了独立作用域。在进行输入时,每个模板使用自己的数据,不会相互干扰。

作用域的数据绑定

自定义标签或者进行扩展时,会有这样的需求场景,要在标签中添加一些属性,实现一些复杂功能。关于这些属性,独立作用域是如何做的呢?举个例子:

1 <xingoo say="name"></xingoo>
2 <xingoo say="name()"></xingoo>

假设传入的是上面这种,我们如何区分它传入的到底是变量呢?还是字符串呢?还是方法呢?

因此AngularJS有了三种自定义的作用域绑定方式:

  • 基于字符串的绑定:使用@操作符,双引号内的内容当做字符串进行绑定。
  • 基于变量的绑定:使用=操作符,绑定的内容是个变量。
  • 基于方法的绑定:使用&操作符,绑定的内容是个方法。

一、举个字符串绑定的例子:

1 <xingoo say="test string"></xingoo>
2 <xingoo say="{{str2}}"></xingoo>
3 <xingoo say="test()"></xingoo>
 1 .controller('Controller', ['$scope', function($scope) {
 2    $scope.str1 = "hello";
 3    $scope.str2 = "world";
 4    $scope.str3 = "angular";
 5 }])
 6 .directive("xingoo",function(){
 7    return {
 8         restrict:'AE',
 9         scope:{
10              say:'@'
11         },
12         template:"<div>{{say}}</div><br>",
13         repalce:true
14    }
15 })

运行结果如下:

 

看一下代码,在body中使用了三次自定义的标签,每种标签的内部有一个say的属性,这个属性绑定了一个双引号的字符串。

在指令的定义中,添加了scope:{say:'@'}这个键值对属性,也就是说,angular会识别say所绑定的东西是一个字符串。

在模板中,使用表达式{{say}}输出say所表示的内容。

在结果中可以看到,双引号内的内容都被当做了字符串。当然{{str2}}表达式会被解析成对应的内容,再当做字符串。

二、举个变量绑定的例子:

1 <div ng-controller="Controller">
2     ctrl:<input type="text" ng-model="testname"><br>
3     directive:<xingoo name="testname"></xingoo>
4 </div>
 1 .controller('Controller', ['$scope', function($scope) {
 2     $scope.testname="my name is wpzheng";
 3 }])
 4 .directive("xingoo",function(){
 5      return {
 6          restrict:'AE',
 7          scope:{
 8                name:'='
 9          },
10          template:'<input type="text" ng-model="name">',
11          repalce:true
12      }
13 })

运行结果如下:

 

这个例子中,在控制器Controller对应的div中,定义了一个变量ng-model-testname,testname对应的是输入框中输入的值,然后把这个变量当做一个参数传递给xingoo这个标签的name属性,在xingoo标签中,又把这个name绑定到模板中的一个输入框内。

最终两个输入框的内容被连接起来,无论改变哪一个输入框内的值,testname与name都会发生改变。

总之,在指令定义中通过scope属性say绑定规则是变量的绑定方式。最终通过xingoo标签内的属性依赖关系把testname与name连接在一起。

三、举个方法绑定的例子:

1 <xingoo say="sayHello(name)"></xingoo>
2 <xingoo say="sayNo(name)"></xingoo>
3 <xingoo say="sayYes(name)"></xingoo>
 1 .controller('Controller', ['$scope', function($scope) {
 2       $scope.sayHello = function(name){
 3            console.log("hello !"+ name);
 4       };
 5       $scope.sayNo = function(name){
 6            console.log("no !"+ name);
 7       };
 8       $scope.sayYes = function(name){
 9            console.log("yes !"+ name);
10       };
11 }])
12 .directive("xingoo",function(){
13       return {
14            restrict:'AE',
15            scope:{
16                 say:'&'
17            },
18            template:'<input type="text" ng-model="username"/><br>'+
19                 '<button ng-click="say({name:username})">click</button><br>',
20            repalce:true
21       }
22 })

运行结果如下:

 这段代码中scope中的绑定规则变成了&,也就是方法绑定,在body中,通过自定义标签传入了三个方法,分别是sayHello(name),sayNo(name),sayYes(name),这三个方法都需要一个name变量。

在指令的定义中,模板替换成一个输入框,一个按钮:

输入框:用于输入username,也就是三个方法需要的参数name。

按钮:点击触发函数——通过绑定规则,绑定到相应的方法。

也就是说:通过say在scope中的定义,angular知道了say对应的是个方法,通过{name:username}的关联,知道了传入的是username。从而交给对应的方法执行。

如何在指令中操作DOM

如果需要在指令中操作DOM,我们可以在选项中使用link,link需要指定一个函数,AngularJS将在编译时调用 这个函数,并传递scope、element和attrs这几个参数进去。

  • scope是scope对象。
  • element是jQuery对象。
  • attrs是规范化后的属性名/值哈希表。
1 <div ng-controller="Controller">
2       Date format: <input ng-model="format"> <hr>
3       Current time is: <span my-current-time="format"></span>
4 </div>
 1 .controller('Controller', ['$scope', function($scope) {
 2   $scope.format = 'M/d/yy h:mm:ss a';
 3 }])
 4 .directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) {
 5   function link(scope, element, attrs) {
 6     var format,
 7         timeoutId;
 8     function updateTime() {
 9       element.text(dateFilter(new Date(), format));
10     }
11     scope.$watch(attrs.myCurrentTime, function(value) {
12       format = value;
13       updateTime();
14     });
15     element.on('$destroy', function() {
16       $interval.cancel(timeoutId);
17     });
18     timeoutId = $interval(function() {
19       updateTime(); 
20     }, 1000);
21   }
22  
23   return {
24     link: link
25   };
26 }]); 

常用指令用法

一个项目最常用的指令有以下几个:

  1. ng-app 初始化一个AngularJS应用程序。
  2. ng-init 初始化应用程序数据,其实是将数据绑定在其作用域scope上。
  3. ng-model 把元素值(比如输入域的值)绑定到应用程序。
  4. ng-bind 把应用程序数据绑定到HTML视图。
  5. ng-repeat 会重复一个HTML元素。

这里举一个例子来说明以上指令的作用和区别:

1 <div ng-app = "">
2     <div ng-init = "firstName='John'">
3     <p>姓名为 <span ng-bind = "firstName"></span></p>
4     <p>在输入框中尝试输入:</p>
5     <p>姓名: <input type="text" ng-model = "name"></p>
6     <p ng-bind = "name"></p>
7     <p>{{name}}</p>
8 </div>

运行结果如下:

 

当网页加载完毕,AngularJS自动开启。ng-app指令告诉AngularJS,这个div元素所包含的内容都归我管了。

ng-init指令是初始化数据,这个指令在开发中并不很常见,因为在AngularJS应用中初始化数据一般会放在控制器里来完成。

ng-model指令把输入的值绑定到应用程序变量name。ng-bind指令把应用程序变量name绑定到某个段落的innerHTML。这两个指令的区别其实就是一个数据绑定方向的问题。

{{}}:作为AngularJS的取值表达式,它把数据绑定到HTML,这与ng-bind指令有异曲同工之妙。在表达式书写的位置“输出”数据。然而{{}}和ng-bind在数据显示上是有一点需要我们考虑的。

因为浏览器需要首先加载index.html页面,渲染它,然后AngularJS才能把它解析成你期望看到的内容。所以使用表达式时,页面渲染时可以会被用户看到表达式。因此对于index.html页面中的数据绑定操作,建议采用ng-bind。这样在数据加载完成之前用户就不会看到任何内容。

四、使用过滤器转化输出

简介:AngularJS中过滤器分为内置过滤器和自定义过滤器,下面从两个主题总结:1、如何使用过滤器。2、如何创建过滤器。

如何使用过滤器

一、在视图模板中使用过滤器

过滤器也是一种服务,负责对输入的内容进行处理转换,以便更好的向用户显示。

过滤器可以在模板中的{{}}标记中使用:

 1 {{ expression | filter:arg1:arg2}} 

1、预置的过滤器

AngularJS的ng模块实现了一些预置的过滤器,如:currency、number等等,可以直接使用。例如下面的示例将对数字12使用currency过滤器,结果将是“$12.00”;

 1 {{12|currency}} 

tips:AngularJS内置了9个过滤器,分别是currency, date, filter, json, limitTo, uppercase, lowercase, number, orderBy。在文档中都要详细的使用说明。

2、带参数的过滤器

过滤器也可以有参数,例如下面的示例将数字格式化为“1,234.00”;

 1 {{1234|number:2}} 

3、过滤器流水线

过滤器可以应用于另一个过滤器的输出,可称之为链式调用,通俗点讲就是Filter叠加--前一filter的输出结果作为后一filter的输入数据源。语法如下:

 1 {{expression|filter1|filter2|…}} 

二、在代码中使用过滤器

别忘了过滤器也是一种服务,所以你可以将它注入你的代码中。

和普通的服务不同,过滤器在注入器中注册时,名称被加了一个后缀:Filter。例如,number过滤器的服务名称是:numberFilter,而currency过滤器的服务名称是:currencyFilter。

通常我们的代码被封装在三个地方:控制器、服务器、指令。这些地方都支持服务的直接注入,例如:

1 angular.module('myModule',[])
2 .controller(function($scope,numberFilter){
3     //...
4 })

有时你需要显式的通过注入器调用过滤器,那么使用注入器的invoke方法:

1 angular.injector(['ng'])
2 .invoke(function(numberFilter){
3     //...
4 })

总之,记住过滤器是一种服务,除了名字需要追加Filter后缀,和其他服务的调用方法没什么区别。 

如何创建过滤器

一、如何创建过滤器

过滤器也是一种特殊的服务,与创建一个普通的服务相比较:

1、必须使用模块的filter()接口定义

2、必须提供factory方法

3、factory方法必须返回一个过滤器函数,其第一个参数为输入变量

 1 // 定义过滤器类工厂
 2 var filterFactory = function(){
 3     // 定义过滤器函数
 4     var filter = function(input){
 5         return output
 6     }
 7 };
 8 // 在模块上注册过滤器
 9 angular.module("someModule",[])
10 .filter(“filterName",filterFactory);

举个例子:

 1 <p>{{text|ezUC}}</p></body> 
1 var ezUCFilterFactory = function(){
2     var filter = function(input){
3         return input.toUpperCase();
4     }
5     return filter;
6 };
7 angular.module("ezstuff",[])
8 .filter("ezUC",ezUCFilterFactory);

二、为过滤器增加参数

过滤器的行为可以通过额外的参数来调整。比如,我们希望改进上面的示例,使其可以支持仅大写每个单词的首字母。

实现阶段:通过在过滤器类工厂返回的过滤器函数中传入额外的参数,就可以实现这个功能。

 1 var filter = function(input,argument1,argument2){…} 

调用阶段:在使用过滤器时,额外的参数通过前缀:引入,比如

 1 {{expression|filter:argument1:argument2}} 

改写例子如下:

1 <body ng-init="text='just a demo!'">
2     <p>{{text|ezUC:true}}</p>
3 </body>
 1 var ezUCFilterFactory = function(){
 2     return function(input){
 3         var output = input.replace(/w+/g, function(word) {
 4             return word.substring(0,1).toUpperCase( ) +word.substring(1);
 5         });
 6         return output;
 7     }
 8 };
 9 angular.module("ezstuff",[])
10 .filter("ezUC",ezUCFilterFactory); 

小结

这部分包含的是AngularJS的主要内容,控制器、服务器、指令和过滤器。以它们在构建应用中的作用为主体,分别做了详细的说明。

1、使用控制器封装业务逻辑:如何定义一个控制器以及如何在控制器上定义变量和方法。定义了一个控制器就意味着建立了一个新的scope作用域。因为控制器的设计出发点是封装单个视图的业务逻辑,所以有一些行为不应该用它来完成,比如DOM操作、变换输出形式、跨控制器共享代码。

2、使用服务器封装可复用的代码:服务器是用来封装可复用代码的,可以用provider定义一个服务器,同时AngularJS提供了一另外两个定义服务的简单方式,分别为factory函数和service函数。只它们定义方式稍有区别。

3、使用指令封装DOM操作:AngularJS如何创建一个指令,逐一介绍了自定义指令的每个属性的意义和使用,有link、restrict、transclude、replace、template(templateUrl、$templateCache)、compile、还独立作用域scope等。列举了指令是如何操作DOM的以及几个常用指令的用法。

4、使用过滤器转化输出:过滤器有内置封装好的,有用户自定义的。过滤器的使用又可分为在视图模板中使用和在代码中使用。最后又总结了一下如何自定义指令和如何使用自定义指令。

最后,这篇文章总算写完了,希望能给正在接触AngualrJS框架的前端人们带来参考。也作为自己后期使用和再次更新的基础。有讲的不对的内容欢迎指正,共同进步。

参考资料

http://www.w3cschool.cc/angularjs/angularjs-intro.html(w3c)

http://www.apjs.net/(中文网)

http://docs.angularjs.cn/api(英文文档)

http://www.angularjs.cn/tag/AngularJS (中文社区)

http://www.imooc.com

欢迎 扫描 加入前端问题讨论公众号 拥有更多前端内容分享

原文地址:https://www.cnblogs.com/zhengwenping/p/4611967.html