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