何为路由
路由机制运可以实现多视图的单页Web应用(single page web application,SPA)。
单页应用在使用期间不会重新加载页面,所有的数据交互由ajax完成。
非单页应用请求不同url,对SEO很不友善,而且刷新或发送url无法保留浏览进度。
而单页应用前端拥有监管url的权利,通过监听页面地址变化来调用前端MVC模块,渲染不同页面。当然,前端这时也拥有了修改URL的能力,只需要管理好前端视图与URL。在确定了需要保持的页面后,前后端围绕页面各自负责视图的渲染。
路由实现
url不变话化
这一种的情况是url完全不变,即你的页面怎么改变,怎么跳转url都不会改变。
这种情况的原理 就是纯ajax拿到页面模板后替换原页面中的元素,重现渲染页面。然而ajax的一个致命缺点就是导致浏览器后退按钮失效。用户体验很不好。
使用hash值
这种类型的优点就是刷新页面,页面也不会丢。关于hash值的介绍链接
通过监听 hash(#)的变化来执行js代码 从而实现 页面的改变
核心代码:
window.addEventListener('hashchange',function(){ //code })
就是通过这个原理 只要#改变了 就能触发这个事件,这也是很多单页面网站的url中都也 (#)的原因
h5 history api
这种类型是通过html5的最新history api来实现的。
通过pushState()记录操作历史,监听window.onpopstate事件来进行视图切换,能正常的回退前进。
这里是关于 history api的连接
关于这三种方法的demo往后会给大家提供完善,大家可以提醒我。
AngularJS 路由
AngularJS 路由就是通过通过hash和h5 history两种方式实现的,允许我们通过不同的 URL 访问不同的内容。
通常我们的URL形式为 http://runoob.com/first/page,但在单页Web应用中 AngularJS 通过 # + 标记 实现,例如:
http://cds.com/#/firstPage http://cds.com/#/secondPage
当我们点击以上的任意一个链接时,向服务端请的地址都是一样的 (http://runoob.com/)。 因为 # 号之后的内容在向服务端请求时会被浏览器忽略掉。 所以我们就需要在客户端实现 # 号后面内容的功能实现。
AngularJS 路由通过 # + 标记 帮助我们区分不同的逻辑页面并将不同的页面(View)绑定到对应的控制器(controller)上。
下面我们介绍一下Angular的路由实现。
ngRouter
ngRouter是angular的一个单独模块。
我们看看他是怎么实现的。
1、加载实现路由的 js 文件:angular-route.js。
2、包含了 ngRoute 模块作为主应用模块的依赖模块。(为什么这么写,下文介绍)
angular.module('routingDemoApp',['ngRoute'])
3、使用 ngView 指令。
<div ng-view></div>
该 div 内的 HTML 内容会根据路由的变化而变化。
4、配置 $routeProvider,AngularJS $routeProvider 用来定义路由规则。
angular.module('MyApp',['ngRoute']).config(['$routeProvider',function($routeProvider){ //$routeProvider 为我们提供了 when(path,object) & otherwise(object) 函数按顺序定义所有路由,函数包含两个参数:第一个参数是 URL 或者 URL 正则规则。第二个参数是路由配置对象。 $routeProvider.when(url,{ template: string,//在 ng-view 中插入简单的 HTML 内容 templateUrl: string,//在 ng-view 中插入 HTML 模板文件 controller: string,function 或 array,//在当前模板上执行的controller函数,生成新的scope。 controllerAs: string,//为controller指定别名。 redirectTo: string,function,//重定向的地址。 resolve: object<key,function>//指定当前controller所依赖的其他模块。 }); $routeProvider.otherwise({redirectTo:'/'})//除配置路由外的其他路径指向 }]);
UI-Router
在使用ngRouter时往往不能进行视图嵌套,而且功能有限,不能满足开发需求。由此我们就需要另一个第三方路由模块,叫做 ui.router ,当然它是基于ngRouter开发的。
ui.Router提供了一种很好的机制,可以实现深层次嵌套。
实现(配置 )
1、在引入ui.route路由源文件
2、加载依赖模块
angular.module("myApp",["ui.router"]); // myApp为自定义模块,依赖第三方路由模块ui.router
在程序启动(bootstrap)的时候,加载依赖模块(如:ui.router),将所有 挂载 在该模块的 服务(provider) , 指令(directive) , 过滤器(filter) 等都进行注册 ,那么在后面的程序中便可以调用了。
angular.module("myApp",["ui.router","myFilter","myDirective","myService"]); //实际中只要依赖首页需要用的模块就可以。
3、定义视图
<div ui-view></div>//视图展示 <a ui-sref='dash'>dash页</a>//定义导航
在视图模板中还可以包含自己的 ui-view ,这就是我们可以支持嵌套路径的原因。
<div ui-view> <div ui-view></div> </div>
4、配置 $stateProvider,在ui.router通过$stateProvider来定义路由规则
angular.module("MyApp").config(['$stateProvider','$urlRouterProvider',function ($stateProvider,$urlRouterProvider) { //$stateProvider为我们提供了state(pathName,object) & otherwise(object) 函数按顺序定义所有路由,函数包含两个参数:第一个参数是 URL名称(视图中导航名称)。第二个参数是路由配置对象。 $urlRouterProvider.otherwise("/dash.html"); //定义默认跳转和除配置之外的定向路由 $stateProvider .state('dash'(string),{//导航用的名字,如<a ui-sref="dash">dash</a>里的login url: "/dash.html"(string),//访问路径,可视的路径 template: string,//在 ui-view 中插入简单的 HTML 内容 templateUrl: "views/dash.html"(string),//在 ui-view 中插入 HTML 模板文件(静态文件路径) data: {pageTitle: "系统主页"}(object<key,val>可以多层嵌套),//data数据不会注入到控制器,可以从父状态向子状态传递数据 controller: "DashController",//在当前模板上执行的controller函数,生成新的scope。 params:object,//params 选项是参数名称(下文有栗子)。当状态激活的时候,应用会使用这些参数填充 $stateParams 服务。 resolve: { deps: ['$ocLazyLoad',function ($ocLazyLoad) {//'$ocLazyLoad'是controller的依赖模块,返回执行方法。AngularJs 通过 ocLazyLoad 实现动态(懒)加载模块和依赖。具体的有时间再扩展。留个链接!!! return $ocLazyLoad.load({ name: 'MetronicApp',insertBefore: '#ng_load_plugins_before',files: [ 'app/controllers/DashController.js',] }); }] }//在下文介绍 }) }]);
resolve 功能
使用 resolve 功能,我们可以准备一组用来注入到控制器中的依赖对象,在 uiRoute 中,resolve 可以在路由实际渲染之前解决掉 promise。
resolve 选项提供一个对象,对象中的 key 就是准备注入 controller 的依赖名称,值则是创建对象的工厂。
如果是一个字符串,就试图用这个串来匹配当前已经注册的服务名称,
如果是一个函数,执行这个函数,返回的值就是依赖。
如果函数返回一个 promise,在控制器被实例化之前,将会被 resolved,返回的值被注入到 controller中。
路由嵌套
先说说为什么要视图嵌套,页面一个主区块显示主内容,主内容中的部分内容要求根据路由变化而变化,这时就需要另一个动态变化的区块嵌套在主区块中。
如果用ngRoute 实现视图嵌套,你会发现是不可能的,因为在ng-view指令link的过程中,代码会无限递归下去。造成这种现象的最根本原因:路由没有明确的父子层级关系。
而uiRouter很好的解决了这一问题。
惯例先上代码:
$stateProvider .state('parent',{ abstract: true,//可以通过它解决依赖问题,或者特定数据处理,或者简单地同样的 url 来嵌套多个路由,例如,所有路由都在 /parent下面。 url: '/parent',//相对路径=>…./index.html#/parent template: 'I am parent <div ui-view></div>'//当然你也可以写在templateUrl中 }) .state('parent.child',{ url: '/child',//相对路径=>…./index.html#/parent/child url:'^/child',//绝对路径=>…./index.html#/child 注意写法区别 template: 'I am child' }); <!--herf --> <a ui-sref="parent">点我显示父view内容</a> <a ui-sref="parent.child">点我显示父view与子view内容</a> <!--view --> <div ui-view></div> <!-- 父View -->
巧妙地,通过 parent 与 parent.child 来确定路由的 父子关系 ,从而解决无限递归问题。另外子路由的模板最终也将被插入到父路由模板的div[ui-view]中去,从而达到视图嵌套的效果。
多视图效果
多视图:页面可以显示多个动态变化的不同区块。
原因在于,在ui.router中:可以给视图命名(字符串),如:ui-view="status"。可以在路由配置中根据视图名字(如:status),配置不同的模板(其实还有controller等)。
放一段代码
<div ng-app="myApp" > <a ui-sref="index">点我显示index内容</a> <div ui-view="header"></div> <div ui-view="nav"></div> <div ui-view="body"></div> </div> var app = angular.module('myApp',['ui.router']); app.config(["$stateProvider",function ($stateProvider) { $stateProvider .state("index",{ url: '/index',views:{ 'header':{template:"<div>头部内容</div>"},'nav':{template:"<div>菜单内容</div>"},'body':{template:"<div>展示内容</div>"} } }) }]);
待完善(项目无涉及)(控制起来也很麻烦,处理不好都是坑,如果要用到再完善吧。)
视图定位
@的作用 是用来绝对定位view,即说明该ui-view属于哪个模板。
同样放一段代码
<div ng-app="myApp" > <a ui-sref="index">show index</a> <a ui-sref="index.content1">content111111</a> <a ui-sref="index.content2">content222222</a> <div ui-view="index"><div> </div> var app = angular.module('myApp',views:{ 'index':{template:"<div><div ui-view='header'></div> <div ui-view='nav'></div> <div ui-view='body'></div> </div>"},//这里必须要绝对定位 'header@index':{template:"<div>头部内容header</div>"},'nav@index':{template:"<div>菜单内容nav</div>"},'body@index':{template:"<div>展示内容contents</div>"} } }) //绝对定位 .state("index.content1",{ url: '/content1',views:{ 'body@index':{template:"<div>content11111111111111111</div>"} //'body@index'表时名为body的view使用index模板 } }) //相对定位:该状态的里的名为body的ui-view为相对路径下的(即没有说明具体是哪个模板下的) .state("index.content2",{ url: '/content2',views:{ 'body':{template:"<div>content2222222222222222222</div>"}// } }) }]);
待完善(项目无涉及)(其实也不难理解,只是这个时间段任务重了,这里有时间再了解完善啊)。
路由传参
Angular应用通过通过$stateParams服务获取参数他有两种方式:(相对温习一下路径穿参方式,$location.search())
1.url: '/index/:id'
2.url: '/index/{id}'
注意这里只是两种不同写法
angular.module("MetronicApp").config(['$stateProvider',$urlRouterProvider) { $stateProvider .state('dash',{ url: "/dash.html/:orderId/:projectId",//或者注意是或者(这种需要直接定义好,或由页面传值) url:"/dash.html/{orderId}",//如果应用访问 /dash.html/42,那么,$stateParameter.orderId 就成为 42,实际上, $stateParams 的值将为 { orderId: 42 } //或者(这种是在路由中定义好参数) url:"/dash.html",params: { orderId: { value: 42} } templateUrl: "/dash.html",controller: function ($stateParams,$scope) { //可以注入$stateParams服务来获取你所传递的服务 $scope.projectId=$stateParams.projectId; $scope.orderId=$stateParams.orderId; //42 } }) }]); //然后大家在看页面中的写法 也是两种方式建议用第二种() <a href="#/dash/42" >href传参数</a> <a ui-sref="dash({'orderId':42,'projectId':256})">ui-sref传参数</a>
angular.module("MetronicApp",['ui.router']); angular.module("MetronicApp").controller('MyCtrl',function($scope,$state,$location) { $scope.pathChange = function() { $state.go('dash',{name: 42}); //第一个参数是定义的urlName,第二个为参数 }; $scope.pathChange = function() { $location.path('/sixth/detail/42'); //$location服务中有介绍 }; });
可能大家会有疑问,我们项目的路径和这个示例不一样,这里的参数是angular是控制路径跳转的参数目的在与定向视图或者视图之间丶控制器之间的通信,不是从后台请求的,这个要搞清楚。
和之前说的$rootScope通信效果是一样的。
监听路由
Angular 路由状态发生改变时可以通过'$stateChangeStart'、'$stateChangeSuccess'、'$stateChangeError'监听,通过注入'$location'实现状态的管理。
代码示例如下:
function run($ionicPlatform,$location,Service,$rootScope,$stateParams) { //路由监听事件 $rootScope.$on('$stateChangeStart',function(event,toState,toParams,fromState,fromParams) { console.log(event); //该事件的基本信息 console.log(toState); //我们可以得到当前路由的信息,比如路由名称,url,视图的控制器,模板路径等等 console.log(toParams); //我们可以得到当前路由的参数 console.log(fromState); //我们可以得到上一个路由的信息,比如路由名称,url,视图的控制器,模板路径等等 console.log(fromParams); //我们可以得到上一个路由的参数 if (toState.name == "dash") { //获取参数之后可以调请求判断需要渲染什么页面,渲染不同的页面通过 $location 实现 if (toParams.id == 10) { //$location.path();//获取路由地址 // $location.path('/validation').replace(); // event.preventDefault()可以阻止模板解析 } } }) // stateChangeSuccess 当模板解析完成后触发 $rootScope.$on('$stateChangeSuccess',fromParams) { //code }) // $stateChangeError 当模板解析过程中发生错误时触发 $rootScope.$on('$stateChangeError',fromParams,error) { //code }) }
在页面渲染中 可通过'$viewContentLoading'和 '$viewContentLoaded'监听页面渲染状态:渲染开始和渲染结束。(在控制器中添加以下代码实现监听)
// $viewContentLoading- 当视图开始加载,DOM渲染完成之前触发,该事件将在$scope链上广播此事件。 $scope.$watch('$viewContentLoading',viewConfig){ alert('模板加载完成前'); }); //$viewContentLoaded- 当视图加载完成,DOM渲染完成之后触发,视图所在的$scope发出该事件。 $scope.$watch('$viewContentLoaded',function(event){ alert('模板加载完成后'); });
我们可以用这些监听事件来判断用户传参,权限判定等等。
工作原理
我们了解一下uiRouter路由的工作原理
大致可以理解为:一个 查找匹配 的过程。就是将 hash值 (#xxx)与一系列的 '路由规则' 进行查找匹配,匹配出一个符合条件的规则,然后根据这个规则,进行数据的获取,以及页面的渲染。
我们分两步学习
路由的创建
我们通过调用 $stateProvider.state(...) 方法,创建了一个简单路由规则(详情看上文)
当我们反问http://...index.html#/dash.html的时候,这个路由规则被匹配到,对应的模板会被填到某个 [ui-view] 中。
它做了些什么呢。首先,创建并存储一个state对象,里面包含着该路由规则的所有配置信息。
然后,调用 $urlRouterProvider.when(...) 方法(上文说过ui-router是基于ngRouter),进行路由的 注册 (之前是路由的创建),代码里是这样写的:
$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会被销毁(如果已经激活的话)。
接着回到路由注册,路由注册调用了 $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。我们之所以要循环遍历rules,是因为要查找匹配到对应的路由(state),然后跳转过去,倘若不循环,也是能直接找到对应的state。在用ui.router在创建路由时:会实例化一个对应的state对象,并存储起来(states集合里面)。每一个state对象都有一个state.name进行唯一标识(如:'dash')。这时候就体现出 ui-sref指令的大作用了。
<a ui-sref="dash">通过ui-sref跳转到dash.html</a>
当点击这个a标签时,会直接跳转到dash.html,而并不需要循环遍历rules。这个元素折行了一个方法,还是直接看代码
element.bind("click",function(e) { // .. var transition = $timeout(function() { // 手动跳转到指定的state $state.go(ref.state,params,options); }); });
ui-sref="dash"指令会给对应的dom添加 click事件 ,然后根据dash(state.name),直接跳转到对应的state。
跳转到对应的state之后,ui.router会做一个善后处理,就是改变hash,此时就会触发’$locationChangeSuccess'事件,然后执行回调,但是在回调中可以通过一个判断代码规避循环rules。
代码段:
function update(evt) { var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl; // 手动调用$state.go(...)时,直接return避免下面的循环 if (ignoreUpdate) return true; }
所以我们在使用中可通过ui-serf来实现路由,达到视图切换,或者在controller中调用 $state.go(....)来实现。
结语
路由介绍的就这么多,但是任然要仔细逐行阅读。也有一部分拓展,项目中没有实际用到,但是也算是给优化做一些思路。与大家共勉。