angular路由 路由 (route) ,幾乎全部的 MVC(VM) 框架都應該具備的特性,由於它是前端構建單頁面應用 (SPA) 必不可少的組成部分。 那麼,對於 angular 而言,它天然也有 內置 的路由模塊:叫作 ngRoute 。 不過,你們不多用它,由於它的功能太有限,每每不能知足開發需求!! 因而,一個基於 ngRoute 開發的 第三方路由模塊 ,叫作 ui.router ,受到了你們的「追捧」。 ngRoute vs ui.router 首先,不管是使用哪一種路由,做爲框架額外的附加功能,它們都將以 模塊依賴 的形式被引入,簡而言之就是:在引入路由 源文件 以後,你的代碼應該這樣寫(以 ui.router 爲例): angular.module("myApp", ["ui.router"]); // myApp爲自定義模塊,依賴第三方路由模塊ui.router 這樣作的目的是: 在程序啓動(bootstrap)的時候,加載依賴模塊(如:ui.router),將全部 掛載 在該模塊的 服務(provider) , 指令(directive) , 過濾器(filter) 等都進行註冊 ,那麼在後面的程序中即可以調用了。 說到這裏,就得看看 ngRoute模塊 和 ui.router模塊 各自都提供了哪些服務,哪些指令? ngRoute $routeProvider(服務提供者) --------- 對應於下面的urlRouterProvider和stateProvider $route(服務) --------- 對應於下面的urlRouter和state $routeParams(服務) --------- 對應於下面的stateParams ng-view(指令) --------- 對應於下面的ui-view ui.router $urlRouterProvider(服務提供者) --------- 用來配置路由重定向 $urlRouter(服務) $stateProvider(服務提供者) --------- 用來配置路由 $state(服務) --------- 用來顯示當前路由狀態信息,以及一些路由方法(如:跳轉) $stateParams(服務) --------- 用來存儲路由匹配時的參數 ui-view(指令) --------- 路由模板渲染,對應的dom相關聯 ui-sref(指令) ... ( 注 : 服務提供者:用來提供服務實例和配置服務。 ) 這樣一看,其實 ui.router 和 ngRoute 大致的設計思路,對應的模塊劃分都是一致的(畢竟是同一個團隊開發),不一樣的地方在於功能點的實現和 加強 。 那麼問題來了: ngRoute 弱在哪些方面, ui.router 怎麼彌補了這些方面? 這裏,列舉兩個最重要的方面來講(其餘細節,後面再說): 多視圖 嵌套視圖 多視圖 多視圖:頁面能夠顯示多個動態變化的不一樣區塊。 這樣的業務場景是有的: 好比:頁面一個區塊用來顯示頁面狀態,另外一個區塊用來顯示頁面主內容,當路由切換時,頁面狀態跟着變化,對應的頁面主內容也跟着變化。 首先,咱們嘗試着用 ngRoute 來作: html
區塊1
區塊2
js $routeProvider .when('/', { template: 'hello world' }); 咱們在html中利用ng-view指令定義了兩個區塊,因而兩個div中顯示了相同的內容,這很合乎情理,但卻不是咱們想要的,可是又不能爲力,由於,在ngRoute中: 視圖沒有名字進行惟一標誌,因此它們被同等的處理。 路由配置只有一個模板,沒法配置多個。 ok,針對上述兩個問題,咱們嘗試用 ui.router 來作: html
js $stateProvider .state('home', { url: '/', views: { '': { template: 'hello world' }, 'status': { template: 'home page' } } }); 此次,結果是咱們想要的,兩個區塊,分別顯示了不一樣的內容,緣由在於,在ui.router中: 能夠給視圖命名,如:ui-view="status"。 能夠在路由配置中根據視圖名字(如:status),配置不一樣的模板(其實還有controller等)。 注 :視圖名是一個字符串,不能夠包含 @ (緣由後面會說)。 嵌套視圖 嵌套視圖:頁面某個動態變化區塊中,嵌套着另外一個能夠動態變化的區塊。 這樣的業務場景也是有的: 好比:頁面一個主區塊顯示主內容,主內容中的部份內容要求根據路由變化而變化,這時就須要另外一個動態變化的區塊嵌套在主區塊中。 其實,嵌套視圖,在html中的最終表現就像這樣:
轉成JavaScript,咱們會在程序裏這樣寫: $routeProvider .when('/', { template: 'I am parent
I am child
' }); 假若,你真的用 ngRoute 這樣寫,你會發現瀏覽器崩潰了,由於在ng-view指令link的過程當中,代碼會無限遞歸下去。 那麼形成這種現象的最根本緣由: 路由沒有明確的父子層級關係! 看看 ui.router 是如何解決這一問題的? $stateProvider .state('parent', { abstract: true, url: '/', template: 'I am parent
' }) .state('parent.child', { url: '', template: 'I am child' }); 巧妙地,經過 parent 與 parent.child 來肯定路由的 父子關係 ,從而解決無限遞歸問題。 另外子路由的模板最終也將被插入到父路由模板的div[ui-view]中去,從而達到視圖嵌套的效果。 ui.router工做原理 路由,大體能夠理解爲:一個 查找匹配 的過程。 對於前端 MVC(VM) 而言,就是將 hash值 (#xxx)與一系列的 路由規則 進行查找匹配,匹配出一個符合條件的規則,而後根據這個規則,進行數據的獲取,以及頁面的渲染。 因此,接下來: 第一步,學會如何建立路由規則? 第二步,瞭解路由查找匹配原理? 路由的建立 首先,看一個簡單的例子: $stateProvider .state('home', { url: '/abc', template: 'hello world' }); 上面,咱們經過調用 $stateProvider.state(...) 方法,建立了一個簡單路由規則,經過參數,能夠容易理解到: 規則名:'home' 匹配的url:'/abc' 對應的模板:'hello world' 意思就是說:當咱們訪問 http://xxxx#/abc 的時候,這個路由規則被匹配到,對應的模板會被填到某個 div[ui-view] 中。 看上去彷佛很簡單,那是由於咱們尚未深究具體的一些路由配置參數(咱們後面再說)。 這裏須要深刻的是: $stateProvider.state(...) 方法,它作了些什麼工做? 首先,建立並存儲一個state對象,裏面包含着該路由規則的全部配置信息。 而後,調用 $urlRouterProvider.when(...) 方法,進行路由的 註冊 (以前是路由的建立),代碼裏是這樣寫的: $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) { // 判斷是不是同一個state || 當前匹配參數是否相同 if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { $state.transitionTo(state, $match, { inherit: true, location: false }); } }]); 上述代碼的意思是:當 hash值 與 state.url 相匹配時,就執行後面那段回調,回調函數裏面進行了兩個條件判斷以後,決定是否須要跳轉到該state? 這裏就插入了一個話題:爲何說 「跳轉到該state,而不是該url」? 其實這個問題跟你們一直說的:「 ui.router是基於state(狀態)的,而不是url 」是同一個問題。 個人理解是這樣的:以前就說過,路由存在着明確的 父子關係 ,每個路由能夠理解爲一個state, 當程序匹配到某一個子路由時,咱們就認爲這個子路由state被激活,同時,它對應的父路由state也將被激活。 咱們還能夠手動的激活某一個state,就像上面寫的那樣, $state.transitionTo(state, ...); ,這樣的話,它的父state會被激活(若是尚未激活的話),它的子state會被銷燬(若是已經激活的話)。 ok,回到以前的路由註冊,調用了 $urlRouterProvider.when(...) 方法,它作了什麼呢? 它建立了一個rule,並存儲在rules集合裏面,以後的,每次hash值變化,路由從新查找匹配都是經過遍歷這個 rules 集合進行的。 路由的查找匹配 有了以前,路由的建立和註冊,接下來,天然會想到路由是如何查找匹配的? 恐怕,這得從頁面加載完畢提及: angular 在剛開始的$digest時, $rootScope 會觸發 $locationChangeSuccess 事件(angular在每次瀏覽器hash change的時候也會觸發 $locationChangeSuccess事件) ui.router 監聽了 $locationChangeSuccess 事件,因而開始經過遍歷一系列rules,進行路由查找匹配 當匹配到路由後,就經過 $state.transitionTo(state,...) ,跳轉激活對應的state 最後,完成數據請求和模板的渲染 能夠從下面這段源代碼看到,看到查找匹配的起始和過程: function update(evt) { // ...省略 function check(rule) { var handled = rule($injector, $location); // handled能夠是返回: // 1. 新的的url,用於重定向 // 2. false,不匹配 // 3. true,匹配 if (!handled) return false; if (isString(handled)) $location.replace().url(handled); return true; } var n = rules.length, i; // 渲染遍歷rules,匹配到路由,就中止循環 for (i = 0; i < n; i++) { if (check(rules[i])) return; } // 若是都匹配不到路由,使用otherwise路由(若是設置了的話) if (otherwise) check(otherwise); } function listen() { // 監聽$locationChangeSuccess,開始路由的查找匹配 listener = listener || $rootScope.$on('$locationChangeSuccess', update); return listener; } if (!interceptDeferred) listen(); 那麼,問題來了:難道每次路由變化(hash變化),因爲監聽了’$locationChangeSuccess'事件,都要進行rules的 遍歷 來查找匹配路由,而後跳轉到對應的state嗎? 答案是:確定的,通常的路由器都是這麼作的,包括ngRoute。 那麼ui.router對於這樣的問題,會怎麼進行 優化 呢? 迴歸到問題:咱們之因此要循環遍歷rules,是由於要查找匹配到對應的路由(state),而後跳轉過去,假若不循環,能直接找到對應的state嗎? 答案是:能夠的。 還記得前面說過,在用ui.router在建立路由時: 會實例化一個對應的state對象,並存儲起來(states集合裏面) 每個state對象都有一個state.name進行惟一標識(如:'home') 根據以上兩點,因而ui.router提供了另外一個指令叫作: ui-sref指令 ,來解決這個問題,好比這樣:
經過ui-sref跳轉到home state 當點擊這個a標籤時,會直接跳轉到home state,而並不須要循環遍歷rules,ui.router是這樣作到的(這裏簡單說一下): 首先,ui-sref="home"指令會給對應的dom添加 click事件 ,而後根據state.name,直接跳轉到對應的state,代碼像這樣: element.bind("click", function(e) { // ..省略若干代碼 var transition = $timeout(function() { // 手動跳轉到指定的state $state.go(ref.state, params, options); }); }); 跳轉到對應的state以後,ui.router會作一個善後處理,就是改變hash,因此理所固然,會觸發’$locationChangeSuccess'事件,而後執行回調,可是在回調中能夠經過一個判斷代碼規避循環rules,像這樣: function update(evt) { var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl; // 手動調用$state.go(...)時,直接return避免下面的循環 if (ignoreUpdate) return true; // 省略下面的循環ruls代碼 } 說了那麼多,其實就是想說,咱們 不建議直接使用href="#/xxx"來改變hash ,而後跳轉到對應state(雖然也是能夠的),由於這樣作會多了一步rules循環遍歷,浪費性能,就像下面這樣:
經過href跳轉到home state 路由詳解 這裏詳細地介紹ui.router的參數配置和一些深層次用法。 不過,在這以前,須要一個demo,ui.router的 官網demo 無非就是最好的學習例子,裏面涉及了大部分的知識點,因此接下來的代碼講解大部分都會是這裏面的(建議下載到本地進行代碼學習)。 爲了更好的學習這個demo,我畫了一張圖來描述這個demo的contacts部分各個視圖模塊,以下: 父與子 以前就說到,在ui.router中,路由就有父與子的關係(多個父與子湊起來就有了,祖先和子孫的關係),從javascript的角度來講,其實就是路由對應的state對象之間存在着某種 引用 的關係。 用一張數據結構的表示下contacts部分,大概是這樣( 原圖 ): 上面的圖看着有點亂,不過不要緊,起碼能看出各個state對象之間經過 parent 字段維繫了這樣一個 父與子 的關係(粉紅色的線)。 ok,接下來就看下是如何定義路由的父子關係的? 假設有一個父路由,以下: $stateProvider .state('contacts', {}); ui.router提供了幾種方法來定義它的子路由: 1.點標記法( 推薦 ) $stateProvider .state('contacts.list', {}); 經過 狀態名 簡單明瞭地來肯定父子路由關係,如:狀態名爲'a.b.c'的路由,對應的父路由就是狀態名爲'a.b'路由。 2. parent 屬性 $stateProvider .state({ name: 'list',// 狀態名也能夠直接在配置裏指定 parent: 'contacts'// 父路由的狀態名 }); 或者: $stateProvider .state({ name: 'list',// 狀態名也能夠直接在配置裏指定 parent: {// parent也能夠是一個父路由配置對象(指定路由的狀態名便可) name: 'contacts' } }); 經過 parent 直接指定父路由,能夠是父路由的狀態名(字符串),也能夠是一個包含狀態名的父路由配置(對象)。 居然路由有了 父與子 的關係,那麼它們的註冊順序有要求嘛? 答案是:沒有要求,咱們能夠在父路由存在以前,建立子路由(不過,不是很推薦),由於ui.router在遇到這種狀況時,在內部會幫咱們先 緩存 子路由的信息,等待它的父路由註冊完畢後,再進行子路由的註冊。 模板渲染 當路由成功跳轉到指定的state時,ui.router會觸發 '$stateChangeSuccess' 事件通知全部的 ui-view 進行模板從新渲染。 代碼是這樣的: if (options.notify) { $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); } 而 ui-view 指令在進行 link 的時候,在其內部就已經監聽了這一事件(消息),來隨時更新視圖: scope.$on('$stateChangeSuccess', function() { updateView(false); }); 大致的模板渲染過程就是這樣的,這裏遇到一個問題,就是:每個 div[ui-view]在從新渲染的時候如何獲取到對應視圖模板的呢? 要想知道這個答案, 首先,咱們得先看一下模板如何設置? 通常在設置 單視圖 的時候,咱們會這樣作: $stateProvider .state('contacts', { abstract: true, url: '/contacts', templateUrl: 'app/contacts/contacts.html' }); 在配置對象裏面,咱們用 templateUrl 指定模板路徑便可。 若是咱們須要設置 多視圖 ,就須要用到 views字段 ,像這樣: $stateProvider .state('contacts.detail', { url: '/{contactId:[0-9]{1,4}}', views: { '' : { templateUrl: 'app/contacts/contacts.detail.html', }, 'hint@': { template: 'This is contacts.detail populating the "hint" ui-view' }, 'menuTip': { templateProvider: ['$stateParams', function($stateParams) { return '
Contact ID: ' + $stateParams.contactId + ''; }] } } }); 這裏咱們使用了另外兩種方式設置模板: template :直接指定模板內容,另外也能夠是函數返回模板內容 templateProvider :經過依賴注入的調用函數的方式返回模板內容 上述咱們介紹了設置 單視圖 和 多視圖 模板的方式,其實最終它們在ui.router內部都會被統一格式化成的 views 的形式,且它們的key值會作特殊變化: 上述的 單視圖 會變成這樣: views: { // 模板內容會被安插在根路由模板(index.html)的匿名視圖下 '@': { abstract: true, url: '/contacts', templateUrl: 'app/contacts/contacts.html' } } 多視圖 會變成這樣: views: { // 模板內容會被安插在父路由(contacts)模板的匿名視圖下 '@contacts': { templateUrl: 'app/contacts/contacts.detail.html', }, // 模板內容會被安插在根路由(index.html)模板的名爲hint視圖下 'hint@': { template: 'This is contacts.detail populating the "hint" ui-view' }, // 模板內容會被安插在父路由(contacts)模板的名爲menuTip視圖下 'menuTip@contacts': { templateProvider: ['$stateParams', function($stateParams) { return '
Contact ID: ' + $stateParams.contactId + ''; }] } } 咱們會發現views對象裏面的 key 變化了,最明顯的是出現了一個 @ 符號,其實這樣的key值是ui.router的一個設計,它的原型是: viewName + '@' + stateName ,解釋下: viewName 指的是 ui-view="status" 中的'status' 也能夠是''(空字符串),由於會有匿名的 ui-view 或者 ui-view="" stateName -默認狀況下是父路由的 state.name ,由於子路由模板通常都安插在父路由的 ui-view 中 也能夠是''(空字符串),表示最頂層rootState 還能夠是任意的祖先 state.name 這樣原型的意思是,表示 該模板將會被安插在名爲stateName路由對應模板的viewName視圖下 (能夠看看上面代碼中的註釋理解下)。 其實這也解釋了以前我說的:「爲何state.name裏面不能存在 @ 符號」?由於 @ 在這裏被用於特殊含義了。 因此,到這裏,咱們就知道在 ui-view 從新進行模板渲染時,是根據 viewName + '@' + stateName 來獲取對應的視圖模板內容(其實還有controller等)的。 其實,因爲路由有了 父與子 的關係,某種程度上就有了override(覆蓋或者重寫)可能。 父路由和子路由之間就存在着視圖的override,像下面這段代碼: $stateProvider .state('contacts.detail', { url: '/{contactId:[0-9]{1,4}}', views: { 'hint@': { template: 'This is contacts.detail populating the "hint" ui-view' } } }); $stateProvider .state('contacts.detail.item', { url: '/item/:itemId', views: { 'hint@': { template: ' This is contacts.detail.item overriding the "hint" ui-view' } } }); 上面兩個路由(state)存在着 父與子 的關係,且他們都對 @hint 定義了視圖,那麼當子路由被激活時(它的父路由也會被激活),咱們應該選擇哪一個視圖配置呢? 答案是:子路由的配置。 具體的,ui.router是如何實現這樣的視圖override的呢? 簡單地回答就是:經過javascript原型鏈實現的,你能夠在每次路由切換成功後,嘗試着打印出 $state.current.locals 這個變量一看究竟。 還有一個很重要的問題,關乎性能:當咱們子路由變化時,頁面中全部的ui-view都會從新進行渲染嗎? 答案是:不會,只會從子路由對應的視圖開始局部從新渲染。 在每次路由變化時,ui.router會記錄變化的子路由,並對子路由進行從新的預處理(包括controller,reslove等),最後局部更新對應的ui-view,父路由部分是不會有任何變化的。 controller控制器 有了模板以後,必然不可缺乏controller向模板對應的做用域(scope)中填寫數據,這樣才能夠渲染出動態數據。 咱們能夠爲每個視圖添加不一樣的controller,就像下面這樣: $stateProvider .state('contacts', { abstract: true, url: '/contacts', templateUrl: 'app/contacts/contacts.html', resolve: { 'contacts': ['contacts', function( contacts){ return contacts.all(); }] }, controller: ['$scope', '$state', 'contacts', 'utils', function ($scope, $state, contacts, utils) { // 向做用域寫數據 $scope.contacts = contacts; }] }); 注意:controller是能夠進行 依賴注入 的,它注入的對象有兩種: 已經註冊的服務(service),如: $state , utils 上面的 reslove 定義的解決項(這個後面來講),如: contacts 可是無論怎樣,目的都是:向做用域裏寫數據。 reslove解決項 resolve在state配置參數中,是一個對象(key-value),每個value都是一個能夠依賴注入的函數,而且返回的是一個promise(固然也能夠是值,resloved defer)。 咱們一般會在resolve中,進行數據獲取的操做,而後返回一個promise,就像這樣: resolve: { 'contacts': ['contacts', function( contacts){ return contacts.all(); }] } 上面有好多contacts,爲了避免混淆,我改一下代碼: resolve: { 'myResolve': ['contacts', function(contacts){ return contacts.all(); }] } 這樣就看清了,咱們定義了resolve,包含了一個myResolve的key,它對應的value是一個函數,依賴注入了一個服務contacts,調用了 contacts.all() 方法並返回了一個promise。 因而咱們即可以在controller中引用myResolve,像這樣: controller: ['$scope', '$state', 'myResolve', 'utils', function ($scope, $state, contacts, utils) { // 向做用域寫數據 $scope.contacts = contacts; }] 這樣作的目的: 簡化了controller的操做,將數據的獲取放在resolve中進行,這在多個視圖多個controller須要相同數據時,有必定的做用。 只有當reslove中的promise所有resolved(即數據獲取成功)後,纔會觸發 '$stateChangeSuccess' 切換路由,進而實例化controller,而後更新模板。 另外,子路由的resolve或者controller都是能夠依賴注入父路由的resolve提供的數據服務,就像這樣: $stateProvider .state('parent', { url: '', resolve: { parent: ['$q', '$timeout', function ($q, $timeout) { var defer = $q.defer(); $timeout(function () { defer.resolve('parent'); }, 1000); return defer.promise; }] }, template: 'I am parent ' }) .state('parent.child', { url: '/child', resolve: { child: ['parent', function (parent) {// 調用父路由的解決項 return parent + ' and child'; }] }, controller: ['child', 'parent', function (child, parent) {// 調用自身的解決項,以及父路由的解決項 console.log(child, parent); }], template: 'I am child' }); 另外每個視圖也能夠單獨定義本身的resolve和controller,它們也是能夠依賴注入自身的state.resolve,或者view下的resolve,或者父路由的reslove,就像這樣: html javascript: $stateProvider .state('home', { url: '/home', resolve: { common: ['$q', '$timeout', function ($q, $timeout) {// 公共的resolve var defer = $q.defer(); $timeout(function () { defer.resolve('common data'); }, 1000); return defer.promise; }], }, views: { '': { resolve: { special: ['common', function (common) {// 訪問state.resolve console.log(common); }] } }, 'status': { resolve: { common: function () {// 重寫state.resolve return 'override common data' } }, controller: ['common', function (common) {// 訪問視圖自身的resolve console.log(common); }] } } }); 總結一下: 路由的controller除了能夠依賴注入正常的service,也能夠依賴注入resolve 子路由的resolve能夠依賴注入父路由的resolve,也能夠重寫父路由的resolve供controller調用 路由能夠有單獨的state.resolve以外,還能夠在views視圖中單獨配置resolve,視圖resolve是能夠依賴注入自身state.resolve甚至是父路由的state.resolve 原文連接:http://www.tuicool.com/articles/MjR3a