angular的uiRouter服务学习(1)
angular有内置的路由服务$route:angular -- $route API翻译
使用$route可以帮助实现路由的切换,视图的改变,但是这个内置的$route只包含了基本的功能,在很多场合下是不够用的.所以,需要学习使用uiRouter.
首先,在页面中链入'angular-ui-router.min.js',然后在模块中写入依赖:
var myapp = angular.module('myApp',['ui.router']);
然后就可以使用一个叫做$state的服务,使用$stateProvider来配置这个服务.
$stateProvider和angualr内置的$routeProvider的用法类似,但是它是通过'状态'来管理路由的.
- 在整个应用的界面和导航中,状态对应了页面中的一个位置(也就是ui-view)
- 状态通过controller,template,view等属性,描述了它对应位置的视图展示和行为.
- 状态之间通常有一些共同点,把这些共同点从模型中分解出来的最好办法就是通过状态继承. 比如父/子状态(又名状态嵌套)
下面例举一个最简单的状态:
<body ng-controller="MainCtrl">
<section ui-view></section>
</body>
$stateProvider.state('contacts', {
template: '<h1>My Contacts</h1>'
})
名为'contacts'的状态,对应了ui中的'ui-view'指令元素.当contacts状态被激活,ui-view元素就会被template填充.
模板的插入位置:
当一个状态被激活,它的模板会被自动填充到父状态模板里的ui-view元素里.如果这个状态是个顶层状态-比如上面例子中的'contacts'状态,它没有父状态.那么,它的父状态模板就是整个html.
另外,ui-view元素可以拥有原始内容,用于当状态还没有被激活时展示,当状态被激活后,原始内容会被替换掉:
<body> <ui-view> <i>Some content will load here!</i> </ui-view> </body>
现在,'contacts'状态不会被激活,下面来看看如何激活它:
激活一个状态:
- 调用 $state.go().具体用法以后再讲.
- 点击一个带有ui-sref属性的a链接.ui-sref属性值就是状态值.具体用法以后再讲.
- 状态里定义对应url,当页面的url改变成对应状态的url时,就激活这个状态.具体用法以后再讲.
状态的模板:
有几种方法可以定义状态对应的视图模板:
1.定义template属性,属性值为字符串html:
$stateProvider.state('contacts', { template: '<h1>My Contacts</h1>' })
2.定义templateUrl属性,属性值一个函数,函数返回值为模板文件对应的url路径:
函数中可以注入$stateParams.$stateParams是url参数组成的键值对对象. 比如这里url里的name就是一个参数,那么,$stateParams就是{name:''}
$stateProvider.state('contacts',{ url:'/contacts/:name', templateUrl: function($stateParams){ return 'partials/contacts.' + $stateParams.name + '.html' } })
3.定义templateProvider属性.属性值是一个函数,函数的返回值为字符串html:
函数中同样可以注入$stateParams
$stateProvider.state('contacts',{ url:'/contacts/:name', templateProvider: function($stateParams){ return '<h1>'+$stateParams.name+'</h1>' } })
状态的控制器:
可以为每个视图模板分配一个控制器. 注意:如果模板没有被定义,那么控制器不会被实例化.
有以下几种方式可以定义控制器:
1.定义controller属性,属性值为模块下定义好的控制器,
在myapp模块下定义了'contact'控制器,然后controller属性就可以直接定义属性值为'contact'
myapp.config(function($stateProvider){ $stateProvider.state('contacts',{ url:'/contacts/:name', controller:'contact', templateProvider: function($stateParams){ return '<h1>'+'{{text}},'+$stateParams.name+'</h1>' } }) }); myapp.controller('contact',function($scope){ $scope.text='hi' });
2.定义controller属性,属性值就是控制器函数
myapp.config(function($stateProvider){ $stateProvider.state('contacts',{ url:'/contacts/:name', controller:function($scope){ $scope.text='hi' }, templateProvider: function($stateParams){ return '<h1>'+'{{text}},'+$stateParams.name+'</h1>' } }) });
3.定义controller属性,属性值为模块下定义好的控制器+'as con',并且定义controllerAs属性为'con',然后在作用域中使用con
这里的con只是我随便取的名字...不是定死的...
myapp.config(function($stateProvider){ $stateProvider.state('contacts',{ url:'/contacts/:name', controller:'contact as con', templateProvider: function($stateParams){ return '<h1>'+'{{con.text}},'+$stateParams.name+'</h1>' }, controllerAs:'con' }) }); myapp.controller('contact',function(){ this.text='hi' });
4.定义controller属性,属性值就是控制器函数,并且定义controllerAs属性为'con',然后在作用域中使用con
这里的con只是我随便取的名字...不是定死的...
myapp.config(function($stateProvider){ $stateProvider.state('contacts',{ url:'/contacts/:name', controller:function(){ this.text='hi' }, templateProvider: function($stateParams){ return '<h1>'+'{{con.text}},'+$stateParams.name+'</h1>' }, controllerAs:'con' }) });
控制器会根据需要,在对应的作用域被创建的时候实例化. 比如: 当url被改变到和状态匹配的url时,$stateProvider会把对应的模板加载到视图里,然后把控制器绑定到模板的作用域下.
状态的resolve:
resolve属性非常重要,它是一个map对象(也就是json对象),它为状态的控制器提供了所需的依赖.这些依赖可以给状态对应的控制器提供所需要的内容或数据.如果resolve属性值中有promise对象,那么它会在控制器被实例化、$stateChangeSuccess事件触发之前被解析并且转换成值.
resolve的属性和属性值应该是这样的:
- 属性名: 这个名字将会被作为依赖,注入到controller里.
- 属性值: 字符串|函数
- 字符串: 当前模型下既有的服务名
- 函数: 函数的返回值会作为依赖,可以被注入到控制器中.如果函数的返回值是一个promise对象,那么它会在控制器被实例化、$stateChangeSuccess事件触发之前被解析并且转换成值.这个值会被作为依赖注入.
下面这段代码介绍了常见的resolve的六种类型:
/*resolve*/ myapp.config(function($stateProvider){ $stateProvider.state('contacts',{ url:'/contacts/:name', resolve:{ //字符串格式:使用一个既有的服务 first:'aService', //函数:函数的返回值就是将被注入的服务 second:function(){ return {data:'second的data'} }, //函数:在函数中注入既有的服务 third:function(anotherService,$stateParams){ var data = anotherService.getName($stateParams.name); return {data:data} }, //函数:返回一个promise对象,最终得到的将是resolve里的内容 fourth:function($q,$timeout){ var defer = $q.defer(); $timeout(function(){ defer.resolve({data:'我是fourth的data'}); //注意,如果一个state的resolve里的某个promise被拒绝了,那这个state直接无法继续下去了. //defer.reject({data:'我是fourth的data'}) },2000); return defer.promise; }, //函数:返回$http返回的promise返回的promise,最终得到的是.then里面return的内容 fifth:function($http){ return $http({ method:'GET', url:'/contacts/name' }).then(function(res){ return {data:res.data} },function(){ }) }, //函数:返回$http返回的promise,最终得到的就是后台返回值. sixth:function($http){ return $http({ method:'GET', url:'/contacts/name' }) } }, templateUrl:function($stateParams){ return 'partials/contacts.' + $stateParams.name + '.html' }, controller:'ctrl' }) }); myapp.factory('aService',function(){ return { getName:function(){ alert('我是aService服务的getName方法') }, data:'first的data' } }); myapp.factory('anotherService',function(){ return { getName:function(data){ return data.toUpperCase() } } }); myapp.controller('ctrl',function($scope,first,second,third,fourth,fifth,sixth){ first.getName(); $scope.data1 = first.data; $scope.data2 = second.data; $scope.data3 = third.data; $scope.data4 = fourth.data; $scope.data5 = fifth.data; $scope.data6 = sixth.data; });
1.first: 一个既有的服务名.
注入'first'依赖就相当于注入了'aService'服务.
{ getName:function(){ alert('我是aService服务的getName方法') }, data:'first的data' }
2.second: 一个函数
注入'second'依赖,得到的是这个函数的返回值
{data:'second的data'}
3.third: 一个函数,函数中可以注入既有的服务
其实这种情况和2一样,只是说,函数里可以注入依赖
{data:'BUNNY'}
4.fourth: 一个promise对象
控制器会等到promise被解析以后再实例化,而注入的依赖,不是promise本身,而是promise被解析的值,需要注入的是,如果promise不是被resolve,而是被reject,那么js会被中断,控制器不会被实例化.状态切换也失败了.
{data:'我是fourth的data'}
5.fifth: 一个promise对象返回的promise对象
其实这种情况和4一样,promise.then返回的promise对象,会被.then()函数里的返回值解析.这适用于对返回值做一些处理后再返回.
(这里后台返回 ['bunny','cat','dog'] )
{data:['bunny','cat','dog']}
6.sixth: 返回一个$http返回的promise对象
其实这种情况也和4一样.这个promise会被返回值解析.所以最后得到的就是返回值了.
{data:['bunny','cat','dog']}
给状态对象添加自定义的数据:
$stateProvider.state('name',{})中的.state第二个参数对象可以添加自定义的属性和值,为了避免冲突,一般使用data属性来为它添加自定义属性.
自定义的属性可以通过$state.current.data来访问到.
myapp.config(function($stateProvider){ $stateProvider.state('contacts',{ url:'/contacts/:name', templateUrl:function($stateParams){ return 'partials/contacts.'+$stateParams.name+'.html' }, data:{ stateData1:111, stateData2:222 }, controller:function($scope,$state){ $scope.data7 = $state.current.data.stateData1 + $state.current.data.stateData2 } }) });
状态的onEnter和onExit回调:
状态的onEnter属性和onExit属性可以用来定义进入状态和退出状态所执行的回调:
注意,回调函数中可以注入resolve里定义的依赖,比如下面的'title':
$stateProvider.state('contacts',{ url:'/contacts/:name', templateUrl:function($stateParams){ return 'partials/contacts.'+$stateParams.name+'.html' }, resolve:{ title:function(){ return 'contacts' } }, onEnter: function(title){ console.log('进入'+title+'状态啦') }, onExit: function(title){ console.log('退出'+title+'状态啦') } })
状态改变事件:
这些事件都是在$rootScope上触发的.
- $stateChangeStart: 当状态开始改变时触发, 接受5个参数:
- event: 事件对象,使用event.preventDefault()可以阻止状态发生改变.
- toState: toState是定义.state时传入的第二个参数对象
- toParams: toParams就是$stateParams
- fromState: fromState是上一个状态(就是离开的状态).state时传入的第二个参数对象
- fromParams: fromParams是上一个状态(就是离开的状态)的$stateParams
- $stateChangeSuccess: 当状态改变结束时触发,可以接受5个参数,5个参数同'$stateChangeStart'的5个参数
- $stateNotFound: 当状态没有找到时触发,接受4个参数:
- event: 事件对象,使用event.preventDefault()可以阻止js继续执行,否则会报错,卡住.
- unfoundState: 一个对象,这个对象有三个属性:
- to: 前往的状态名(也就是没有找到的这个状态名)
- toParams: 前往的状态的参数(在使用ui-sref或者$state.go()的时候可以传入)
- options: 使用$state.go()的时候传入的第三个参数
- to: 前往的状态名(也就是没有找到的这个状态名)
- fromState: 同上
- fromParams: 同上
- event: 事件对象,使用event.preventDefault()可以阻止js继续执行,否则会报错,卡住.
- $stateChangeError: 当状态改变失败时触发,需要注意,如果在状态的resolve过程中遇到了问题(比如js错误,服务找不到,请求得不到响应等),这些错误不会像传统的那样被抛出,而是会在$stateChangeError里被捕获.它可以接受6个参数
- event: 事件对象
- toState: 同上
- toParams: 同上
- fromState: 同上
- fromParams: 同上
- error: 一个包含了错误信息的对象
<div> <a href="contacts/bunny">查看视图</a> <a href="contacts/exit">离开</a> <a ui-sref="lalala({a:1,b:2})">无</a> <section ui-view loading>点击链接后内容会被加载在这里</section> </div>
myapp.directive('loading',function($rootScope){ return { restrict:'EA', link:function(scope,iEle,iAttrs,ctrl){ console.log(scope===$rootScope); scope.$on('$stateChangeStart',function(event,toState,toParams,fromState,fromParams){ console.log('状态开始改变'); /*toState是定义.state时传入的第二个参数对象*/ //console.log(toState); /*toParams就是$stateParams*/ //console.log(toParams); /*fromState是上一个状态.state时传入的第二个参数对象*/ //console.log(fromState); /*fromParams是上一个状态的$stateParams*/ //console.log(fromParams); }); scope.$on('$stateChangeSuccess',function(event,toState,toParams,fromState,fromParams){ console.log('状态改变结束'); /*参数全部同上*/ }); scope.$on('$stateNotFound',function(event,unfoundState,fromState,fromParams){ console.log('没有找到对应的状态'); /*unfoundState包含了三个属性:*/ /*1.to:前往的状态名(也就是没有找到的这个状态名) * 2.toParams:前往的状态的参数(在使用ui-sref或者$state.go()的时候可以传入,这个例子里就是{a:1,b:2}) * 3.options:使用$state.go()的时候传入的第三个参数. * */ /*最后两个参数同上*/ console.log(unfoundState); //如果不写这句,那么接下来就会报错,卡住js进程了. event.preventDefault() }); scope.$on('$stateChangeError',function(event, toState, toParams, fromState, fromParams, error){ console.log('切换状态出错'); /*error是一个包含了错误信息的对象*/ console.log(error); }); scope.$on('$viewContentLoading',function(event,viewConfig){ console.log('视图开始加载'); }); scope.$on('$viewContentLoaded',function(event){ console.log('视图渲染完毕') }) } } });
*注意,官网里是说事件都在$rootScope上触发,但是这里直接在指令元素的scope上也能触发.
视图加载事件:
- $viewContentLoading: 当视图开始渲染的时候,$rootScope传播这个事件. 它接受2个参数
- event: 事件对象
- viewConfig: 包含一个属性:targetView
- $viewContentLoaded: 当视图渲染完毕的时候,视图所在的scope传播这个事件.
scope.$on('$viewContentLoading',function(event,viewConfig){ console.log('视图开始加载'); }); scope.$on('$viewContentLoaded',function(event){ console.log('视图渲染完毕') })
事件触发执行顺序:
下面来理一下一个状态被激活的过程是怎样的:
1. 触发$stateChangeStart事件,如果使用event.preventDefault(),会阻止状态改变.
如果没有找到对应状态,会触发$stateNotFound事件,然后中断.
2. 触发$viewContentLoading事件.
3. 如果在切换状态的过程中出错(比如resolve出错),触发$stateChangeError事件,无出错跳过此步.
4. 触发上一个状态(若有)的onExit回调事件
5. 触发当前状态的onEnter回调事件
6. 触发$stateChangeSuccess事件
7. 触发$viewContentLoaded事件
完整代码: https://github.com/OOP-Code-Bunny/angular/tree/master/uiRouter
参考网站: https://github.com/angular-ui/ui-router/wiki