Angular 简介
虽然最近出了 Angular2,但是因为Angular2 依然是alpha版本,所以Angular1作为之前最火的前端框架依然有大量人在使用。
Angular 的特点:
- 双向数据绑定,主打卖点
- MVVM 模型,把视图和逻辑分开
- 依赖注入
个人感觉,在Angular中,视图对应 HTML 模板,视图逻辑对应directive 和 controller。
模块
Angular 中通过模块来管理命名空间,可以通过不同的模块来隔离不同页面的逻辑。所虽然它称作 “module”,但其实更像是一个命名空间,或者叫一个包。
通过 angular.module(“name”, [/依赖/]) 来声明一个模块。Angular中的模块声明周期分两步,一个是配置阶段,一个是运行阶段。
angular.module(“app-name”, [])
.config(function() { //配置阶段,先执行
})
.run(function() { //运行阶段,后执行
});
一般我们给按页面划分模块,也可以把整个应用都声明成一个模块,然后通过模块来启动整个页面的逻辑。
可以把run当做main函数,如果有一些在应用启动时需要执行的代码,比如判断当前有没有登录,可以放在run函数中。
双向数据绑定
双向数据绑定是 Angular 的主打特色。一般我的数据都是单向绑定的,也就是当JS中的变量发生变化的时候更新到DOM,但是大部分时候并不会在DOM中的值变化的时候去自动更新JS中的变量。
看一个双向绑定的例子:
<input ng-model=“user.name" type="text" placeholder="Your name">
<h1>Hello {{ user.name }}</h1>
这样就实现了一个双向绑定,当在input中输入值的时候,h1
中的值会立刻发生变化。
因为JS传值的问题,建议绑定的时候总是通过对象属性而不是通过直接传值的方式来做。
控制器(controller)
在 Angular 中,控制器的作用就是创建新的作用域,Angular创建一个控制器的时候都会为其创建一个 $scope
,这个 $scope
就是一个新的作用域。当然你可以声明这个作用域和父作用域的关系,到底是隔离还是继承。
在angular中这样声明一个控制器:
app.module(“home”, [])
.controller(“MyController”, function($scope) {
$scope.name = “Mr Lee”;
});
在HTML中这样使用
<div MyController>{{name}}</div>
<!— 或者这样 —>
<MyController>{{name}}</div>
<!— 还有通过class或者注释等方式都可以使用 —>
Angular 是一个 MVVM 模型,即 Model - View - ViewModel,其中的 ViewModel 是视图对应的Model,在Angular中就是 $scope。因此 $scope
的作用就是存放与对应视图相关的数据。比如上例中我们就是存储了一个名字,如果是一个个人名片,我们存储的就是这个人的基本信息。
在 Angular 中 , 存在一个 $rootScope
,所有的其他 $scope
形成了一颗以 $rootScope
为根节点的树。每一个 $scope 都负责对应视图的数据存储,业务逻辑等。
在一个 controller 的作用域范围内,可以直接使用 $scope 上的属性,比如你的 $scope
是这样声明的:
$scope = { name: “Lily”, sayName: function(){alert($scope.name{});
那么你在HTML中可以这么使用 scope
<h2>name: {{name}}</h2>
<button ng-click=“sayName()”>say name</button>
脏检查
Angular 内部通过 dirty check
来跟踪数据变化,这是双向数据绑定的实现基础。
所谓脏检查,就是 angular 会给每一个数据绑定一个 watcher,当到“特定检查阶段”时,angular就会逐个询问 watcher 它对应的数据有没有发生变化,如果有,则运行对应的监视器。直到没有任何脏数据为止。这个过程称为 digest 循环。
注意,并不是有一个定时线程来不停做脏检查。Angular 只有当特定的事件发生时才会进行脏检查。所以如果用户在input中进行输入,或者通过 $http 取回数据,angular当然直到此时数据可能会变化,会自动指定 digest 循环。但是如果像下面的代码这样,angular就无法知晓,因此DOM中显示的数据并不会变化。
.controller("dirty-check-test", function($scope) {
var model = $scope.model = {
time:1,
};
setInterval(function(){
model.time ++; //错误,无法触发 digest 循环。
}, 1000);
那么我们可以通过 $scope.$apply
方法来通知 Angular 需要进行 digest 循环了。
setInterval(function() {
//正确的方式,通过 $apply 通知变化
$scope.$apply(function() {
model.time ++;
});
}, 1000);
我们可以通知 Angular 数据变化,也可以让Angular来告诉我们一个数据是否发生变化,通过 $scope.$watch
我们可以注册一个监听,当数据变化的时候立刻执行回调
$scope.$watch(“model.time”, function() {//xx});
Angular 为什么要用脏检查,因为这样是兼容性最好并且对用法限制最少的实现方式。其实还有两种备选方案可以实现:
- 通过 ES5 中定义的新的 getter setter 接口来观察数据是否变化,不过对不支持的浏览器就没办法了
- 通过自定义的 getter setter 接口来改变数据,这样就对 $scope 有极大限制,必须通过 Angular 提供的接口来修改数据。
内置指令
在 Angular 中通过指令对DOM的功能进行扩展。这也是对常用功能的模块化封装。Angular 提供了一系列常用的指令,这些指定都是以 ng
开头的,我们成为内置指令。
后面会讲到如何自定义指令。其实内置指令和自定义指令是没有区别的,只是angular已经帮我们定义好了。
默认指令有如下几种常用的:
- ng-app 启用一个模块
- ng-model 双向绑定数据
- ng-init 直接初始化生成一个 scope
- ng-controller 调用 controller
- ng-click 绑定click事件
- ng-href
- ng-src
- ng-disabled
- ng-checked
- ng-readonly
- ng-selected
- ng-class
- ng-style
以 ng-click
为例,这样使用:
<div ng-click=“onClick()”></div>
自定义指令(directive)
我们完全可以只依赖angular内置的指令,以及自己写的各种 controller 来完成一个应用。但是这样就无法复用一些通用的视图逻辑。
在 Angular 中,通过指令来拓展DOM元素的功能。我们可以把通用的逻辑封装成指令,在不同的地方都可以调用这些指令。比如我们可以把 分页器封装成一个指令,使用分页器的时候就不需要写复杂的逻辑,只要像下面这样指定几个参数即可:
<div pagination page-total=‘100’ page-current=’20’></div>
这样就会调用 pagination
指令自动生成一个分页器,并通过两个属性告诉它总页数是100,当前是第20页。
那么我们这样编写指令:
.directive("pagination", function() {
return {
restrict: "EA",
template:
"<div>\
<button ng-click='prev()' ng-disabled='!hasPrev()'>prev</button>\
<span>{{current}}</span>\
<button ng-click='next()' ng-disabled='!hasNext()'>next</button>\
<div>",
replace: true,
scope: {
total: "@pageTotal",
current: "@pageCurrent"
},
link: function($scope) {
$scope.total = parseInt($scope.total);
$scope.current = parseInt($scope.current);
$scope.prev = function() {
$scope.current --;
}
$scope.next = function() {
$scope.current ++;
}
$scope.hasNext = function() {
return $scope.current < $scope.total;
}
$scope.hasPrev = function() {
return $scope.current > 0;
}
}
}
})
其中, 我们定义了一个 pagination
指令,返回的一个对象描述了这个指令的全部定义。我们一点一点看:
restrict: “EA”
表示这个指令可以通过元素名或者属性名的方式来使用,可选值有 “EAC” 三种,分别是 Element, Attribute 和 Class 的缩写。
template
表示这个指令对应的模板,结合下面的 replace: true
会直接讲使用此指令的dom换成对应的模板。
scope
是一个JSON对象,表示为这个指令创建了一个隔离的作用域,其中通过 @
语法把 DOM 中的属性复制到 scope 对象中。
link
是一个链接函数,一般我们指令中的逻辑代码写在链接函数中。当Angular完成模块编译和数据绑定之后,会调用link函数进行链接。这里我们定义了四个函数,用来进行页面跳转动作和跳转条件的判断。
看完这个分页器指令基本就能知道一个指令的基本组成,一般一个指令的最重要部分就是模板、作用域和链接函数。如果你在这个模板上声明了一个 controller ,那么这里的作用域其实就是 controller的作用域。不过由于angular限制一个DOM上只能绑定一个隔离作用域,所以这里如果声明了一个controller,则无法再在 指令中声明隔离作用域。
依赖注入
angular 提供了依赖注入的功能。有两种方式可以声明依赖,一个是通过参数名来声明,比如:
.controller(“MyController”, function($scope, $http) {})
angular会通过 $http
这个参数名来推断出需要注入 $http
服务。不过这个方式有一个坏处就是,如果代码进行了压缩,那么参数名会被修改掉。此时我们应该用显示的声明:
.controller(“MyController”, [“$scope”, “$http”, function($scope, $http) {} ]);
因为字符串常量是不会被压缩的,所以这种声明方式不会被代码压缩工具所影响。
服务
服务是在不同的控制器中共享数据并且在整个应用生命周期内保持一致的最好方式。
比如很多网站都需要登录,而登录用户的信息其实是贯穿整个Session的。那么我们可以创建一个 CurrentUser 服务。
.factory(“CurrentUserService”, function() {});
当需要的时候,我们直接通过依赖注入把这个服务注入进来即可:
.controller(“MyController”, [“$scope”, “CurrentUserService, function($scope, $user) {}]);
Angular中的服务都是单例的。并且angular也提供了 $http 这种内置的服务。
参考
- 《精通AngularJS》
- 《AngularJS权威教程》
- angular 官网 https://angularjs.org/