在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'); }); });