翻譯:深入 AngularUI Router
原文地址:http://www.ng-newsletter.com/posts/angular-ui-router.html
ui-router: https://angular-ui.github.io/ui-router/site/#/api/ui.router
ui-router 是 AngularUI 庫提供的特別有用的一個部分,是一個通過提供狀態機機制,而不是簡單的 URL 來組織我們的界面的路由框架。
這個庫提供了針對視圖的衆多的額外控制,我們可以創建嵌套的視圖,在單個頁面使用多個視圖,多個視圖來控制單個視圖,還有更多特性。對於更加精細的控制和更爲複雜的應用,ui-router 是非常棒的工具。
ui-router 從狀態着手來管理路由,它將應用視爲多個狀態的組合,通過狀態的切換進行路由。
- 一個狀態可以對應一個頁面地址,通過特定的地址到達應用的特定狀態。
- 通過狀態的 controller、template 和 views 來管理特定狀態的 UI 和行爲
- 通過嵌套視圖來解決頁面中重複出現的內容。
Installation
安裝 ui-router,既可以直接下載發佈版本,也可以通過 bower 來獲取。
$ bower install angular-ui-router --save
然後,需要在頁面中進入這個庫,當然要先加載 angular 庫。
<script type="text/javascript" src="angular-ui-router.js"></script>
在 angular 中注入 ui.router.
angular.module('myApp', ['ui.router'])
不像 angular 內置的 ngRoute, ui-router 可以嵌套視圖,它是基於狀態,而不 URL 的。
也不像 ngRoute 使用 ng-view 指令,在 ui-router 中,我們使用 ui-view 指令。
當在 ui-router 中考慮路由和狀態的關係時,我們主要關注應用的什麼狀態對應應用的什麼路由。
<div ng-controller="DemoController"> <div ui-view></div> </div>
類似 ngRoute, 對於給定的狀態,模板中的內容將會填充到 <div ui-view></div> 元素,每個模板還可以包含自己的 ui-view ,這就是我們可以支持嵌套路徑的原因。
定義路徑的時候,我們使用 .config 方法,像通常一樣,但是使用 $stateProvider 來替換 $routeProvider。
.config(function($stateProvider, $urlRouterProvider) { $stateProvider .state('start', { url: '/start', templateUrl: 'partials/start.html' }) });
這樣,我們;定義了名爲 start 的狀態,傳遞的對象定義了狀態配置信息,或者稱爲 stateConfig,類似於路由配置對象,我們通過它配置狀態信息。
template, templateUrl, templateProvider
可以使用下面三種之一的方式來定義視圖使用的模板:
- template, 字符串方式的模板內容,或者是一個返回 HTML 的函數
- templateUrl, 模板的路徑,或者返回模板路徑的函數
- templateProvider, 返回 HTML 內容的函數
例如
$stateProvider.state('home', { template: '<h1>Hello {{ name }}</h1>' });
Controller
類似於 ngRoute,我們既可以通過控制器的名字來關聯一個預定義的控制器,也可以直接創建一個控制器函數來處理。
如果沒有對應的模板定義,控制器對象就不會被創建。
Resolve
使用 resolve 功能,我們可以準備一組用來注入到控制器中的依賴對象。在 ngRoute 中,resolve 可以在路由實際渲染之前解決掉 promise
resolve 選項提供一個對象,對象中的 key 就是準備注入 controller 的依賴名稱,值則是創建對象的工廠。
如果是一個串,就試圖用這個串來匹配當前已經註冊的服務名稱,如果是一個函數,執行這個函數,返回的值就是依賴。如果函數返回一個 promise,在控制器被實例化之前,將會被 resolved,返回的值被注入到 controller 中。
$stateProvider.state('home', { resolve: { // This will return immediately as the // result is not a promise person: function() { return { name: "Ari", email: "[email protected]" } }, // This function returns a promise, therefore // it will be resolved before the controller // is instantiated currentDetails: function($http) { return $http({ method: 'JSONP', url: '/current_details' }); }, // We can use the resulting promise in another // resolution facebookId: function($http, currentDetails) { $http({ method: 'GET', url: 'http://facebook.com/api/current_user', params: { email: currentDetails.data.emails[0] } }) } }, controller: function($scope, person, currentDetails, facebookId) { $scope.person = person; } })
URL
url 用來設置應用對應的一個特定狀態. 也就是說,我們可以通過 url 來到達某個特定的狀態,所以這裏的 url 不是簡單的 url 地址,而是某個可到達狀態的標誌。
這個特性類似於 ngRoute 中的 URL, 但是,可以被看作一個重大的升級,後面我們就會看到。
簡單的路由類似下面所示。
$stateProvider .state('inbox', { url: '/inbox', template: '<h1>Welcome to your inbox</h1>' });
當我們導航到 /index 的時候,應用將會過渡到 inbox 狀態,使用這裏提供的內容模板填充 ui-view 的內容。
URL 中可以包含多種內容,令人難以置信的強大,可以像在 ngRoute 中設置簡單的參數。
$stateProvider .state('inbox', { url: '/inbox/:inboxId', template: '<h1>Welcome to your inbox</h1>', controller: function($scope, $stateParams) { $scope.inboxId = $stateParams.inboxId; } });
這裏,我們創建了 :inboxId 參數來捕獲 url 中的第二部分,例如,如果應用訪問 /inbox/1,那麼,$stateParameter.inboxId 就成爲 1, 實際上, $stateParams 的值將爲 { inboxId: 1 }
也可以使用另外一種語法。
url: '/inbox/{inboxId}'
路徑必須完全匹配,不像 ngRoute, 如果用戶訪問 /inbox/,這個路徑配置將會工作,但是,如果訪問 /inbox,這個狀態就不會被激活。
還可以使用正則表達式來表示參數,這樣可以通過正則表達式來設置匹配規則,例如。
// Match only inbox ids that contain // 6 hexidecimal digits url: '/inbox/{inboxId:[0-9a-fA-F]{6}}', // Or // match every url at the end of `/inbox` // to `inboxId` (a catch-all) url: '/inbox/{inboxId:.*}'
注意,不能在路由中使用捕獲組
甚至可以在路徑中使用查詢參數。
// will match a route such as // /inbox?sort=ascending url: '/inbox?sort'
絕對路由
如果你使用絕對 url 方式,需要在 url 字符串的開發加上特殊字符 ^
$stateProvider .state('contacts', { url: '/contacts', ... }) .state('contacts.list', { url: '^/list', ... });
'contacts'
狀態將匹配"/contacts"
'contacts.list'
狀態將匹配"/list"
。子狀態的url沒有附在父狀態的url之後的,因爲使用了^
。
嵌套路由
我們可以使用 url 參數添加到路由中來實現嵌套路由。這樣可以提供多個 ui-views 在我們的頁面中,例如,我們可以在 /inbox 之上,提供嵌套的獨立路由。這裏使用了子狀態。
$stateProvider .state('inbox', { url: '/inbox/:inboxId', template: '<div><h1>Welcome to your inbox</h1>\ <a ui-sref="inbox.priority">Show priority</a>\ <div ui-view></div>\ </div>', controller: function($scope, $stateParams) { $scope.inboxId = $stateParams.inboxId; } }) .state('inbox.priority', { url: '/priority', template: '<h2>Your priority inbox</h2>' });
第一個路由與前面一樣,現在還有第二個路由,一個匹配 inbox 之下的子路由,語法 (.) 表示這是一個子路由。
/inbox/1 匹配第一個狀態,/inbox/1/priority 則匹配第二個狀態。使用這種語法,我們可以在父路由中支持嵌套的 url。在父視圖中的 ui-view 指令將會處理 priority。
Params
params 選項是參數名稱或者正則的數組。它不能合併 url 選項,當狀態激活的時候,應用會使用這些參數填充 $stateParams 服務。
Views
我們可以在 state 中提供命名的視圖。這是強大的特性,在單個視圖中,我們可以定義多個視圖,甚至使用單個模板。
如果我們使用了 views 參數,那麼,templateUrl, template 和 templateProvider 就會忽略。如果我們希望包含父模板,我們需要創建一個抽象模板。
假如我們有如下模板。
<div> <div ui-view="filters"></div> <div ui-view="mailbox"></div> <div ui-view="priority"></div> </div>
主要注意的是,頂級的狀態自然對應母版頁中的視圖,一般這個視圖是 noname 的,所以,需要一個 noname 的視圖來匹配這個 placeholder。其它的視圖需要匹配父狀態中的視圖 placeholder,這些 placeholder 可以是命名的,也可以是 naname的,自然,noname 的只能有一個,否則無法進行區分,我們在 provider 中進行配置的時候,就需要描述清楚這些 view 和 placeholder 之間的對應關係。
使用 @ 可以定義絕對命名的視圖名稱,@ 的前面是 placeholder 的名稱,後面是狀態的名稱。@ 前面爲空表示未命名的 ui-view,@ 後面爲空表示相對於根模板,通常是 index.html
我們可以創建命名的視圖,然後填充對應的模板。每個子視圖都可以有特有的模板,控制器和數據。
$stateProvider .state('inbox', { views: { 'filters': { template: '<h4>Filter inbox</h4>', controller: function($scope) {} }, 'mailbox': { templateUrl: 'partials/mailbox.html' }, 'priority': { template: '<h4>Priority inbox</h4>', resolve: { facebook: function() { return FB.messages(); } } } } });
在這個例子中,我們有兩個命名的視圖嵌套在抽象視圖中。
Abstract
我們永遠不能直接激活抽象模板,但是,可以通過派生模板來激活。
抽象模板提供封裝命名視圖的模板,可以傳遞 $scope 對象給派生子模板。可以通過它解決依賴問題,或者特定數據處理,或者簡單地同樣的 url 來嵌套多個路由,例如,所有路由都在 /admin 下面。
$stateProvider .state('admin', { abstract: true, url: '/admin', template: '<div ui-view></div>' }) .state('admin.index', { url: '/index', template: '<h3>Admin index</h3>' }) .state('admin.users', { url: '/users', template: '<ul>...</ul>' });
onEnter, onExit
在應用進入或者退出視圖的時候,會調用這些回調函數。它們都可以設置回調函數;函數可以訪問獲取的數據。
這些回調函數可以提供我們一些能力,在訪問新視圖,或者改變當前狀態的時候。這裏是很好的執行 "Are you sure?" 對話框,或者請求用戶在進入之前登陸的地方。
兩個函數都不提供參數,需要的信息需要自己提供。
Data
我們可以附加任意的數到我們的狀態配置對象 configObject 上,data 屬性類似於 resolve 屬性,除了不會注入到控制器,也不會 resolve promise。
當需要從父狀態向子狀態傳遞數據的時候,附加數據是方便的途徑。
Evnets
類似 ngRoute 服務,angular-route 服務在狀態生命週期的不同時間點會觸發多種事件。我們可以通過在 $scope 中監聽來處理這些事件。
所有的下面的事件都會在 $rootScope 中觸發,所以,我們可以在任何 $scope 對象中監聽這些事件。
State change events
可以如下監聽
$scope.$on('$stateChangeStart', function(evt, toState, toParams, fromState, fromParams), { // We can prevent this state from completing evt.preventDefault(); });
$stateChangeStart
當從一個狀態開始向另外一個狀態過度的時候觸發。
$stateChangeSuccess
當狀態過渡完成之後觸發。
$stateChangeError
在狀態過渡中出現錯誤。常見的錯誤例如,不能獲取模板,或者 promise 不能成功 resolve 等等
視圖加載事件
ui-router 也提供了視圖加載階段的事件。
$viewContentLoading
視圖開始加載,但是,在 DOM 渲染之前。
可以如下監聽。
$scope.$on('$viewContentLoading', function(event, viewConfig){ // Access to all the view config properties. // and one special property 'targetView' // viewConfig.targetView });
$viewContentLoad
視圖已經加載,渲染完成。
$stateParams
在前面的內容中,我們使用 $stateParams 來從 url 參數中獲取 params ,這個服務,與 url 不同。
例如,如果我們 inbox 狀態的 url 如下。
url: '/inbox/:inboxId/messages/{sorted}?from&to'
用戶使用下面的 url 訪問
/inbox/123/messages/ascending?from=10&to=20
我們的 $stateParams 對象將獲取如下數據。
{inboxId: '123', sorted: 'ascending', from: 10, to: 20}
$urlRouterProvider
類似 ngRoute, 可以創建當特定的 url 訪問時處理的規則。
可以通過不同的 url 激活不同的狀態,所以在管理激活和加載狀態的時候, $urlRouterProvider 並不是必須的。在狀態管理之外的時候纔會需要,比如重定向,或者驗證的時候。
when()
when 函數需要兩個參數,我們希望匹配的路徑,另外就是我們希望重新定向的目標。也可以是一個函數。
例如,如果希望任何空的路由到我們的 /inbox 路由中。
.config(function($urlRouterProvider) { $urlRouterProvider.when('', '/inbox'); });
如果提供一個函數處理,路由匹配的時候,這個函數就會被調用,它可以返回下列三種之一的結果。
- false,這個迴應告訴 $urlRouter 規則並不匹配,應該查找其它匹配的狀態,在我們希望驗證用戶是否訪問正確地址的時候很有用。
- 字符串,$urlRouter 將其作爲重定向目標。
- true 或者 undefined,函數已經處理了這個 url
otherwise()
與 ngRoute 中的 oterwise() 方法類似,oterwiese() 在沒有其它路由匹配的情況下重定向。這是創建默認 url 的好方法。
otherwise() 函數只需要一個參數,一個字符串或者一個函數。
如果提供了一個字符串,就會被看做一個默認地址,在任何錯誤的或者不能匹配任何路由的時候,就會被重定向到這個地址。
如果是一個函數,在沒有其它路由匹配的時候,就會被執行
.config(function($urlRouterProvider) { $urlRouterProvider.otherwise('/'); // or $urlRouterProvider.otherwise( function($injector, $location) { $location.path('/'); }); });
rule()
如果我們希望處理任何路由,或者在其它路由之前進行一些處理,可以使用 rule() 函數。
我們必須返回一個驗證的路徑串
app.config(function($urlRouterProvider){ $urlRouterProvider.rule( function($injector, $location) { return '/index'; }); })
激活狀態
有三種方式來激活特定的狀態
- 使用 $state.go() 方法
- 使用 ui-sref 綁定的連接
- 直接導航到與狀態關聯的 url
創建報名嚮導
爲什麼不使用一下它呢?
我們創建一個報名的嚮導來演練一下 ui-router 的使用。
使用 ui-router ,我們創建一個簡單的報名服務,使用一個控制器來處理報名。
首先,我們創建應用的視圖。
<div ng-controller="WizardSignupController"> <h2>Signup wizard</h2> <div ui-view></div> </div>
在這個視圖中,我們定義了報名視圖。下一步,在報名嚮導中,需要三步
- start,在這一步,我們獲取用戶名稱,提供歡迎信息
- email, 這裏,我們獲取用戶的 email 信息
- finish, 這裏,用戶完成報名,我們簡單地顯示完成頁面
報名處理依賴 wizardapp.controllers 模塊,
angular.module('wizardApp', [ 'ui.router', 'wizardapp.controllers' ]);
我們的 wizardSignupController 控制器,使用 $scope.user 對象在整個過程中收集信息。
angular.module('wizardapp.controllers', []) .controller('WizardSignupController', ['$scope', '$state', function($scope, $state) { $scope.user = {}; $scope.signup = function() {} }]);
現在,嚮導處理邏輯處理主要工作,配置 config()
angular.module('wizardApp', [ 'ui.router', 'wizardapp.controllers' ]) .config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { $stateProvider .state('start', { url: '/step_1', templateUrl: 'partials/wizard/step_1.html' }) .state('email', { url: '/step_2', templateUrl: 'partials/wizard/step_2.html' }) .state('finish', { url: '/finish', templateUrl: 'partials/wizard/step_3.html' }); }]);
這樣,我們基本的流程就已經有了。現在,如果用戶導航到 /step_1,就會看到開始頁面,儘管現在地址是 /step_1, 而我們希望是 /wizard/step_1
爲了這個效果,我們創建 abstract 狀態來寄宿各個步驟。
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { $stateProvider .state('wizard', { abstract: true, url: '/wizard', template: '<div><div ui-view></div></div>' }) .state('wizard.start', { url: '/step_1', templateUrl: 'partials/wizard/step_1.html' }) .state('wizard.email', { url: '/step_2', templateUrl: 'partials/wizard/step_2.html' }) .state('wizard.finish', { url: '/finish', templateUrl: 'partials/wizard/step_3.html' }); }]);
這樣,它們都安全地嵌套到 /wizard 之下了。
在狀態之間進行導航,我們使用 ui-router 提供的指令 ui-sref 來生成鏈接,這個指令用來生成導航鏈接。
例如,step_1.html 如下。
<!-- step_1.html --> <h3>Step 1</h3> <form ng-submit=""> <input type="text" ng-model="user.name" placeholder="Your name" /> <input type="submit" class="button" value="Next" ui-sref="wizard.email"/> </form>
我們還希望在報名流程完成之後,執行特定的動作,來調用定義在父控制器上的 signup 函數,我們可以在最後的步驟中添加一個控制器來調用 $scope.signup() 函數。由於整個嚮導封裝在 WizardSignupControoler 中,我們可以像通常一樣訪問嵌套的 scope 對象。
.state('wizard.finish', { url: '/finish', templateUrl: 'partials/wizard/step_3.html', controller: function($scope) { $scope.signup(); } });
總結
在這裏,我們深入討論了 ui-router 幾乎全部的特性,我們發現這個庫非常有用,希望也能幫到你。