AngularJS是什么
AngularJS的官方文档这样介绍它:
完全使用JavaScript编写的客户端技术。同其它历史悠久的Web技术(HTML、CSS、JavaScript)配合使用,使Web应用开发比以往更简单、更快捷。
AngularJS的开发团队将其描述为一种构建动态Web应用的结构化框架。AngularJS主要用于构建单页面Web应用。
AngularJS通过原生的Model-View-Controller
(MVC,模型-视图-控制器)功能增强了HTML。利用它可以将页面的一部分内容封装成一个应用,并且不强迫整个页面都使用AngularJS进行开发。
MVC(模型 视图 控制器)
MVC背后的核心理念就是,你在你的代码之间明确分离管理数据(模型)、应用程序逻辑(控制器)、并将数据给用户(视图)。
在AngularJS应用中,视图层就是DOM,控制器就是JavaScript类,模型数据存储在对象属性中。
视图从模型中获取数据展示给用户。当用户通过点击或者输入和应用程序进行交互时,控制器通过改变模型中的数据响应。最终,模型层通知视图层,已经发生改变,更新显示。
数据绑定
Hello World
第一个AngularJS例子
{{ name }}
运行效果:
AngularJS中的数据绑定
要实现AngularJS的功能,只要在页面中引入angular.js
,并在某个DOM元素上明确设置ng-app
属性即可。ng-app
属性声明所有被其包含的内容都属于这个angularJS应用。只有被具有ng-app
属性的DOM元素包含的元素才会受AngularJS影响。
AngularJS会记录数据模型所包含的数据在任何特定时间点的值,而不是原始值。当AngularJS认为某个值可能发生变化时,它会运行自己的事件循环来检查这个值是否变“脏”。如果该值从上次事件循环运行之后发生了变化,则该值被认为是“脏”值。这个过程被称作脏检查。脏检查是检查数据模型变化的有效手段。当有潜在的变化存在时,AngularJS会在事件循环时执行脏检查来保证数据的一致性。
模块
在AngularJS中,模块是定义应用的最主要方式。模块包含了主要的应用代码。一个应用可以包含多个模块,每一个模块都包含了定义具体功能的代码。
我们使用angular.module()
方法来声明模块,这个方法接受两个参数,第一个是模块的名称,第二个是依赖列表,也就是可以被注入到模块中的对象列表。
angular.module('myApp', []);
//这个方法相当于AngularJS模块的setter方法,是用来定义模块的。
调用这个方法时如果只传递一个参数,就可以用它来引用模块。
// 这个方法用于获取应用
angular.module(' myApp ')
//这个方法相当于AngularJS模块的getter方法,用来获取对模块的引用。
接下来,就可以在angular.module('myApp')
返回的对象上创建应用了。
参数
下面是angular.module()
的参数列表。
- name(字符串):
name
是模块的名称,字符串变量。 - requires(字符串数组):
requires
包含了一个字符串变量组成的列表,每个元素都是一个模块名称,本模块依赖于这些模块,依赖需要在本模块加载之前由注入器进行预加载。
作用域
应用的作用域是和应用的数据模型相关联的,同时作用域也是表达式执行的上下文。$scope
对象是定义应用业务逻辑、控制器方法和视图属性的地方。
在应用将视图渲染并呈献给用户之前,视图中的模板会和作用域进行连接,然后应用会对DOM进行设置以便将属性变化通知给AngularJS。
作用域是应用状态的基础。基于动态绑定,我们可以依赖视图在修改数据时立刻更新$scope
,也可以依赖$scope
在其发生变化时立刻重新渲染视图。
AngularJS将$scope
设计成和DOM类似的结构,因此$scope
可以进行嵌套,也就是说我们可以引用父级$scope
中的属性。
作用域提供了监视数据模型变化的能力。它允许开发者使用其中的apply
机制,将数据模型的变化在整个应用范围内进行通知。我们在作用域的上下文中定义和执行表达式,同时它也是将事件通知给另一个控制器和应用其他部分的中介。
视图和$scope
AngularJS启动并生成视图时,会将根ng-app
元素同$rootScope
进行绑定。$rootScope
是所有$scope
对象的最上层。
$rootScope
是AngularJS中最接近全局作用域的对象。在$rootScope
上附加太多业务逻并不是好主意,这与污染JS的全局作用域是一样的。
$scope
对象就是一个普通的JS对象,我们可以在其上随意修改或添加属性。
$scope
对象充当数据模型,$scope
并不负责处理和操作数据,它只是视图和HTML之间的桥梁。
$scope
的所有属性,都可以自动被视图访问到。假设我们有如下的代码:
Hello {{name}}
我们希望{{name}}
变量是本地$scope
的一个属性。
angular.module('myApp', []).run(function($rootScope) {
$rootScope.name = "World";
});
我们可以在AngularJS应用的模板中使用多种标记,包括下面这些:
- 指令:将DOM元素增强为可复用的DOM组件的属性或元素
- 值绑定:模板语法
{{ }}
可以将表达式绑定到视图上 - 过滤器:可以在视图中使用的函数,用来进行格式化
- 表单控件:用来检验用户输入的控件
作用域能做什么
作用域有以下的基本功能:
- 提供观察者以监视数据模型的变化
- 可以将数据模型的变化通知给整个应用,甚至是系统外的组件
- 可以进行嵌套,隔离业务功能和数据
- 给表达式提供运算时所需的执行环境
作用域包含了渲染视图时所需的功能和数据,它是所有视图的唯一源头。可以将作用域理解成视图模型(view model
)。
angular.module('myApp', []).run(function($rootScope) {
$rootScope.name = "World";
});
Hello {{ name }}
我们可以不将变量设置在$rootScope
上,而是用控制器显式创建一个隔离的子$scope
对象,把它设置到这个子对象上。使用ng-controller
指令可以将一个控制器对象附加到DOM元素上,如下所示:
Hello {{ name }}
可以创建一个控制器来管理与其相关的变量,而不用将name
变量直接放在$rootScope
上。
angular.module("myApp",[]).controller('MyController',function($scope) {
$scope.name = "Ari";
});
ng-controller
指令为这个DOM元素创建了一个新的$scope
对象,并将它嵌套在$rootScope
中。
$scope的生命周期
当AngularJS关心的事件发生在浏览器中时,比如用户在通过ng-model
属性监控的输入字段中输入,或者带有ng-click
属性的按钮被点击时,AngularJS的事件循环都会启动。这个事件将在AngularJS执行上下文中处理。
每当事件被处理时,$scope
就会对定义的表达式求值。此时事件循环会启动,并且AngularJS应用会监控应用程序内的所有对象,脏值检测循环也会运行。
$scope
对象的生命周期处理有四个不同阶段。
- 创建
在创建控制器或指令时,AngularJS会用$injector
创建一个新的作用域,并在这个新建的控制器或指令运行时将作用域传递进去。 - 链接
当AngularJS开始运行时,所有的$scope
对象都会附加或者链接到视图中。所有创建$scope
对象的函数也会将自身附加到视图中。这些作用域将会注册当AngularJS应用上下文中发生变化时需要运行的函数。这些函数被称为$watch
函数,AngularJS通过这些函数获知何时启动事件循环。 - 更新
当事件循环运行时,它通常执行在顶层$scope
对象上(被称作$rootScope
),每个子作用域都执行自己的脏值检测。每个监控函数都会检查变化。如果检测到任意变化,$scope
对象就会触发指定的回调函数。 - 销毁
当一个$scope
在视图中不再需要时,这个作用域将会清理和销毁自己。
尽管永远不会需要清理作用域(因为AngularJS会为你处理),但是知道是谁创建了这个作用域还是有用的,因为你可以使用这个$scope
上叫做$destory()
的方法来清理这个作用域。
指令和作用域
指令通常不会创建自己的$scope
,但也有例外。比如ng-controller
和ng-repeat
指令会创建自己的子作用域并将它们附加到DOM元素上。
控制器
控制器的作用是增强视图。在AngularJS中的控制器是一个函数,用来向视图的作用域中添加额外的功能。我们用它来给作用域对象设置初始状态,并添加自定义行为。
当我们在页面上创建一个新的控制器时,AngularJS会生成并传递一个新$scope
给这个控制器。可以在这个控制器里初始化$scope
。由于AngularJS会负责处理控制器的实例化过程,我们只需编写构造函数即可。
function FirstController($scope) {
$scope.message = "hello";
}
我们是在全局作用域中创建的这个函数。这样做并不合适,因为会污染全局命名空间。更合理的方式是创建一个模块,然后在模块中创建控制器。
var app = angular.module('app', []);
app.controller('FirstController', function($scope) {
$scope.message = "hello";
});
只需创建控制器作用域中的函数,就能创建可以在视图中使用的自定义操作。我们可以在视图中像调用普通数据一样调用$scope
上的函数。
用内置指令ng-click
可以将按钮、链接等其他任何DOM元素同点击事件进行绑定。ng-click
指令将浏览器中的mouseup
事件,同设置在DOM元素上的事件处理程序绑定在一起。
Subtract
Current count: {{counter}}
按钮和链接都被绑定在了内部$scope
的一个操作上,当点击任何一个元素时AngularJS都会调用相应的方法。下面给FirstController
添加一个操作:
app.controller('FirstController', function($scope) {
$scope.counter = 0;
$scope.add = function(amount) { $scope.counter += amount; };
$scope.subtract = function(amount) { $scope.counter -= amount; };
});
用这种设置方式我们可以在视图中调用add()
或subtract()
方法,这两个方法可以定义在FirstController
的作用域中,或其父级的$scope
中。
控制器可以将与一个独立视图相关的业务逻辑封装在一个独立的容器中。
控制器并不适合用来执行DOM操作、格式化或数据操作,以及除存储数据模型之外的状态维护操作。它只是视图和$scope
之间的桥梁。
AngularJS允许在$scope
上设置包括对象在内的任何类型的数据,并且在视图中还可以展示对象的属性。
app.controller('MyController', function($scope) {
$scope.person = {
name: 'Ari Lerner'
};
});
在拥有ng-controller='MyController'
这个属性的元素内部的任何子元素中,都可以访问person
对象,因为它是定义在$scope
上的。
{{ person }}
and their name:
{{ person.name }}
效果如图:
正如看到的这样,
$scope
对象用来从数据模型向视图传递信息。同时,它也可以用来设置事件监听器,同应用的其他部分进行交互,以及创建与应用相关的特定业务逻辑。
控制器嵌套(作用域包含作用域)
AngularJS应用的任何一个部分,无论它渲染在哪个上下文中,都有父级作用域存在。对于ng-app
所处的层级来讲,它的父级作用域就是$rootScope
。有一个例外:在指令内部创建的作用域被称作孤立作用域。除了孤立作用域外,所有的作用域都通过原型继承而来,也就是说它们都可以访问父级作用域。
默认情况下,AngularJS在当前作用域中无法找到某个属性时,便会在父级作用域中进行查找。如果AngularJS找不到对应的属性,会顺着父级作用域一直向上寻找,直到抵达$rootScope
为止。如果在$rootScope
中也找不到,程序会继续运行,但视图无法更新。
app.controller('ParentController', function($scope) {
$scope.person = {greeted: false};
});
app.controller('ChildController', function($scope) {
$scope.sayHello = function() {
$scope.person.name = 'Ari Lerner';
};
});
如果我们将ChildController
置于ParentController
内部,那ChildController
的$scope
对象的父级作用域就是ParentController
的$scope
对象。根据原型继承的机制,我们可以在子作用域中访问ParentController
的$scope
对象。
{{ person }}
效果如图:
控制器应该尽可能保持短小精悍,而在控制器中进行DOM操作和数据操作则是一个不好的实践。
angular.module('myApp',[]).controller('MyController', function($scope) {
$scope.shouldShowLogin = true;
$scope.showLogin = function () {
$scope.shouldShowLogin = !$scope.shouldShowLogin;
};
$scope.clickButton = function() {
$('#btn span').html('Clicked');
};
$scope.onLogin = function(user) {
$http({
method: 'POST',
url: '/login',
data: {
user: user
}
}).success(function(data) {
// user
});
};
});
设计良好的应用会将复杂的逻辑放到指令和服务中。通过使用指令和服务,我们可以将控制器重构成一个轻量且更易维护的形式。
angular.module('myApp',[]).controller('MyController',function($scope,UserSrv) {
// 内容可以被指令控制
$scope.onLogin = function(user) {
UserSrv.runLogin(user);
};
});
表达式
用{{ }}
符号将一个变量绑定到$scope
上的写法本质上就是一个表达式:{{expression}}
。当用$watch
进行监听时,AngularJS会对表达式或函数进行运算。
表达式和eval(js)
非常相似,但是由于表达式由AngularJS来处理,它们有以下显著不同的特性:
- 所有的表达式都在其所属的作用域内部执行,并有访问本地
$scope
的权限 - 如果表达式发生了
TypeError
和ReferenceError
并不会抛出异常 - 不允许使用任何流程控制功能(例如
if/else
) - 可以接受过滤器和过滤器链
对表达式进行的任何操作,都会在其所属的作用域内部执行,因此可以在表达式内部调用那些限制在此作用域内的变量,并进行循环、函数调用、将变量应用到数学表达式中等操作。
解析AngularJS表达式
尽管AngularJS会在运行$digest
循环的过程中自动解析表达式,但有时手动解析表达式也是非常有用的。AngularJS通过$parse
这个内部服务来进行表达式的运算,这个服务能够访问当前所处的作用域。这个过程允许我们访问定义在$scope
上的原始JS数据和函数。
将$parse
服务注入到控制器中,然后调用它就可以实现手动解析表达式。```
{{ parseValue }}
angular.module("myApp", [])
.controller('MyController',function ($scope,$parse) {
$scope.$watch('expr',function (newVal,oldVal,scope) {
if (newVal !==oldVal) {
// 用该表达式设置parseFun
var parseFun = $parse(newVal);
// 获取经过解析后表达式的值
$scope.parsedValue = parseFun(scope);
}
})
})
插值字符串
在AngularJS中,我们的确有手动运行模板编译的能力。例如,插值允许基于作用域上的某个条件实时更新文本字符串。要在字符串模板中做插值操作,需要在对象中注入$interpolate
服务。
angular.module('myApp',[])
.controller('MyController',function($scope,$interpolate) {
// 我们同时拥有访问$scope和$interpolate服务的权限
});
$interpolate
服务是一个可以接受三个参数的函数,其中第一个参数是必需的。
- text(字符串):一个包含字符插值标记的字符串。
- mustHaveExpression(布尔型):如果将这个参数设为
true
,当传入的字符串中不含有表达式时会返回null
。 - trustedContext(字符串):AngularJS会对已经进行过字符插值操作的字符串通过
$sec.getTrusted()
方法进行严格的上下文转义。
$interpolate
服务返回一个函数,用来在特定的上下文中运算表达式。
设置好这些参数后,就可以在控制器中进行字符插值的操作了。例如,假设我们希望可以在电子邮件的正文中进行实时编辑,当文本发生变化时进行字符插值操作并将结果展示出来。
{{ previewText }}
由于控制器内部设置了一个需要每次变化都重新进行字符插值的自定义输入字段,因此需要设置一个$watch
来监听数据的变化。$watch
函数会监视$scope
上的某个属性。只要属性发生变化就会调用对应的函数。可以使用$watch
函数在$scope
上某个属性发生变化时直接运行一个自定义函数。
angular.module('myApp',[])
.controller('MyController', function($scope, $interpolate) {
// 设置监听
$scope.$watch('emailBody', function(body) {
if (body) {
var template = $interpolate(body);
$scope.previewText =template({to: $scope.to});
}
};
});
现在,在{{previewText}}
内部的文本中可以将{{to}}
当做一个变量来使用,并对文本的变化进行实时更新。如果需要在文本中使用不同于{{ }}
的符号来标识表达式的开始和结束,可以在$inter polateProvider
中配置。
用startSymbol()
方法可以修改标识开始的符号。这个方法接受一个参数。
- value(字符型):开始符号的值。
用endSymbol()
方法可以修改标识结束的符号。这个方法也接受一个参数。
- value(字符型): 结束符号的值。
如果要修改这两个符号的设置,需要在创建新模块时将$interpolateProvider
注入进去。
下面我们来创建一个服务:
angular.module('emailParser',[])
.config(['$interpolateProvider', function($interpolateProvider) {
$interpolateProvider.startSymbol('__');
$interpolateProvider.endSymbol('__');
}])
.factory('EmailParser', ['$interpolate', function($interpolate) {
// 处理解析的服务
return {
parse: function(text, context) {
var template = $interpolate(text);
return template(context);
}
};
}]);
现在,我们已经创建了一个模块,可以将它注入到应用中,并在邮件正文的文本中运行这个邮件解析器:
angular.module('myApp',['emailParser'])
.controller('MyController', ['$scope', 'EmailParser',function($scope, EmailParser) {
// 设置监听
$scope.$watch('emailBody', function(body) {
if (body) {
$scope.previewText = EmailParser.parse(body, {
to: $scope.to
});
}
});
}]);
由于我们将表达式开始和结束的符号都设置成了__
,因此需要将HTML修改成用这个符号取代{{ }}
的版本。
完整代码:
__ previewText __