赞
踩
angular路由:
1.ng-route
路由功能是由 routeProvider服务 和 ng-view 搭配实现,ng-view相当于提供了页面模板的挂载点,当切换URL进行跳转时,不同的页面模板会放在ng-view所在的位置; 然后通过 routeProvider 配置路由的映射。
一般主要通过两个方法:
when():配置路径和参数;
otherwise:配置其他的路径跳转,可以想成default。
when的第二个参数:
controller:对应路径的控制器函数,或者名称
controllerAs:给控制器起个别名
template:对应路径的页面模板,会出现在ng-view处,比如"<div>xxxx</div>"
templateUrl:对应模板的路径,比如"src/xxx.html"
resolve:这个参数着重说下,该属性会以键值对对象的形式,给路由相关的控制器绑定服务或者值。然后把执行的结果值或者对应的服务引用,注入到控制器中。如果resolve中是一个promise对象,那么会等它执行成功后,才注入到控制器中,此时控制器会等待resolve中的执行结果。
详细的例子,可以参考下面的样例。
redirectTo:重定向地址
reloadOnSearch:设置是否在只有地址改变时,才加载对应的模板;search和params改变都不会加载模板
caseInsensitiveMatch
:路径区分大小写
路由有几个常用的事件:
$routeChangeStart:这个事件会在路由跳转前触发
$routeChangeSuccess:这个事件在路由跳转成功后触发
$routeChangeError:这个事件在路由跳转失败后触发
使用
在页面中,写入URL跳转的按钮链接 以及 ng-view标签
<div ng-controller="myCtrl"> <ul> <li><a href="#/a">click a</a></li> <li><a href="#/b">click b</a></li> </ul> <ng-view></ng-view> <!-- <div ng-view ></div> --> </div>
其中,ng-view可以当作元素或者标签等。
javascript中需要定义跳转的相关配置
<script type="text/javascript"> angular.module("myApp",["ngRoute"]) .controller("aController",function($scope,$route){ $scope.hello = "hello,a!"; }) .controller("bController",function($scope){ $scope.hello = "hello,b!"; }) .controller("myCtrl",function($scope,$location){ $scope.$on("$viewContentLoaded",function(){ console.log("ng-view content loaded!"); }); $scope.$on("$routeChangeStart",function(event,next,current){ //event.preventDefault(); //cancel url change console.log("route change start!"); }); }) .config(function($routeProvider, $locationProvider) { $routeProvider .when('/a', { templateUrl: 'a.html', controller: 'aController' }) .when('/b', { templateUrl: 'b.html', controller: 'bController', resolve: { // I will cause a 3 second delay delay: function($q, $timeout) { var delay = $q.defer(); $timeout(delay.resolve, 3000); return delay.promise; } } }) .otherwise({ redirectTo: '/a' }); }); </script>
上面的代码中,/b路径中的resolve关联来一个延迟方法,这个方法返回的时Promise对象,而且3秒钟后才会返回结果。因此b页面,在3秒后才会加载成功。
额外还需要提供两个html:
a.html:
<div ng-controller="aController" style="height:500px;width:100%;background-color:green;">{{hello}}</div>
以及b.html:
<div ng-controller="bController" style="height:2500px;width:100%;background-color:blue;">{{hello}}</div>
这样,就可以实现路由的跳转了。
路由 (route)
,几乎所有的 MVC(VM)
框架都应该具有的特性,因为它是前端构建单页面应用 (SPA)
必不可少的组成部分。
那么,对于 angular
而言,它自然也有 内置
的路由模块:叫做 ngRoute
。
不过,大家很少用它,因为它的功能太有限,往往不能满足开发需求!!
于是,一个基于 ngRoute
开发的 第三方路由模块 ,叫做 ui.router
,受到了大家的“追捧”。
首先,无论是使用哪种路由,作为框架额外的附加功能,它们都将以 模块依赖
的形式被引入,简而言之就是:在引入路由 源文件
之后,你的代码应该这样写(以 ui.router
为例):
angular.module("myApp", ["ui.router"]); // myApp为自定义模块,依赖第三方路由模块ui.router
这样做的目的是: 在程序启动(bootstrap)的时候,加载依赖模块(如:ui.router),将所有 挂载
在该模块的 服务(provider)
, 指令(directive)
, 过滤器(filter)
等都进行注册 ,那么在后面的程序中便可以调用了。
说到这里,就得看看 ngRoute模块
和 ui.router模块
各自都提供了哪些服务,哪些指令?
ng-view(指令) --------- 对应于下面的ui-view
ui.router
( 注
: 服务提供者:用来提供服务实例和配置服务。 )
这样一看,其实 ui.router
和 ngRoute
大体的设计思路,对应的模块划分都是一致的(毕竟是同一个团队开发),不同的地方在于功能点的实现和 增强
。
那么问题来了: ngRoute
弱在哪些方面, ui.router
怎么弥补了这些方面?
这里,列举两个最重要的方面来说(其他细节,后面再说):
多视图
多视图:页面可以显示多个动态变化的不同区块。
这样的业务场景是有的:
比如:页面一个区块用来显示页面状态,另一个区块用来显示页面主内容,当路由切换时,页面状态跟着变化,对应的页面主内容也跟着变化。
首先,我们尝试着用 ngRoute
来做:
html
- <div ng-view>区块1</div>
- <div ng-view>区块2</div>
js
- $routeProvider
- .when('/', {
- template: 'hello world'
- });
我们在html中利用ng-view指令定义了两个区块,于是两个div中显示了相同的内容,这很合乎情理,但却不是我们想要的,但是又不能为力,因为,在ngRoute中:
ok,针对上述两个问题,我们尝试用 ui.router
来做:
html
- <div ui-view></div>
- <div ui-view="status"></div>
js
- $stateProvider
- .state('home', {
- url: '/',
- views: {
- '': {
- template: 'hello world'
- },
- 'status': {
- template: 'home page'
- }
- }
- });
这次,结果是我们想要的,两个区块,分别显示了不同的内容,原因在于,在ui.router中:
注
:视图名是一个字符串,不可以包含 @
(原因后面会说)。
嵌套视图
嵌套视图:页面某个动态变化区块中,嵌套着另一个可以动态变化的区块。
这样的业务场景也是有的:
比如:页面一个主区块显示主内容,主内容中的部分内容要求根据路由变化而变化,这时就需要另一个动态变化的区块嵌套在主区块中。
其实,嵌套视图,在html中的最终表现就像这样:
- <div ng-view>
- I am parent
- <div ng-view>I am child</div>
- </div>
转成javascript,我们会在程序里这样写:
- $routeProvider
- .when('/', {
- template: 'I am parent <div ng-view>I am child</div>'
- });
倘若,你真的用 ngRoute
这样写,你会发现浏览器崩溃了,因为在ng-view指令link的过程中,代码会无限递归下去。
那么造成这种现象的最根本原因: 路由没有明确的父子层级关系!
看看 ui.router
是如何解决这一问题的?
- $stateProvider
- .state('parent', {
- abstract: true,
- url: '/',
- template: 'I am parent <div ui-view></div>'
- })
- .state('parent.child', {
- url: '',
- template: 'I am child'
- });
parent
与 parent.child
来确定路由的 父子关系
,从而解决无限递归问题。 路由,大致可以理解为:一个 查找匹配
的过程。
对于前端 MVC(VM)
而言,就是将 hash值
(#xxx)与一系列的 路由规则
进行查找匹配,匹配出一个符合条件的规则,然后根据这个规则,进行数据的获取,以及页面的渲染。
所以,接下来:
路由的创建
首先,看一个简单的例子:
- $stateProvider
- .state('home', {
- url: '/abc',
- template: 'hello world'
- });
上面,我们通过调用 $stateProvider.state(...)
方法,创建了一个简单路由规则,通过参数,可以容易理解到:
意思就是说:当我们访问 http://xxxx#/abc
的时候,这个路由规则被匹配到,对应的模板会被填到某个 div[ui-view]
中。
看上去似乎很简单,那是因为我们还没有深究具体的一些路由配置参数(我们后面再说)。
这里需要深入的是: $stateProvider.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.transitionTo(state, ...);
,这样的话,它的父state会被激活(如果还没有激活的话),它的子state会被销毁(如果已经激活的话)。 ok,回到之前的路由注册,调用了 $urlRouterProvider.when(...)
方法,它做了什么呢?
它创建了一个rule,并存储在rules集合里面,之后的,每次hash值变化,路由重新查找匹配都是通过遍历这个 rules
集合进行的。
路由的查找匹配
有了之前,路由的创建和注册,接下来,自然会想到路由是如何查找匹配的?
恐怕,这得从页面加载完毕说起:
$rootScope
会触发 $locationChangeSuccess
事件(angular在每次浏览器hash change的时候也会触发 $locationChangeSuccess
事件)$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在创建路由时:
根据以上两点,于是ui.router提供了另一个指令叫做: ui-sref指令
,来解决这个问题,比如这样:
<a ui-sref="home">通过ui-sref跳转到home state</a>
当点击这个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循环遍历,浪费性能,就像下面这样:
<a href="#/abc">通过href跳转到home state</a>
这里详细地介绍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 '<hr><small class="muted">Contact ID: ' + $stateParams.contactId + '</small>';
- }]
- }
- }
- });
这里我们使用了另外两种方式设置模板:
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 '<hr><small class="muted">Contact ID: ' + $stateParams.contactId + '</small>';
- }]
- }
- }
我们会发现views对象里面的 key
变化了,最明显的是出现了一个 @
符号,其实这样的key值是ui.router的一个设计,它的原型是: viewName + '@' + stateName
,解释下:
viewName
ui-view="status"
中的'status'ui-view
或者 ui-view=""
stateName
state.name
,因为子路由模板一般都安插在父路由的 ui-view
中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是可以进行 依赖注入
的,它注入的对象有两种:
$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;
- }]
这样做的目的:
'$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 <div ui-view></div>'
- })
- .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
- <div ui-view></div>
- <div ui-view="status"></div>
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);
- }]
- }
- }
- });
总结一下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。