AngularJS 控制器
概括
控制器是 AngularJS MVC 架构中的关键组件之一。虽然保持控制器精简是一个好目标,最好将代码放在模型和服务中,但控制器及其使用方法仍然有很多内容。在本文中,我将讨论控制器以及在 AngularJS 应用程序中使用它们的各种方法。这将包括讨论如何处理模型、使用服务、嵌套控制器以及将控制器连接到视图的各种方法。
控制器基础知识
在之前关于AngularJS 中的 MVC 模式的文章中,我讨论了控制器的工作是如何将视图与模型连接起来的。视图处理视觉布局,而模型处理大部分业务逻辑。但是,由于控制器支持AngularJS 中的依赖注入系统,而简单模型对象不支持,因此控制器通常利用服务来帮助构建模型,或处理来自视图的交互。
将控制器连接到视图
为了为视图准备模型,控制器需要了解视图。将控制器连接到视图的最简单方法是使用 HTML 标记中的指令将两者静态链接起来,如下所示。
<body>
<h1>Hack Hands - Controllers</h1>
<div ng-controller="technologyController">
<label>{{heading}}</label>
</div>
<script src="./scripts/app.js"></script>
<script src="./scripts/technologyController.js"></script>
</body>
此指令指示 AngularJS 实例化在同名 JavaScript 文件中定义的 technologyController,并使用其定义的模型进行数据绑定。这意味着数据或事件处理程序的绑定语句将尝试解析为控制器在创建时设置的模型对象。给定以下控制器定义,“heading”绑定将正确解析。
angular.module('hackApp')
.controller('technologyController', technologyController);
function technologyController($scope) {
$scope.heading = "Choose your technology";
}
控制器连接到视图的另一种常见方式是使用路由。在这种情况下,视图和控制器都被指定为特定路由的参数,如这个简单示例所示。
angular.module('hackApp', ['ngRoute'])
.config(function($routeProvider){
$routeProvider.when('/mentoring', {
templateUrl: '/views/technologies.html',
controller: 'technologyController'
})
.otherwise({
templateUrl: '/views/main.html',
controller: 'technologyController'
})
});
使用路由时,控制器仍会以相同的方式为视图设置模型,但控制器本身会通过路由链接到视图,因此无需在模板标记中使用指令。假设控制器将提供模型,则模板或视图可以专注于绑定语句和自定义指令。
路由的一个不同之处在于路由可以有参数,而控制器几乎总是需要访问部分或全部参数才能正确加载模型。例如,列出技术的页面可能会提供链接,以便深入了解每种技术以查找可用导师的列表。所讨论的技术将是该视图的控制器加载适当导师所需的 URL 参数。给定这样的 URL:/mentoring/jquery,参数“jquery”应该传递给控制器。
实现该功能需要两个步骤:将路由参数声明为路由配置的一部分,并设置控制器以接收路由参数。对于第一步,在注册路由时在位置参数中添加占位符来定义参数的名称和位置。
$routeProvider.when('/mentoring/:tech', {
templateUrl: '/views/technologies.html',
controller: 'technologyController'
})
如此处所示,路由参数包含在 URL 中的适当位置,名称以冒号 (:) 为前缀,以区别于路径的其他部分。使用上一个 URL 示例 (/mentoring/jquery),“tech”路由参数现在具有“jquery”的值。
解决方案的第二部分是让控制器使用 $routeParams 服务来收集参数。来自路由位置的每个命名参数都将有一个可访问的值挂在此对象上。继续之前的示例,“tech”参数可以通过控制器代码中的 $routeParams 对象访问,并用于以任何需要的方式帮助构建模型。
function technologyController($scope, $routeParams) {
if($routeParams.tech) {
$scope.selectedTech = $routeParams.tech;
}
}
需要注意的是,每次导航到路线都会导致调用控制器函数来创建模型。这是有道理的,因为在这种情况下,每次调用都可能针对不同的参数,并且应该为模型收集不同的数据集。但是,理解这一点很重要,因为这意味着在导航到控制器的另一个实例时,尝试在控制器中本地存储数据会失败。此外,如果您预计用户可能会频繁重新访问路线,那么在您的应用程序设计中包含某种形式或缓存可能是有意义的。
控制者身份
在以后的文章中,我将讨论范围和管理模型,但模型的一个方面是能够通过名称将控制器对象本身添加到范围中。使用此选项时,无需直接在控制器内使用 $scope 对象,因为您可以直接将属性和方法添加到控制器并绑定到视图中的属性和方法。
angular.module('hackApp').controller('modelController', modelController);
function modelController() {
var vm = this;
vm.techs = ['JavaScript', 'ASP.NET', 'C#'];
vm.sortAsc = function(){
vm.techs.sort();
};
vm.sortDesc = function(){
vm.techs.sort();
vm.techs.reverse();
};
}
控制器函数的第一行将一个作用域变量设置为“this”变量,该变量当前代表控制器本身。一旦创建了该作用域变量,它就会被视为视图模型,并在该模型上定义属性和函数。在这种情况下,定义了一个技术数组和两种按升序或降序对数组进行排序的方法。(请不要过多关注排序函数。有很多更好的方法来对数组进行排序,但这不是本例的重点。)
为了使用这个控制器模型,必须更新视图并且还必须修改路由定义或控制器指令。
<div ng-controller="modelController as model">
<button ng-click="model.sortAsc()">Asc</button>
<button ng-click="model.sortDesc()">Desc</button>
<br />
<ul>
<li ng-repeat="tech in model.techs">{{tech}}</li>
</ul>
</div>
在上面的例子中,控制器指令使用了声明要使用的控制器并提供在视图中引用的名称的模式。在这种情况下,上面的 modelController 将通过名称“model”引用。然后在 ng-click 事件处理程序指令和 ng-repeat 指令中,绑定语句基于对控制器的“model”引用。在后台,控制器只是添加了带有名为“model”的属性的 $scope。考虑到这一点,绑定语法应该完全合理。
使用路由时,controllerAs 参数被传递给“when”或“otherwise”函数,以提供在视图中用来引用控制器的名称。
$routeProvider.when('/model', {
templateUrl: '/views/model.html',
controller: 'modelController',
controllerAs: 'model'
})
与路由一起使用的视图与没有控制器指令的上一个示例看起来相同。这种方法简化了控制器中的代码,并使视图中的数据绑定更加明确。本质上,这种方法删除了控制器的一个依赖项,这让我们开始讨论依赖项以及如何管理它们。
使用依赖项
当控制器为视图准备模型或处理来自视图的交互时,它们通常会与服务一起完成实际工作。AngularJS 的主要设计目标之一是使用依赖注入来消除直接创建这些服务的需要。这不仅使单元测试控制器更容易(我将很快展示),而且还简化了代码并使其更易于维护。有几种不同的方法来管理控制器依赖项的声明和解析。
处理依赖项的主要模型是将一个数组传递给控制器的构造函数,该函数按名称列出依赖项,并包含一个要调用的函数,该函数将这些依赖项作为参数,如本例所示。
angular.module('hackApp')
.controller('mentorController', ['$http','$scope',mentorController]);
function mentorController($http, $scope) {
$scope.mentors = ['Matt', 'Bob', 'Alice', 'Raj', 'Priscilla'];
}
请注意,在此示例中,控制器函数采用控制器的名称,后跟一个包含依赖项列表和控制器函数的数组。另请注意,依赖项的顺序在声明和参数列表中是相同的。请注意,也可以排除列出依赖项的数组,并从控制器函数参数名称中推断出它们。这假设参数名称与服务名称匹配,并且还会使您的代码不适合缩小,因此通常不推荐这种方法。
另一种方法是通过属性将依赖项列表应用于控制器函数,这提供了另一个最小化安全选项。许多开发人员更喜欢此选项,因为它使控制器函数本身更简洁、更易读,尤其是当依赖项列表变大时。
angular.module('hackApp')
.controller('technologyController', technologyController);
technologyController.$inject = ['$http', '$scope', '$routeParams'];
function technologyController($http, $scope, $routeParams) {
//implementation omitted
}
在此示例中,technologyController 被声明为控制器函数,但未列出依赖项。然后在列出这些依赖项的函数上设置 $inject 属性。与上一种方法一样,依赖项列表的顺序必须与参数列表的顺序相同,因此如果您添加、删除或重新排序参数,则必须保持它们同步。
因为这两种方法都将依赖项列为字符串,所以在对代码进行压缩处理时,这些值不会被压缩。这确实意味着需要做更多工作来列出依赖项两次并保持列表同步。因此,创建了一个开源项目 ng-annotate 来简化此过程,使您能够简单地定义函数的参数,并让预处理工具在控制器上为您创建 $inject 属性。此工具确实要求您在控制器函数中添加注释或属性,但这是一个与参数列表无关的一致标记。在开始构建应用程序时,值得查看 ng-annotate 并将其纳入您的工具链中,以简化控制器的 JavaScript 代码。
使用路由时,在某些情况下,您可能希望将依赖项作为承诺提供给控制器,而不是直接由 AngularJS 注入器解析它们。例如,如果您有异步加载数据的服务,或者您需要根据某些路由参数或依赖项返回的数据决定是否显示视图,从而调用控制器。对于这些情况,路由提供程序有一个 resolve 属性,您可以设置该属性,它允许您解析依赖项并创建在移至控制器之前全部解析的承诺。
.when('/model', {
templateUrl: '/views/model.html',
controller: 'modelController',
controllerAs: 'model',
resolve: {'modelService':modelServiceWrapper}
在路由提供程序配置的这段代码中,名为“modelService”的依赖项将使用此处显示的函数 modelServiceWrapper 进行解析。
function modelServiceWrapper(){
//replace this pseudocode with an asynchronous request to the server
return {techs: ['JavaScript', 'ASP.NET', 'AngularJS']};
}
在此示例中,我使用了简单的测试代码,您通常会在其中放置服务逻辑以从服务器加载项目或解析控制器所需的其他数据。然后,控制器函数可以声明对 modelService 的依赖,并在其设置逻辑中使用生成的对象,此时承诺已解析,这意味着数据已加载。
angular.module('hackApp')
.controller('modelController', ['modelService', modelController]);
function modelController(modelService) {
var vm = this;
vm.techs = modelService.techs;
}
这种方法的好处是,控制器所需的所有承诺都会在控制器需要结果之前得到解决。这使得路由能够根据上下文和承诺结果做出决策,以取消路由或应用其他逻辑。
嵌套控制器
让控制器在工作中只发挥单一作用是一种很好的做法。但是,这意味着有时控制器可能需要与另一个控制器合作来生成和处理页面中的整个视图。例如,对于主从视图,一个控制器可能负责主上下文,而另一个单独的控制器负责处理细节。在这些情况下,您可以嵌套控制器。
在我的示例中,我有一个视图,它显示一种特定的技术,然后列出可用于该技术的导师及其详细信息。在给定的视图中,我可以将 mentorController 嵌套在 technologyController 中,以提供完整的视图模型和处理程序。这样,我的控制器可以在不同的视图中单独使用,但一起使用时可以相互协调。
当控制器以这种方式嵌套时,实际上会为每个控制器创建一个新作用域,并且子作用域从父级继承。下一篇文章将更详细地介绍该主题,其中我将深入介绍作用域和模型。现在,重要的是要知道您可以以这种方式使用控制器以保持它们的单一用途。
您可能想在子控制器中构建引用父上下文的逻辑,但这种紧密耦合在几乎所有情况下都行不通。在下一篇文章中,我将展示如何使用服务和其他技术来使用嵌套控制器/范围,而无需考虑这些依赖关系。
测试控制器
在本系列文章中,我曾多次提到 AngularJS 大量利用依赖注入,而可测试性是其主要优势之一。如果我不谈论解决方案中的测试控制器,那我就太失职了。在测试时,依赖注入允许向更适合单元测试场景的控制器提供服务,并支持结果验证和模拟或存根。AngularJS 提供了 ngMock 模块来帮助对 AngularJS 应用程序进行单元测试。
ngMock 模块提供了一些有助于测试过程的功能。第一个是“mock”对象上的“module”和“inject”方法,用于在测试框架中加载模块并将服务注入应用程序。第二个是一组模拟服务,它们为运行时服务提供了可测试的替代方案。这些服务包括 HTTP 后端服务、日志记录服务和异常处理程序,仅举几例。这些替代服务允许对控制器进行受控输入并对控制器函数执行的结果进行预期检查,从而使测试更加容易。
例如,$httpBackend mock 允许为某些请求预定义响应,从而在调用请求时为控制器提供一致的输入。它还允许定义期望,以便在控制器测试过程中未执行预期请求时抛出错误。$log mock 捕获日志输出并使其可用于期望测试。
大多数 AngularJS 开发人员使用 Mocha 或 Jasmine 作为单元测试框架,而 Karma 是 AngularJS 团队创建的测试运行器,可简化单元测试的运行。使用这些工具以及 ngMock 模块可提供编写完整单元测试所需的核心框架。根据开发人员及其团队的个人偏好,还有其他工具通常用于更高级的模拟或存根功能。
在下面的代码中,Jasmine 测试规范中使用了几个 ngMock 组件。除了 AngularJS 核心和路由模块以及应用程序本身之外,还包含 ngMock 模块。
describe("technologyController", function() {
var $httpBackend, techRequestHandler, $controller;
beforeEach(function(){
module('hackApp');
});
beforeEach(function() {
inject(function($injector){
$controller = $injector.get("$controller");
$httpBackend = $injector.get('$httpBackend');
techRequestHandler = $httpBackend.when('GET', '/techs')
.respond(['Angular', 'JavaScript', 'HTTP', 'HTML']);
});
});
ngMock 的“module”和“inject”方法用于获取对模块的引用并注入 $httpBackend mock 并获取 $controller 对象,这将允许在下一步中创建控制器实例。请注意,$httpBackend 是使用 when().respond() 方法链设置的,以便在调用 HTTP GET 请求时为控制器创建预期输入。由于这些数据是已知的,因此可以在调用控制器函数后测试结果。
describe("Create technology based on route information", function(){
it("should create the list of technologies when no route parameters are present", function() {
var $scope = {};
var $routeParams = {};
var controller = $controller('technologyController',
{$http:null, $scope: $scope, $routeParams:$routeParams });
expect($scope.selectedTech).toBeUndefined();
expect($scope.techs.length).toEqual(3);
});
设置模拟后,每个测试都可以创建其他输入,例如 $scope 和 $routeParams 对象作为输入,并使用 $controller 对象在需要时使用依赖注入创建控制器实例。调用控制器函数后,可以检查 $scope 以测试预期。此特定示例不使用 $http 服务或模拟,但下一个示例会使用。
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~