在Angular中,控制器就像 JavaScript 中的构造函数一般,是用来增强 Angular作用域(scope) 的。
当一个控制器通过 ng-controller 指令被添加到DOM中时,ng 会调用该控制器的构造函数来生成一个控制器对象,这样,就创建了一个新的子级 作用域(scope)。在这个构造函数中,作用域(scope)会作为$scope参数注入其中,并允许用户代码访问它。
一般情况下,我们使用控制器做两件事:
$scope 对象$scope 对象添加行为(方法)$scope 对象当我们创建应用程序时,我们通常要为Angular的 $scope 对象设置初始状态,这是通过在 $scope 对象上添加属性实现的。这些属性就是供在视图中展示用的视图模型(view model),它们在与此控制器相关的模板中均可以访问到。
下面的例子中定义了一个非常简单的控制器构造函数:GreetingCtrl,我们在该控制器所创建的 scope 中添加一个 greeting 属性:
function GreetingCtrl($scope) {
$scope.greeting = 'Hola!';
}
如上所示,我们有了一个控制器,它初始化了一个 $scope对象,并且有一个greeting属性。当我们把该控制器关联到DOM节点上,模板就可以通过数据绑定来读取它:
<div ng-controller="GreetingCtrl">
</div>
注意:虽然Angular允许我们在全局作用域下(window)定义控制器函数,但建议不要用这种方式。在一个实际的应用程序中,推荐在 Angular模块 下通过 .controller 为你的应用创建控制器,如下所示:
var myApp = angular.module('myApp',[]);
myApp.controller('GreetingCtrl', ['$scope', function($scope) {
$scope.greeting = 'Hola!';
}]);
在上面例子中,我们使用内联注入的方式声明 GreetingCtrl 依赖于Angular提供的 $scope 服务。更多详情,参阅 依赖注入 。
$scope 对象添加行为为了对事件作出响应,或是在视图中执行计算,我们需要为 scope 提供相关的行为操作的逻辑。上面一节中,我们为 scope 添加属性来让模板可以访问数据模型,现在,我们为 $scope 添加方法来让它提供相关的交互逻辑。添加完之后,这些方法就可以在模板/视图中被调用了。
下面的例子将演示为控制器的 scope 添加方法,它用来使一个数字翻倍:
var myApp = angular.module('myApp',[]);
myApp.controller('DoubleCtrl', ['$scope', function($scope) {
$scope.double = function(value) { return value * 2; };
}]);
当上述控制器被添加到DOM之后,double 方法即可被调用,如在模板中的一个Angular表达式中:
<div ng-controller="DoubleCtrl">
<input ng-model="num"> 翻倍后等于
</div>
如 概述 部分所指出的一样,任何对象(或者原生类型的变量)被添加到 scope 后都将成为 scope 的属性,作为数据模型供模板/视图调用。任何方法被添加到 scope 后,也能在模板/视图中通过Angular表达式或是Angular的事件处理器(如:ngClick)调用。
通常情况下,控制器不应被赋予太多的责任和义务,它只需要负责一个单一视图所需的业务逻辑。
最常见的保持控制器“纯度”的方法是将那些不属于控制器的逻辑都封装到服务(services)中,然后在控制器中通过依赖注入调用相关服务。详见指南中的 依赖注入 服务 这两部分。
注意,下面的场合千万不要用控制器:
通过两种方法可以实现控制器和 scope 对象的关联:
ngController指令 这个指令就会创建一个新的 scope为了更深入地阐释Angular的控制器是如何工作的,我们用以下几个部件来构建一个小型应用:
spice 的数据模型对象,是一个字符串spice 的值模板中的消息包含了一个到数据模型 spice 的绑定,默认值为 very。之后,取决于哪个按钮被点击,spice 的值会被置为 chili 或是 jalapeño ,受益于数据绑定,模板中的这个消息会在 spice 变化时自动更新。
<!doctype html>
<html ng-app="spicyApp1">
<head>
<script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-app="spicyApp1" ng-controller="SpicyCtrl">
<button ng-click="chiliSpicy()">Chili</button>
<button ng-click="jalapenoSpicy()">Jalapeño</button>
<p>The food is spicy!</p>
</div>
</body>
</html>
var myApp = angular.module('spicyApp1', []);
myApp.controller('SpicyCtrl', ['$scope', function($scope){
$scope.spice = 'very';
$scope.chiliSpicy = function() {
$scope.spice = 'chili';
};
$scope.jalapenoSpicy = function() {
$scope.spice = 'jalapeño';
};
}]);
上面的例子中有几个值得注意的地方:
ng-controller 指令用来为我们的模板创建一个 scope ,而且它受到 SpicyCtrl 控制器的管理SpicyCtrl 就是一个普通的 JavaScript 函数,只是命名上以首字母大写,以 "Ctrl" 或 "Controller" 结尾$scope 这样会创建或更新一个数据模型chiliSpicy 方法<div> 元素及其子节点控制器方法可以带参数,我们看一下如下范例(是上面例子的变种):
<!doctype html>
<html ng-app="spicyApp2">
<head>
<script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-app="spicyApp2" ng-controller="SpicyCtrl">
<input ng-model="customSpice">
<button ng-click="spicy('chili')">Chili</button>
<button ng-click="spicy(customSpice)">Custom spice</button>
<p>The food is spicy!</p>
</div>
</body>
</html>
var myApp = angular.module('spicyApp2', []);
myApp.controller('SpicyCtrl', ['$scope', function($scope){
$scope.customSpice = "wasabi";
$scope.spice = 'very';
$scope.spicy = function(spice){
$scope.spice = spice;
};
}]);
注意上面的 SpicyCtrl 控制器现在只定义了一个 spicy 方法,带一个 spice 参数。然后在模板中,第一个按钮调用 spicy 方法的时候传进一个字符串常量 'chili' ;第二个按钮则传进一个与<input>进行了双向绑定的数据模型 customSpice(初始值在 scope 中设置为了 'wasabi')。(译者注:这样在 <input> 输入框输入什么,点击第二个按钮时,<p> 标签就会显示 <input> 中的当前值。)
我们常常会在不同层级的DOM结构中添加控制器。由于 ng-controller 指令会创建新的子级 scope ,这样我们就会获得一个与DOM层级结构相对应的的基于继承关系的 scope 层级结构。(译者注:由于 Js 是基于原型的继承,所以)底层(内层)控制器的 $scope 能够访问在高层控制器的 scope 中定义的属性和方法。详情参见 理解“作用域” 。
译者注:下面是一个拥有三层div结构,也就对应有三层 scope 继承关系的层级结构(不包括 rootScope 的话),demo中的蓝色边框很清晰的展现了 scope 的层级和DOM层级的对应关系。它还展示了“scope 是由 ng-controller 指令创建并由其对应的控制器所管理”这个概念。
<!doctype html>
<html ng-app="scopeInheritance">
<head>
<script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-app="scopeInheritance" class="spicy">
<div ng-controller="MainCtrl">
<p>Good {{timeOfDay}}, {{name}}!</p>
<div ng-controller="ChildCtrl">
<p>Good {{timeOfDay}}, {{name}} !</p>
<div ng-controller="GrandChildCtrl">
<p>Good {{timeOfDay}}, {{name}}!</p>
</div>
</div>
</div>
</div>
</body>
</html>
div.spicy div {
padding: 10px;
border: solid 2px blue;
}
var myApp = angular.module('scopeInheritance', []);
myApp.controller('MainCtrl', ['$scope', function($scope){
$scope.timeOfDay = 'morning';
$scope.name = 'Nikki';
}]);
myApp.controller('ChildCtrl', ['$scope', function($scope){
$scope.name = 'Mattie';
}]);
myApp.controller('GrandChildCtrl', ['$scope', function($scope){
$scope.timeOfDay = 'evening';
$scope.name = 'Gingerbreak Baby';
}]);
注意,上面例子中我们在HTML模板中嵌套了三个 ng-controller 指令,这导致我们的视图中有4个 scope:
MainCtrl 控制器管理的 scope (简称 MainCtrl scope),拥有 timeOfDay 和 name 两个属性ChildCtrl 控制器管理的 scope (简称 ChildCtrl scope),继承了 MainCtrl scope 中的 timeOfDay 属性,但重写了它的 name 属性GrandChildCtrl 控制器管理的 scope (简称 GrandChildCtrl scope),重写了 MainCtrl scope 中的 timeOfDay 属性和 ChildCtrl scope 中的 name 属性控制器中,方法继承和属性继承的工作方式是一样的,所以,上面例子中的所有属性,我们也可以改写成能够返回字符串值的方法,同样有效。
虽然我们有很多方法可以对控制器进行测试,但在这里,我们仅展示最常见的一种,包括注入 $rootScope 以及 $controller :
控制器定义:
var myApp = angular.module('myApp',[]);
myApp.controller('MyController', function($scope) {
$scope.spices = [{"name":"pasilla", "spiciness":"mild"},
{"name":"jalapeno", "spiceiness":"hot hot hot!"},
{"name":"habanero", "spiceness":"LAVA HOT!!"}];
$scope.spice = "habanero";
});
控制器测试:
describe('myController function', function() {
describe('myController', function() {
var $scope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
$controller('MyController', {$scope: $scope});
}));
it('should create "spices" model with 3 spices', function() {
expect($scope.spices.length).toBe(3);
});
it('should set the default value of spice', function() {
expect($scope.spice).toBe('habanero');
});
});
});
如果有需要测试嵌套关系的控制器,那么在你的测试代码中,你也得创建对应于 scope 层级结构的测试代码:
describe('state', function() {
var mainScope, childScope, grandChildScope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
mainScope = $rootScope.$new();
$controller('MainCtrl', {$scope: mainScope});
childScope = mainScope.$new();
$controller('ChildCtrl', {$scope: childScope});
grandChildScope = childScope.$new();
$controller('GrandChildCtrl', {$scope: grandChildScope});
}));
it('should have over and selected', function() {
expect(mainScope.timeOfDay).toBe('morning');
expect(mainScope.name).toBe('Nikki');
expect(childScope.timeOfDay).toBe('morning');
expect(childScope.name).toBe('Mattie');
expect(grandChildScope.timeOfDay).toBe('evening');
expect(grandChildScope.name).toBe('Gingerbreak Baby');
});
});