AngularJS 模块加载
AngularJS模块可以在被加载和执行之前对其自身进行配置。我们可以在应用的加载阶段应用不同的逻辑组。
在模块的加载阶段, AngularJS会在提供者注册和配置的过程中对模块进行配置。在整个AngularJS的工作流中,这个阶段是唯一能够在应用启动前进行修改的部分。
config()函数接受一个参数。
angular.module('myApp', [])
.config(function($provide) {
});
使用config()函数的语法糖,并在配置阶段执行。例如,我们在某个模块之上创建一个服务或指令时:
angular.module('myApp', []) .factory('myFactory', function(){ var service = {}; return service; }).directive('myDirective', function(){ return { template: '<button>Click me</button>' } })
AngularJS会在编译时执行这些辅助函数。它们在功能上等同于下面的写法:
angular.module('myApp', []) .config(function($provide ,$compileProvider) { $provide.factory('myFactory', function() { var service = {}; return service; }); $compileProvider.directive('myDirective', function() { return { template: '<button>Click me</button>' }; }); });
AngularJS会以这些函数书写和注册的顺序来执行它们。
唯一例外的是constant()方法,这个方法总会在所有配置块之前被执行。
当对模块进行配置时,只有少数几种类型的对象可以被注入到config()函数中:提供者和常量。
这种对配置服务进行严格限制的另外一个副作用就是,我们只能注入用provider()语法构建的服务,其他的则不行。
定义多个配置块,它们会按照顺序执行,这样就可以将应用不同阶段的配置代码集中在不同的代码块中
angular.module('myApp', []) .config(function($routeProvider) { $routeProvider.when('/', { controller: 'WelcomeController', template: 'views/welcome.html' }); }) .config(function(ConnectionProvider) { ConnectionProvider.setApiKey('SOME_API_KEY'); });
运行块
和配置块不同,运行块在注入器创建之后被执行,它是所有AngularJS应用中第一个被执行的方法。run
运行块是AngularJS中与main方法最接近的概念。运行块通常用来注册全局的事件监听器。例如,我们会在.run()块中设置路由事件的监听器以及过滤未经授权的请求。
假设我们需要在每次路由发生变化时,都执行一个函数来验证用户的权限,放置这个功能唯一合理的地方就是run方法:
angular.module('myApp', []) .run(function($rootScope, AuthService) { $rootScope.$on('$routeChangeStart', function(evt, next, current) { // 如果用户未登录 if (!AuthService.userLoggedIn()) { if (next.templateUrl === "login.html") { // 已经转向登录路由因此无需重定向 } else { $location.path('/login'); } } }); });
多重视图和路由
从1.2版本开始, AngularJS将ngRoutes从核心代码中剥离出来成为独立的模块。我们需要安装并引用它,才能够在AngularJS应用中正常地使用路由功能
创建一个独立的路由:用when方法来添加一个特定的路由。有两个参数(when(path,route))。
angular.module('myApp', []). .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/', { templateUrl: 'views/home.html', controller: 'HomeController' }); }]);
稍微复杂一点:
angular.module('myApp', []). .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/', { templateUrl: 'views/home.html', controller: 'HomeController' }) .when('/login', { templateUrl: 'views/login.html', controller: 'LoginController' }) .when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardController', resolve: { user: function(SessionService) { return SessionService.getCurrentUser(); } } }) .otherwise({ redirectTo: '/' }); }]);
1. controller controller: 'MyController' 或者 controller: function($scope) {}
如果配置对象中设置了controller属性,制器会与路由所创建的新作用域关联在一起。如果参数值是字符型,会在模块中所有注册过的控制器中查找,如果参数值是函数,函数会作为模板中DOM元素的控制器并与模板进行关联
2. template
template: '<div><h2>Route</h2></div>' 渲染到对应的具有ng-view指令的DOM元素中
5. redirectTo
redirectTo: '/home'
// 或者
redirectTo: function(route,path,search)
如果redirectTo属性的值是一个字符串,那么路径会被替换成这个值,如果redirectTo属性的值是一个函数,那么路径会被替换成函数的返回值,并根据这个目标路径触发路由变化。
$routeParams
如果我们在路由参数的前面加上:, AngularJS就会把它解析出来并传递给$routeParams。
$routeProvider.when('/inbox/:name', { controller: 'InboxController', templateUrl: 'views/inbox.html' })
在$routeParams中添加一个名为name的键,它的值会被设置为加载进来的URL中的值。如果浏览器加载/inbox/all这个URL,$routeParams:{ name:'all' }
如果想要在控制器中访问这些变量,需要把$routeParams注入进控制器:
app.controller('InboxController',function($scope,$routeParams) { // 在这里访问$routeParams });;
$location 服务
用以解析地址栏中的URL,可以访问应用当前路径所对应的路由。它同样提供了修改路径和处理各种形式导航的能力。
$location服务对JavaScript中的window.location对象的API进行了更优雅地封装,并且和AngularJS集成在一起。
当应用需要在内部进行跳转时是使用$location服务的最佳场景,比如当用户注册后、修改或者登录后进行的跳转.
$location服务没有刷新整个页面的能力。如果需要刷新整个页面,需要使用$window.location对象(window.location的一个接口)。
$location.path(); // 返回当前路径
$location.path('/'); // 把路径修改为'/'路由
如果你希望跳转后用户不能点击后退按钮.AngularJS提供了replace()方法来实现
$location.path('/home'); $location.replace(); // 或者 $location.path('/home').replace();
路由模式
路由模式决定你的站点的URL长成什么样子。
标签模式
标签模式是HTML5模式的降级方案, URL路径会以#符号开头。 使用标签模式的URL看起来是这样的:http://yoursite.com/#!/inbox/all
要显式指定配置并使用标签模式,需要在应用模块的config函数中进行配置:
angular.module('myApp', ['ngRoute']) .config(['$locationProvider', function($locationProvider) { $locationProvider.html5Mode(false);
$locationProvider.hashPrefix('!'); //这一句要不要都行 }]);
HTML5 模式
$location服务通过HTML5历史API让应用能够使用普通的URL路径来路由。当浏览器不支持HTML5历史API时, $location服务会自动使用标签模式的URL作为替代方案。
在HTML5模式中, AngularJS会负责重写<a href=""></a>中的链接。也就是说AngularJS会根据浏览器的能力在编译时决定是否要重写href=""中的链接
例如<a href="/person/42?all=true">Person</a>这个标签,在老式浏览器中会被重写成标签模式的URL: /index.html#!/person/42?all=true。但在现代浏览器中会URL会保持本来的样子
当在HTML5模式的AngularJS中写链接时,永远都不要使用相对路径。如果你的应用是在根路径中加载的,这不会有什么问题,但如果是在其他路径中, AngularJS应用就无法正确处理路由了。
12.5.2 路由事件
$route服务在路由过程中的每个阶段都会触发不同的事件,可以为这些不同的路由事件设置监听器并做出响应。
我们需要给路由设置事件监听器,用$rootScope来监听这些事件。
1. $routeChangeStart
在路由变化之前会广播 $routeChangeStart事件,路由服务会开始加载路由变化所需要的所有依赖,并且模板和resolve键中的promise也会被resolve。
angular.module('myApp', []) .run(['$rootScope', '$location', function($rootScope, $location) { $rootScope.$on('$routeChangeStart', function(evt, next, current) { }); }]);
$routeChangeStart事件带有两个参数:
将要导航到的下一个URL; 路由变化前的URL。
2. $routeChangeSuccess
AngularJS会在路由的依赖被加载后广播$routeChangeSuccess事件。
angular.module('myApp', []) .run(['$rootScope', '$location', function($rootScope, $location) { $rootScope.$on('$routeChangeSuccess', function(evt, next, previous) { }); }]);
$routeChangeStart事件带有三个参数:
原始的AngularJS evt对象; 用户当前所处的路由; 上一个路由(如果当前是第一个路由,则为undefined)。
3. $routeChangeError
AngularJS会在任何一个promise被拒绝或者失败时广播$routeChangeError事件。
angular.module('myApp', []) .run(function($rootScope, $location) { $rootScope.$on('$routeChangeError', function(current, previous, rejection) { }); });
$routeChangeError事件有三个参数:
当前路由的信息; 上一个路由的信息; 被拒绝的promise的错误信息。
依赖注入
一个对象通常有三种方式可以获得对其依赖的控制权:
(1) 在内部创建依赖;
(2) 通过全局变量进行引用;
(3) 在需要的地方通过参数进行传递。
依赖注入是通过第三种方式实现的。其余两种方式会带来各种问题,例如污染全局作用域,使隔离变得异常困难等。
依赖注入会事先自动查找依赖关系,并将注入目标告知被依赖的资源,这样就可以在目标需要时立即将资源注入进去。
在运行期,注入器会创建依赖的实例,并负责将它传递给依赖的消费者。
// 出自Angular文档的优秀示例 function SomeClass(greeter) { this.greeter = greeter; } SomeClass.prototype.greetName = function(name) { this.greeter.greet(name); }; //示例代码在全局作用域上创建了一个控制器,这并不是一个好主意,这里只是为了方便演示。
SomeClass能够在运行时访问到内部的greeter,不关心如何获得对greeter的引用。为了获得对greeter实例的引用, SomeClass的创建者会负责构造其依赖关系并传递进去。
AngularJS使用$injetor(注入器服务)来管理依赖关系的查询和实例化。$injetor负责实例化AngularJS中所有的组件,包括应用的模块、指令和控制器等
在运行时, 任何模块启动时$injetor都会负责实例化,并将其需要的所有依赖传递进去。
一个简单的应用,声明了一个模块和一个控制器:
angular.module('myApp', []) .factory('greeter', function() { return { greet: function(msg) {alert(msg);} } }) .controller('MyController',function($scope, greeter) { $scope.sayHello = function() { greeter.greet("Hello!"); }; });
当AngularJS实例化这个模块时,会查找greeter并自然而然地把对它的引用传递进去:
<div ng-app="myApp"> <div ng-controller="MyController"> <button ng-click="sayHello()">Hello</button> </div> </div>
而在内部, AngularJS的处理过程是下面这样的:
var injector = angular.injector(['ng', 'myApp']); // 使用注入器加载应用 var $controller = injector.get('$controller'); // 通过注入器加载$controller服务: var scope = injector.get('$rootScope').$new(); // 加载控制器并传入一个作用域,同AngularJS在运行时做的一样 var MyController = $controller('MyController', {$scope: scope})
代码中没有说明是如何找到greeter,但是它的确能正常工作,因为$injector会负责为我们查找并加载它。
通过annotate函数,在实例化时从传入的函数中把参数列表提取出来。
任何一个AngularJS的应用中,都有$injector在进行工作,当编写控制器时,如果没有使用[]标记或进行显式的声明, $injector就会尝试通过参数名推断依赖关系
推断式注入声明
如果没有明确的声明, AngularJS会假定参数名称就是依赖的名称。因此,它会在内部调用函数对象的toString()方法,分析并提取出函数参数列表,然后通过$injector将这些参数注入进对象实例。
例如: 注入过程如下: injector.invoke(function($http, greeter) {}); 只适用于未经过压缩和混淆的代码,因为AngularJS需要原始未经压缩的参数列表来进行解析。(此时,参数的顺序 没有什么 意义了)
显式注入声明
显式的方法来明确定义一个函数在被调用时需要用到的依赖关系。通过这种方法声明依赖,即使在源代码被压缩、参数名称发生改变的情况下依然能够正常工作。
通过$inject属性来实现显式注入声明的功能。函数对象的$inject属性是一个数组,数组元素的类型是字符串,它们的值就是需要被注入的服务的名称。
var aControllerFactory =function aController($scope, greeter) { console.log("LOADED controller", greeter); // ……控制器 }; aControllerFactory.$inject = ['$scope', 'greeter']; // Greeter服务 // 我们应用的控制器 angular.module('myApp', []) .controller('MyController', aControllerFactory) .factory('greeter', greeterService); // 获取注入器并创建一个新的作用域 var injector = angular.injector(['ng', 'myApp']), controller = injector.get('$controller'), rootScope = injector.get('$rootScope'), newScope = rootScope.$new(); // 调用控制器 controller('MyController', {$scope: newScope});
这种声明方式来讲,参数顺序是非常重要的,$inject数组元素的顺序必须和注入参数的顺序一一对应。这种声明方式可以在压缩后的代码中运行,
行内注入声明
行内声明的方式允许我们直接传入一个参数数组而不是一个函数。数组的元素是字符串,它们代表的是可以被注入到对象中的依赖的名字,最后一个参数就是依赖注入的目标函数对象本身。
angular.module('myApp') .controller('MyController', ['$scope', 'greeter', function($scope, greeter) { }]);
处理的是一个字符串组成的列表,行内注入声明也可以在压缩后的代码中正常运行。 也是有顺序的
服 务:
出于内存占用和性能的考虑,控制器只会在需要时被实例化,并且不再需要就会被销毁。这意味着每次切换路由或重新加载视图时,当前的控制器会被AngularJS清除掉。
服务提供了一种能在应用的整个生命周期内保持数据的方法,它能够在控制器之间进行通信,并且能保证数据的一致性。(能够在控制器中 复用)
服务是一个单例对象,在每个应用中只会被实例化一次(被$injector实例化),并且是延迟加载的(需要时才会被创建)。
以AngularJS的$http服务为例,提供了对浏览器的XMLHttpRequest对象的底层访问功能,通过$http的API同XMLHttpRequest进行交互,而不需要因为调用这些底层代码而污染应用。
// 示例服务,在应用的整个生命周期内保存current_user angular.module('myApp', []) .factory('UserService', function($http) { var current_user; return { getCurrentUser: function() { return current_user; }, setCurrentUser: function(user) { current_user = user; } }; });
在AngularJS中创建自己的服务是非常容易的:只需要注册这个服务即可。服务被注册后,AngularJS编译器就可以引用它,并且在运行时把它当作依赖加载进来。
注册一个服务
使用angular.module的factory API创建服务,是最常见也是最灵活的方式:
angular.module('myApp.services', []) .factory('githubService', function() { var serviceInstance = {}; // 我们的第一个服务 return serviceInstance; });
用githubService作为名字注册成为这个AngularJS应用的一个服务了。
服务的工厂函数用来生成一个单例的对象或函数,这个对象或函数就是服务,它会存在于应用的整个生命周期内。当我们的AngularJS应用加载服务时,这个函数会被执行并返回一个单例的服务对象
同创建控制器的方法一样,服务的工厂函数既可以是一个函数也可以是一个数组:
// 用方括号声明工厂,声明了一个 服务: githubService angular.module('myApp.services', []) .factory('githubService', [function($http) { }]);
使用服务
可以在控制器、指令、过滤器或另外一个服务中通过依赖声明的方式来使用服务。
将服务的名字当作参数传递给控制器函数,可以将服务注入到控制器中。当服务成为了某个控制器的依赖,就可以在控制器中调用任何定义在这个服务对象上的方法。
angular.module('myApp', ['myApp.services']) .controller('ServiceController', function($scope, githubService) { // 我们可以调用对象的事件函数 $scope.events = githubService.events('auser'); });
githubService服务已经被注入到ServiceController中,可以像使用任何其他服务一样使用它.
//自己修改后的,原来的代码,用到$http 所以就没用。了解原理就行 angular.module('services', []) .factory('githubService', function($http) { // 返回带有一个events函数的服务对象 return { events: function(username) { return [{actor:"一号",name:"Linda"},{actor:"二号",name:"Joke"}, {actor:"三号",name:"elm"},{actor:"四号",name:"LIm"}]; } } }); var app = angular.module("app",['services']); app.controller('ServiceController', function($scope,githubService) { // 注意username属性的变化, 如果有变化就运行该函数 $scope.$watch('username',function(newname){ console.log($scope.count); // 从使用JSONP调用Github API的$http服务中返回promise $scope.events = githubService.events(newname); /*.success(function(data, status, headers) { // success函数在数据中封装响应 // 因此我们需要调用data.data来获取原始数据 $scope.events = data.data; })*/ }); });
<div ng-controller="ServiceController">
<label for="username">Type in a GitHub username</label>
<input type="text" ng-model="username" placeholder="Enter a GitHub username" /><br>
<ul>
<li ng-repeat="event in events">
{{ event.actor }} {{ event.name }}
</li>
</ul>
</div>
不推荐在控制器中使用$watch,这里只是为了方便演示。在实际生产中会将这个功能封装进一个指令,并在指令中设置$watch
内置服务$timeout来介绍一下这个延时。同注入githubService一样,需要将$timeout服务注入到控制器中:
app.controller('ServiceController', function($scope, $timeout, githubService) {
});
我觉得最好写成这样: 行内形式声明
app.controller('ServiceController',['$scope','$timeout',"githubService",function($scope, $timeout, githubService) { }]);
在自定义服务之前注入所有的AngularJS内置服务,这是约定俗成的规则。
$timeout服务会取消所有网络请求,并在输入字段的两次变化之间延时350 ms。换句话说,如果用户两次输入之间有350 ms的间隔,就推断用户已经完成了输入,然后开始向GitHub发送请求:
app.controller('ServiceController', function($scope, $timeout, githubService) { // 和上面的示例一样, 添加了$timeout服务 var timeout; $scope.$watch('username', function(newUserName) { if (newUserName) { // 如果在进度中有一个超时(timeout) if (timeout) $timeout.cancel(timeout); timeout = $timeout(function() { githubService.events(newUserName) .success(function(data, status) { $scope.events = data.data; }); }, 350); } }); });
在控制器之间共享数据,需要在服务中添加一个用来储存用户名的方法。服务在应用的生命周期内是单例模式的,因此可以将用户名安全地储存在其中。
angular.module('app',[]) .factory('githubService', function($http) { var githubUsername; var runUserRequest = function(path) { // 从使用JSONP的Github API的$http服务中返回promise return $http({ }); }; return { events: function() { return runUserRequest('events'); }, setUsername: function(username) { githubUsername = username; } }; }); //服务中有一个setUsername方法,用来保存当前的GitHub用户名了。 //可以来获取用户名 angular.module('myApp', ['myApp.services']) .controller('ServiceController', function($scope, githubService) { $scope.setUsername = githubService.setUsername; });
创建服务时的设置项
有5种方法用来创建服务:
factory()
service()
constant()
value()
provider()
factory() 是创建和配置服务的最快捷方式。 接受两个参数, name(字符串) 服务名, getFn(函数)或者 一个包含可被注入对象的数组 【一个特殊的数组,前面是字符串,最后一个是函数】, 创建服务实例时被调用。
angular.module('myApp') .factory('myService', function() { return { 'username': 'auser' }; }) angular.module('myApp') .factory('githubService', ['$http', function($http) { return { getUserEvents: function(username) { } }; }]);
服务是单例对象, getFn在应用的生命周期内只会被调用一次。
service()
一个支持构造函数的服务,它允许我们为服务对象注册一个构造函数
两个参数。 name(字符串) constructor(函数) 构造函数,我们调用它来实例化服务对象。
service()函数会在创建实例时通过new关键字来实例化服务对象。
var Person = function($http) { this.getName = function() { return $http({ method: 'GET', url: '/api/user'}); }; }; angular.service('personService', Person);
provider()
所有服务工厂都是由$provide服务创建的, $provide服务负责在运行时初始化这些提供者。提供者是一个具有$get()方法的对象, $injector通过调用$get方法创建服务实例。
$provider提供了数个不同的API用于创建服务,每个方法都有各自的特殊用途
所有创建服务的方法都构建在provider方法之上。 provider()方法负责在$providerCache中注册服务。
我们假定传入的函数就是$get()时, factory()函数就是用provider()方法注册服务的简略形式。
//两种方法的作用完全一样,并且会创建同一个服务 angular.module('myApp') .factory('myService', function() { return { 'username': 'auser' }; }) // 这与上面工厂的用法等价 .provider('myService', { $get: function() { return { 'username': 'auser' }; } });
是否可以一直使用.factory()方法来代替.provider()呢?
取决于是否需要用AngularJS的.config()函数来对.provider()方法返回的服务进行额外的扩展配置。config()方法可以被注入特殊的参数。
// 使用`.provider`注册该服务 angular.module('myApp', []) .provider('githubService', function($http) { // 默认的,私有状态 var githubUrl = 'https://github.com', setGithubUrl: function(url) { // 通过.config改变默认属性 if (url) { githubUrl = url } }, method: JSONP, // 如果需要,可以重写 $get: function($http) { self = this; return $http({ method: self.method, url: githubUrl + '/events'}); } });
通过使用.provider()方法,可以在多个应用使用同一个服务时获得更强的扩展性,特别是在不同应用或开源社区之间共享服务时。
如果希望在config()函数中可以对服务进行配置,必须用provider()来定义服务
provider() 两个参数。 name(字符串) name参数在providerCache中是注册的名字。name+Provider会成为服务的提供者。同时name也是服务实例的名字
aProvider(对象/函数/数组)
如果aProvider是函数,那么它会通过依赖注入被调用,并且负责通过$get方法返回一个对象
如果aProvider是数组,会被当做一个带有行内依赖注入声明的函数来处理。数组的最后一个元素应该是函数,可以返回一个带有$get方法的对象。
如果aProvider是对象,它应该带有$get方法。
provider()函数返回一个已经注册的提供者实例。
直接使用provider() API是最原始的创建服务的方法: // 在模块对象上直接创建provider的例子 angular.module('myApp', []) .provider('UserService', { favoriteColor: null, setFavoriteColor: function(newColor) { this.favoriteColor = newColor; }, // $get函数可以接受injectables $get: function($http) { return { 'name': 'Ari', getFavoriteColor: function() { return this.favoriteColor || 'unknown'; } }; } });
用这个方法创建服务,必须返回一个定义有$get()函数的对象,否则会导致错误。
可以通过注入器来实例化服务
var injector = angular.injector(['myApp']); // Invoke our service injector.invoke(['UserService', function(UserService) { // UserService returns // { // 'name': 'Ari', // getFavoriteColor: function() {} // } }]);
constant()
将一个已经存在的变量值注册为服务,并将其注入到应用的其他部分当中。constant()方法返回一个注册后的服务实例。
angular.module('myApp') .constant('apiKey','123123123');
这个常量服务可以像其他服务一样被注入到配置函数中:
angular.module('myApp') .controller('MyController', function($scope, apiKey) { // 可以像上面一样用apiKey作为常量 // 用123123123作为字符串的值 $scope.apiKey = apiKey; });
value()
如果服务的$get方法返回的是一个常量,通过value()函数方便地注册服务。
value()方法返回以name参数的值为名称的注册后的服务实例。
angular.module('myApp').value('apiKey','123123123');
何时使用value()和constant()
value()方法和constant()方法之间最主要的区别是, 常量可以注入到配置函数中,而值不行。
可以通过value()来注册服务对象或函数,用constant()来配置数据。
angular.module('myApp', []) .constant('apiKey', '123123123') .config(function(apiKey) { // 在这里apiKey将被赋值为123123123 // 就像上面设置的那样 }) .value('FBid','231231231') .config(function(FBid) { // 这将抛出一个错误,未知的provider: FBid // 因为在config函数内部无法访问这个值 });
decorator()
$provide服务提供了在服务实例创建时对其进行拦截的功能,可以对服务进行扩展,或者用另外的内容完全代替它。
装饰器是非常强大的,它不仅可以应用在我们自己的服务上,也可以对AngularJS的核心服务进行拦截、中断甚至替换功能的操作。事实上 AngularJS中很多功能的测试就是借助$provide.decorator()建立的。
例如,我们想给之前定义的githubService服务加入日志功能,可以借助decorator()函数方便地实现这个功能,而不需要对原始的服务进行修改
decorator() 接受两个参数。 name(字符串) 将要拦截的服务名称。 decoratorFn(函数) 在服务实例化时调用该函数,这个函数由injector.invoke调用,可以将服务注入这个函数中
$delegate是可以进行装饰的最原始的服务,为了装饰其他服务,需要将其注入进装饰器。
一个例子:
var githubDecorator = function($delegate,$log) { var events = function(path) { var startedAt = new Date(); var events = $delegate.events(path); // 事件是一个promise events.finally(function() { $log.info("Fetching events" +" took " +(new Date() - startedAt) + "ms");}); return events; }; return { events: events }; }; angular.module('myApp') .config(function($provide) { $provide.decorator('githubService',githubDecorator); });
XHR和服务器通信
使用$http
使用内置的$http服务直接同外部进行通信,$http服务简单的封装了原生的XMLHttpRequest对象。
$http只能接受一个参数,参数是一个对象,包含了用来生成HTTP请求的配置内容。 返回一个promise对象,具有success和error两个方法。
$http({method: 'GET',url: '/api/users.json'}) .success(function(data,status,headers,config) { // 当相应准备就绪时调用 }).error(function(data,status,headers,config) { // 当响应以错误状态返回时调用 });
$http方法返回一个promise对象,可以在响应返回时用then方法来处理回调。如果使用then方法,会得到一个特殊的参数,它代表了相应对象的成功或失败信息,还可以接受两个可选的函数作为参数。或者可以使用success和error回调代替
var promise = $http({method: 'GET',url: '/api/users.json'}); promise.then(function(resp){ // resp是一个响应对象 }, function(resp) { // 带有错误信息的resp }); // 或者使用success/error方法 promise.success(function(data, status, headers, config){ // 处理成功的响应 }); // 错误处理 promise.error(function(data, status, headers, config){ // 处理非成功的响应 }); //如果响应状态码在200和299之间,会认为响应是成功的, success回调会被调用,否则error回调会被调用。
then()方法与其他两种方法的主要区别是,它会接收到完整的响应对象,而success()和error()则会对响应对象进行析构。
//一个小例子: var app = angular.module("app",[]); app.controller("appcontrol",["$scope","$http",function($scope,$http){ $http({ method:"GET", url:"name.json" }).success(function(data,status,headers,config){ console.log("OK"); $scope.books = data; console.log($scope.books); }).error(function(data,status,headers,config){ console.log("error"); }); }])
<div ng-controller="appcontrol">
<ul ng-repeat="book in books">
<li>{{book.name}}=={{book.price}}</li>
</ul>
</div>
快捷方法:
// 快捷的GET请求
$http.get('/api/users.json'); 可以接受两个参数。 url(字符串) config(可选,对象) 这是一个可选的设置对象。
4. jsonp()
这是用来发送JSONP请求的快捷方式。 参数 同上
$http.jsonp("/api/users.json?callback=JSON_CALLBACK");
5. post()
这是用来发送POST请求的快捷方式。
post()函数可以接受三个参数。
url(字符串) 代表请求的目的地。 data(对象或字符串) 这个对象包含请求的数据。 config(可选,对象) 这是一个可选的设置对象
设置对象
当我们将$http当作函数来调用时,需要传入一个设置对象,用来说明如何构造XHR对象。
$http({ method: 'GET', url: '/api/users.json', params: { 'username': 'auser' } });
设置对象可以包含以下键。
1. method(字符串) ‘GET’、‘DELETE’、‘HEAD’、 ‘JSONP’、 ‘POST’、 ‘PUT’。
2. url(字符串)
3.params(字符串map或对象)这个键的值是一个字符串map或对象,会被转换成查询字符串追加在URL后面。如果值不是字符串,会被JSON序列化。
// 参数会转化为?name=ari的形式
$http({ params: {'name': 'ari'} })
4. data(字符串或对象)
这个对象中包含了将会被当作消息体发送给服务器的数据。通常在发送POST请求时使用。
var blob = new Blob(['Hello World'], {type: 'text/plain'}); $http({ method: 'POST', url: '/', data: blob });
响应对象
AngularJS传递给then()方法的响应对象包含四个属性。
data(字符串或对象)
这个数据代表转换过后的响应体(如果定义了转换的话)。
status(数值型)
响应的HTTP状态码。
headers(函数)
这个函数是头信息的getter函数,可以接受一个参数,用来获取对应名字的值。例如,用如
下代码获取X-Auth-ID的值:
$http({
method: 'GET',
url: '/api/users.json'
}).then (resp) {
// 读取X-Auth-ID
resp.headers('X-Auth-ID');
});
config(对象)
这个对象是用来生成原始请求的完整设置对象。
statusText(字符串)
这个字符串是响应的HTTP状态文本。
缓存 HTTP 请求
默认情况下, $http服务不会对请求进行本地缓存。在发送单独的请求时,我们可以通过向$http请求传入一个布尔值或者一个缓存实例来启用缓存。
$http.get('/api/users.json',{ cache: true }) .success(function(data) {}) .error(function(data) {});
第一次发送请求时, $http服务会向/api/users.json发送一个GET请求。第二次发送同一个GET请求时, $http服务会从缓存中取回请求的结果,而不会真的发送一个HTTP GET请求。
如果想要对AngularJS使用的缓存进行更多的自定义控制,可以向请求传入一个自定义的缓存实例代替true。
每次发送请求时都传入一个自定义缓存是很麻烦的事情(即使是在服务中)。可以通过应用的.config()函数给所有$http请求设置一个默认的缓存:
angular.module('myApp', []) .config(function($httpProvider, $cacheFactory) { $httpProvider.defaults.cache = $cacheFactory('lru', { capacity: 20 }); }); //$cacheFactory('lru', {capacity: 20}); 就是自定义的缓存实例
拦截器
果我们想要为请求添加全局功能,例如身份验证、错误处理等,在请求发送给服务器之前或者从服务器返回时对其进行拦截,是比较好的实现手段。
一共有四种拦截器,两种成功拦截器,两种失败拦截器。
拦截器的核心是服务工厂,通过向$httpProvider.interceptors数组中添加服务工厂,在$httpProvider中进行注册。
request $http设置对象来对请求拦截器进行调用。 返回一个更新过的设置对象,或者一个可以返回新的设置对象的promise。
response $http设置对象来对响应拦截器进行调用 返回一个更新过的响应,或者一个可以返回新响应的promise。
requestError
responseError
调用模块的.factory()方法来创建拦截器,可以在服务中添加一种或多种拦截器:
angular.module('myApp', []) .factory('myInterceptor', function($q) { var interceptor = { 'request': function(config) { // 成功的请求方法 return config; // 或者 $q.when(config); }, 'response': function(response) { // 响应成功 return response; // 或者 $q.when(config); }, 'requestError': function(rejection) { // 请求发生了错误,如果能从错误中恢复,可以返回一个新的请求或promise return response; // 或新的promise // 或者,可以通过返回一个rejection来阻止下一步 // return $q.reject(rejection); }, 'responseError': function(rejection) { // 请求发生了错误,如果能从错误中恢复,可以返回一个新的响应或promise return rejection; // 或新的promise // 或者,可以通过返回一个rejection来阻止下一步 // return $q.reject(rejection); } }; return interceptor; }); //我们需要使用$httpProvider在.config()函数中注册拦截器: angular.module('myApp', []) .config(function($httpProvider) { $httpProvider.interceptors.push('myInterceptor'); });
设置$httpProvider
使用.config()可以向所有请求中添加特定的HTTP头
默认的请求头保存在$httpProvider.defaults.headers.common对象中。默认的头如下所示:
Accept: application/json, text/plain, */*
通过.config()函数可以对这些头进行修改或扩充,如下所示:
angular.module('myApp', []) .config(function($httpProvider) { $httpProvider.defaults.headers .common['X-Requested-By'] = 'MyAngularApp'; });
Restangular是一个专门用来从外部读取数据的AngularJS服务。
尽管$http和$resource是AngularJS的内置服务,但这两个服务在某些方面的功能是有限的。 Restangular通过完全不同的途径实现了XHR通信,并提供了良好的使用体验
1. promise
Restangular支持promise模式的异步调用,使用起来更符合AngularJS的习惯。可以像使用原
始的$http方法一样对响应进行链式操作。
2. promise展开
也可以像使用$resource服务一样使用Restangular,通过很简单的方式同时操作promise和对象。
3. 清晰明了
Restangular库几乎没有复杂或神奇的东西,无需通过猜测或研究文档就可以知道它是如何工
作的。
4. 全HTTP方法支持
Restangular支持所有的HTTP方法。
5. 忘记URL
$resource要求明确的指定想要拉取数据的URL, Restangular并不需要事先知道URL或提前
指定它们(除基础URL外)。
6. 资源嵌套
Restangular可以直接处理嵌套的资源,无需创建新的Restangular实例。
7. 一个实例
同$resource不同,使用过程中仅需要创建一个Restangular资源对象的实例。
Restangular依赖Lo-Dash或Underscore,因此为了确保Restangular可以正常运行,需要引入这两个库中的一个。
<script type="text/javascript" src="/js/vendor/lodash/dist/lodash.min.js"></script>
<script type="test/javascript" src="js/vendor/restangular.min.js"></script>
同其他的AngularJS库一样,我们需要将restangular资源当作依赖加载进应用模块对象。
angular.module('myApp', ['restangular']);
完成后,就可以将Restangular服务注入到AngularJS对象中:
angular.module('myApp', [])
.factory('UserService', ['Restangular', function(Restangular) {
// 现在我们已经在UserService中访问了Restangular
}])
Restangular有两种方式创建拉取数据的对象。可以为拉取数据的对象设置基础路由:
var User = Restangular.all('users'); 会让所有的HTTP请求将/users路径作为根路径来拉取数据。
调用上述对象的getList()方法会从/users拉取数据: var allUsers = User.getList(); // GET /users
也可通过单个对象来发送嵌套的请求,用唯一的ID来代替路由发送请求: var oneUser = Restangular.one('users', 'abc123');
调用oneUser上的get()时向/users/abc123发送请求。
oneUser.get().then(function(user) {
// GET /users/abc123/inboxes
user.getList('inboxes');
});
Restangular非常聪明,知道如何根据在Restangular源对象上调用的方法来构造URL。但设置拉取数据的URL是很方便的,特别是当后端不支持纯粹的RESTful API时。
var messages = Restangular.all('messages');
通过这个对象,可以使用getList()来获取所有信息。 getList()方法返回了一个集合,其中包含了可以用来操作特定集合的方法
Restangular返回的是增强过的promise对象,因此除了可以调用then方法,还可以调用一些特殊的方法,比如$object。 $object会立即返回一个空数组(或对象),在服务器返回信息后,数组会被用新的数据填充。
// POST到/messages var newMessage = { body: 'Hello world' }; var messages = Restangular.all('messages'); // 然后在promise中调用 messages.post(newMessage).then(function(newMsg){ // 首先将消息设置成空数组 // 然后一旦getList是完整的就填充它 $scope.messages = messages.getList().$object; }, function(errorReason) // 出现了一个错误 });
使用remove()方法发送一个DELETE HTTP请求,调用集合中一个对象(或元素)的remove()方法来发送删除请求。
var message = messages.get(123);
message.remove(); // 发送DELETE HTTP请求
更新和储存对象 由HTTP PUT方法完成。 Restangular 通过put()方法来支持这个功能。通过put()方法来支持这个功能。
更新一个对象,首先查询这个对象,然后在实例中设置新的属性值,再调用对象的put()方法将更新保存到后端。 在更新对象时使用Restangular.copy()是一个比较好的实践。
嵌套资源是指包含在其他组件内部的组件。
//一个 作者所写的所有的书籍, 作家: abc123 var author = Restangular.one('authors', 'abc123'); // 构建一个GET到/authors/abc123/books的请求 var books = author.getList('books');
也可以在服务器返回的对象上调用:
Restangular.one('authors', 'abc123').then(function(author) { $scope.author = author; }); // 构建一个GET到/authors/abc123/authors的请求 // 使用$scope.author,它是从服务器返回的真实对象 $scope.author.getList('books');
Restangular支持所有的HTTP方法。它支持GET、 PUT、 POST、 DELETE、 HEAD、 TRACE、OPTIONS和PATCH。
author.get(); // GET/authors/abc123 author.getList('books'); // GET/authors/abc123/books author.put(); // PUT/authors/abc123 author.post(); // POST/authors/abc123 author.remove(); // DELETE/authors/abc123 author.head(); // HEAD/authors/abc123 author.trace(); // TRACE/authors/abc123 author.options(); // OPTIONS/authors/abc123 author.patch(); // PATCH/author/abc123
下面是一些例子。
- GET /zoos: 列出所有动物园
- POST /zoos: 新建一个动物园
- GET /zoos/ID: 获取某个指定动物园的信息
- PUT /zoos/ID: 更新某个指定动物园的信息(提供该动物园的全部信息)
- PATCH /zoos/ID: 更新某个指定动物园的信息(提供该动物园的部分信息)
- DELETE /zoos/ID: 删除某个动物园
- GET /zoos/ID/animals: 列出某个指定动物园的所有动物
- DELETE /zoos/ID/animals/ID: 删除某个指定动物园的指定动物
设置 Restangular
Restangular具有高度的可定制性,可以根据应用的需要进行相应的设置。每个属性都有默认值,所以我们也无需在不必要的情况下对其进行设置。
将RestangularProvider注入到config()函数中,或者将Restangular注入到一个run()函数中,
如果设置Restangular时需要用到其他服务,那么就在run()方法中设置,否则就在config()中进行设置。
通过setBaseUrl()方法给所有后端 API 请求设置 baseUrl
angular.module('myApp', ['restangular']) .config(function(RestangularProvider) { RestangularProvider.setBaseUrl('/api/v1'); });
2. 添加元素转换
使用elementTransformers可以在Restangular对象被加载后为其添加自定义方法。
例如,如果我们只想更新authors资源,可以用如下方法:
angular.module('myApp', ['restangular']) .config(function(RestangularProvider) { // 3个参数: // route RestangularProvider.extendModel('authors', function(element) { element.getFullName = function() { return element.name + ' ' + element.lastName; }; return element; }); });
设置responseInterceptors
Restangular可以设置响应拦截器。responseInterceptors在需要对服务器返回的响应进行转换时非常有用。
responseInterceptors在每个响应从服务器返回时被调用。调用时会传入以下参数。
data:从服务器取回的数据。
operation:使用的HTTP方法。
what:所请求的数据模型。
url:请求的相对URL。
response:完整的服务器响应,包括响应头。
deferred:请求的promise对象。
会使getList()返回一个带有元信息的数组,,向/customers发送GET请求会返回一个像{customers: []}这样的数组。
angular.module('myApp', ['restangular']) .config(function(RestangularProvider) { RestangularProvider.setResponseInterceptor(function(data, operation, what) { if (operation == 'getList') { var list = data[what]; list.metadata = data.metadata; return list; } return data; }); });
使用requestInterceptors
在将数据实际发送给服务器之前对其进行操作。
自定义Restangular服务
将Restangular服务注入到工厂函数中,就可以方便地对Restangular进行封装。在工厂函数内部,使用withConfig()函数来创建自定义设置
angular.module('myApp', ['restangular']) .factory('MessageService', ['Restangular', function(Restangular) { var restAngular = Restangular.withConfig(function(Configurer) { Configurer.setBaseUrl('/api/v2/messages'); }); var _messageService = restAngular.all('messages'); return { getMessages: function() { return _messageService.getList(); } }; }]);
XHR实践
跨域和同源策略:同源策略允许页面从同一个站点加载和执行特定的脚本。站外其他来源的脚本同页面的交互则被严格限制。
跨域资源共享(Cross Origin Resource Sharing, CORS)是一个解决跨域问题的好方法,从而可以使用XHR从不同的源加载数据和资源。
JSONP是一种可以绕过浏览器的安全限制,从不同的域请求数据的方法。使用JSONP需要服务器端提供必要的支持。
JSONP的原理是通过<script>标签发起一个GET请求来取代XHR请求。 JSONP生成一个<script>标签并插到DOM中,然后浏览器会接管并向src属性所指向的地址发送请求。
当服务器返回请求时,响应结果会被包装成一个JavaScript函数,并由该请求所对应的回调函数调用。
$http服务中提供了一个JSONP辅助函数。
$http .jsonp("https://api.github.com?callback=JSON_CALLBACK") .success(function(data) { // 数据 });
当请求被发送时, AngularJS会在DOM中生成一个如下所示的<script>标签:
<script src="https://api.github.com?callback=angular.callbacks._0" type="text/javascript"></script> //JSON_CALLBACK被替换成了一个特地为此请求生成的自定义函数。
当支持 JSOPN的服务器返回数据时,数据会被包装在由 AngularJS生成的具名函数angular.callbacks._0中。
使用JSONP需要意识到潜在的安全风险。首先,服务器会完全开放,允许后端服务调用应用中的任何JavaScript。
不受我们控制的外部站点(或者蓄意攻击者)可以随时更改脚本,使我们的整个站点变得脆弱。服务器或中间人有可能会将额外的JavaScript逻辑返回给页面,从而将用户的隐私数据暴露出来。
请求是由<script>标签发送的,所以只能通过JSONP发送GET请求。并且脚本的异常也很难处理。使用JSONP一定要谨慎,同时只跟信任并可以控制的服务器进行通信
使用 CORS
CORS规范简单地扩展了标准的XHR对象,以允许JavaScript发送跨域的XHR请求。它会通过预检查(preflight)来确认是否有权限向目标服务器发送请求。
告诉AngularJS我们正在使用CORS。使用config()方法在应用模块上设置两个参数以达到此目的,
首先,告诉AngularJS使用XDomain,并从所有的请求中把X-Request-With头移除掉。X-Request-With头默认就是移除掉的,但是再次确认没有坏处。
angular.module('myApp', []) .config(function($httpProvider) { $httpProvider.defaults.useXDomain = true; delete $httpProvider.defaults.headers .common['X-Requested-With']; });
服务器端CORS支持,确保服务器支持CORS是很重要的
CORS请求分为简单和非简单两种类型。
CORS并不是一个安全机制,只是现代浏览器实现的一个标准。在应用中设置安全策略依然是我们的责任。
使用 XML
假如服务器返回的是XML而非JSON格式的数据,需要将其转换成JavaScript对象。
以X2JS库为例,这是一个非常好用的开源库,将XML格式转换成JavaScript对象
首先引入:
<script type="text/javascript" src="https://x2js.googlecode.com/hg/xml2json.js"></script>
创建一个工厂服务,功能就是在DOM中解析XML
angular.factory('xmlParser', function() { var x2js = new X2JS(); return { xml2json: x2js.xml2json, json2xml: x2js.json2xml_str }; }); //借助这个轻量的解析服务,可以将$http请求返回的XML解析成JSON格式,如下所示: angular.factory('Data', [$http, 'xmlParser', function($http, xmlParser) { $http.get('/api/msgs.xml', { transformResponse: function(data) { return xmlParser.xml2json(data); } }); });
使用 AngularJS 进行身份验证
纯客户端身份验证
通过令牌授权来实现客户端身份验证,服务器需要做的是给客户端应用提供授权令牌。
令牌本身是一个由服务器端生成的随机字符串,由数字和字母组成,它与特定的用户会话相关联。 uuid库是用来生成令牌的好选择。
当用户登录到我们的站点后,服务器会生成一个随机的令牌,并将用户会话同令牌之间建立关联,用户无需将ID或其他身份验证信息发送给服务器
客户端发送的每个请求都应该包含此令牌,这样服务器才能根据令牌来对请求的发送者进行身份验证。
服务器端则无论请求是否合法,都会将对应事件的状态码返回给客户端,这样客户端才能做出响应。
下面的表格中是一些常用的状态码:
状 态 码 含 义
200 | 一切正常 |
401 | 未授权的请求 |
403 | 禁止的请求 |
404 | 页面找不到 |
500 | 服务器错误 |
当客户端收到这些状态码时会做出相应的响应。
数据流程如下:
(1) 一个未经过身份验证的用户浏览了我们的站点;
(2) 用户试图访问一个受保护的资源,被重定向到登录页面,或者用户手动访问了登录页面;
(3) 用户输入了他的登录ID(用户名或电子邮箱)以及密码,接着AngularJS应用通过POST请求将用户的信息发送给服务端;
(4) 服务端对ID和密码进行校验,检查它们是否匹配;
(5) 如果ID和密码匹配,服务端生成一个唯一的令牌,并将其同一个状态码为200的响应一起返回。如果ID和密码不匹配,服务器返回一个状态码为401的响应
有下面几种方法可以将路由定义为公共或非公共。
1. 保护API访问的资源
对一个会发送受保护的API请求的路由进行保护,但又希望可以正常加载页面,可以简单地通过$http拦截器来实现。
创建一个$http拦截器并能够处理未通过身份验证的API请求,首先要创建一个拦截器
我们在应用的.config()代码块内设置$http响应拦截器,并将$httpProvider注入其中。
angular.module("app",[]) .config(function($httpProvider){ //这里构造拦截器,会处理所有请求的响应 以及 响应错误。 var interceptor = function($q,$rootScope,Auth){ return { "response":function(resp){ if(resp.config.url=="/api/login") { //假设后台返回的数据格式 如下:{token:"AUTH_TOKEN"} Auth.setToken(resp.data.token); } return resp; }, "responseError":function(rejection){ //错误处理, switch(rejection.status){ case 401: if(rejection.config.url !="api/login"){ //如果当前不是登陆页面 $rootScope.$broadcast('auth:loginRequired'); } break; case 403: $rootScope.$broadcast('auth:forbidden'); break; case 404: $rootScope.$broadcast('page:notFound'); break; case 500: $rootScope.$broadcast('server:error'); break; } return $q.reject(rejection); } } } // 将拦截器和$http的request/response链整合在一起 $httpProvider.interceptors.push(interceptor); });
如果我们希望始终对某些路径进行保护,或者请求的API不会对路由进行保护,那就需要监视路由的变化,以确保访问受保护路由的用户是处于登录状态的。
为了监视路由变化,需要为$routeChangeStart事件设置一个事件监听器。这个事件会在路由属性开始resolve时触发,
通过监听器对事件进行监听,并检查路由,看它是否定义为可被当前用户访问。
先要定义应用的访问规则。可以通过在应用中设置常量,然后在每个路由中通过对比这些常量来判断用户是否具有访问权限。
angular.module('myApp', ['ngRoute']) .constant('ACCESS_LEVELS', { pub: 1, user: 2 });
把ACCESS_LEVELS设置为常量,可以将它注入到.confgi()和.run()代码块中,并在整个应用范围内使用
//使用这些常量来为每个路由都定义访问级别: angular.module('myApp', ['ngRoute']) .config(function($routeProvider, ACCESS_LEVELS) { $routeProvider .when('/', { controller: 'MainController', templateUrl: 'views/main.html', access_level: ACCESS_LEVELS.pub }) .when('/account', { controller: 'AccountController', templateUrl: 'views/account.html', access_level: ACCESS_LEVELS.user }) .otherwise({ redirectTo: '/' }); });
为了验证用户的身份,创建一个服务来对已经存在的用户进行监视。同时让服务能够访问浏览器的cookie,这样当用户重新登录时,只要会话有效就无需再次进行身份验证。
angular.module('myApp.services', []) .factory('Auth', function($cookieStore,ACCESS_LEVELS) { var _user = $cookieStore.get('user'); var setUser = function(user) { if (!user.role || user.role < 0) { user.role = ACCESS_LEVELS.pub; } _user = user; $cookieStore.put('user', _user); }; return { isAuthorized: function(lvl) { return _user.role >= lvl; }, setUser: setUser, isLoggedIn: function() { return _user ? true : false; }, getUser: function() { return _user; }, getId: function() { return _user ? _user._id : null; }, getToken: function() { return _user ? _user.token : ''; }, logout: function() { $cookieStore.remove('user'); _user = null; } } }; });
当用户已经通过身份验证并登录后,可以在$routeChangeStart事件中对其有效性进行检查。
angular.module('myApp', []) .run(function($rootScope, $location, Auth) { // 给$routeChangeStart设置监听 $rootScope.$on('$routeChangeStart', function(evt, next, curr) { if (!Auth.isAuthorized(next.$$route.access_level)) { if (Auth.isLoggedIn()) { // 用户登录了,但没有访问当前视图的权限 $location.path('/'); } else { $location.path('/login'); } } }); });
如果提供的令牌是合法的,且与一个合法用户是关联的状态,那服务器就会认为用户的身份是合法且安全的。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步