angular路由详解

Posted 前端初学者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了angular路由详解相关的知识,希望对你有一定的参考价值。

每天一篇好文章系列18年第99期

编者按

        文章作者解析路由的用法以及ui-router和ng-router的区别

01












路由 (route) ,几乎所有的 MVC(VM) 框架都应该具有的特性,因为它是前端构建单页面应用 (SPA) 必不可少的组成部分。

那么,对于 angular 而言,它自然也有 内置 的路由模块:叫做 ngRoute 。

不过,大家很少用它,因为它的功能太有限,往往不能满足开发需求!!

于是,一个基于 ngRoute 开发的 第三方路由模块 ,叫做 ui.router ,受到了大家的“追捧”。

ngRoute vs ui.router

首先,无论是使用哪种路由,作为框架额外的附加功能,它们都将以 模块依赖 的形式被引入,简而言之就是:在引入路由 源文件 之后,你的代码应该这样写(以 ui.router 为例):

angular.module("myApp", ["ui.router"]); 

这样做的目的是: 在程序启动(bootstrap)的时候,加载依赖模块(如:ui.router),将所有 挂载 在该模块的 服务(provider) , 指令(directive) , 过滤器(filter) 等都进行注册 ,那么在后面的程序中便可以调用了。

  1. 多视图:页面可以显示多个动态变化的不同区块。

  2. 这样的业务场景是有的:

  3. 比如:页面一个区块用来显示页面状态,另一个区块用来显示页面主内容,当路由切换时,页面状态跟着变化,对应的页面主内容也跟着变化。

  4. 首先,我们尝试着用 ngRoute 来做:

  5. html

    
       
         
         
       
    1. <div ng-view>区块1</div>

    2. <div ng-view>区块2</div>

    js

    
       
         
         
       
    1. $routeProvider

    2. .when('/', {

    3.    template: 'hello world'

    4. });

    我们在html中利用ng-view指令定义了两个区块,于是两个div中显示了相同的内容,这很合乎情理,但却不是我们想要的,但是又不能为力,因为,在ngRoute中:

    1. 视图没有名字进行唯一标志,所以它们被同等的处理。

    2. 路由配置只有一个模板,无法配置多个。

    ok,针对上述两个问题,我们尝试用 ui.router 来做:

    html

    
       
         
         
       
    1. <div ui-view></div>

    2. <div ui-view="status"></div>

    js

    1. $stateProvider

    2. .state('home', {

    3. url: '/',

    4. views: {

    5. '': {

    6. template: 'hello world'

    7. },

    8. 'status': {

    9. template: 'home page'

    10. }

    11. }

    12. });

    13. 可以给视图命名,如:ui-view="status"。

    14. 可以在路由配置中根据视图名字(如:status),配置不同的模板(其实还有controller等)。

     :视图名是一个字符串,不可以包含 @ (原因后面会说)。

02











































嵌套视图

嵌套视图:页面某个动态变化区块中,嵌套着另一个可以动态变化的区块。

这样的业务场景也是有的:

比如:页面一个主区块显示主内容,主内容中的部分内容要求根据路由变化而变化,这时就需要另一个动态变化的区块嵌套在主区块中。

其实,嵌套视图,在html中的最终表现就像这样:


    
      
      
    
  1. <div ng-view>

  2. I am parent

  3.      <div ng-view>I am child</div>

  4. </div>

转成javascript,我们会在程序里这样写:


    
      
      
    
  1. $routeProvider

  2. .when('/', {

  3.      template: 'I am parent <div ng-view>I am child</div>'

  4. });

倘若,你真的用 ngRoute 这样写,你会发现浏览器崩溃了,因为在ng-view指

令link的过程中,代码会无限递归下去。

那么造成这种现象的最根本原因: 路由没有明确的父子层级关系!

看看 ui.router 是如何解决这一问题的?


    
      
      
    
  1. $stateProvider

  2. .state('parent', {

  3. abstract: true,

  4. url: '/',

  5. template: 'I am parent <div ui-view></div>'

  6. })

  7. .state('parent.child', {

  8. url: '',

  9. template: 'I am child'

  10. });

  1. 巧妙地,通过 parent 与 parent.child 来确定路由的 父子关系 ,从而解决无限递归问题。

  2. 另外子路由的模板最终也将被插入到父路由模板的div[ui-view]中去,从而达到视图嵌套的效果。



03



































ui.router工作原理

路由,大致可以理解为:一个 查找匹配 的过程。

对于前端 MVC(VM) 而言,就是将 hash值 (#xxx)与一系列的 路由规则 进行查找匹配,匹配出一个符合条件的规则,然后根据这个规则,进行数据的获取,以及页面的渲染。

所以,接下来:

  • 第一步,学会如何创建路由规则?

  • 第二步,了解路由查找匹配原理?

路由的创建

首先,看一个简单的例子:


  
    
    
  
  1. $stateProvider

  2. .state('home', {

  3. url: '/abc',

  4. template: 'hello world'

  5. });

上面,我们通过调用 $stateProvider.state(...) 方法,创建了一个简单路由规则,通过参数,可以容易理解到:

  1. 规则名:'home'

  2. 匹配的url:'/abc'

  3. 对应的模板:'hello world'

意思就是说:当我们访问 http://xxxx#/abc 的时候,这个路由规则被匹配到,对应的模板会被填到某个 div[ui-view] 中。

看上去似乎很简单,那是因为我们还没有深究具体的一些路由配置参数(我们后面再说)。

这里需要深入的是: $stateProvider.state(...) 方法,它做了些什么工作?

  1. 首先,创建并存储一个state对象,里面包含着该路由规则的所有配置信息。

  2. 然后,调用 $urlRouterProvider.when(...) 方法,进行路由的 注册 (之前是路由的创建),代码里是这样写的:

    1. $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) {

    2. // 判断是否是同一个state || 当前匹配参数是否相同

    3.      if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) {

    4. $state.transitionTo(state, $match, { inherit: true, location: false });

    5. }

    6. }]);

  3. 上述代码的意思是:当 hash值 与 state.url 相匹配时,就执行后面那段回调,回调函数里面进行了两个条件判断之后,决定是否需要跳转到该state?

  4. 这里就插入了一个话题:为什么说 “跳转到该state,而不是该url”?

  5. 其实这个问题跟大家一直说的:“ ui.router是基于state(状态)的,而不是url ”是同一个问题。

    我的理解是这样的:之前就说过,路由存在着明确的 父子关系 ,每一个路由可以理解为一个state,

    1. 当程序匹配到某一个子路由时,我们就认为这个子路由state被激活,同时,它对应的父路由state也将被激活。

    2. 我们还可以手动的激活某一个state,就像上面写的那样, $state.transitionTo(state, ...); ,这样的话,它的父state会被激活(如果还没有激活的话),它的子state会被销毁(如果已经激活的话)。

    用ui.router在创建路由时:

    1. 会实例化一个对应的state对象,并存储起来(states集合里面)

    2. 每一个state对象都有一个state.name进行唯一标识(如:'home')

    根据以上两点,于是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,代码像这样:

    
       
         
         
       
    1. element.bind("click", function(e) {

    2. // ..省略若干代码

    3.      var transition = $timeout(function() {

    4. // 手动跳转到指定的state

    5. $state.go(ref.state, params, options);

    6. });

    7. });

    跳转到对应的state之后,ui.router会做一个善后处理,就是改变hash,所以理所当然,会触发’$locationChangeSuccess'事件,然后执行回调,但是在回调中可以通过一个判断代码规避循环rules,像这样:

    
       
         
         
       
    1. function update(evt) {

    2.     var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl;


    3. // 手动调用$state.go(...)时,直接return避免下面的循环

    4.     if (ignoreUpdate) return true;


    5. // 省略下面的循环ruls代码

    6. }

    说了那么多,其实就是想说,我们 不建议直接使用href="#/xxx"来改变hash ,然后跳转到对应state(虽然也是可以的),因为这样做会多了一步rules循环遍历,浪费性能,就像下面这样:

    <a href="#/abc">通过href跳转到home state</a>

    路由详解

    父与子

    之前就说到,在ui.router中,路由就有父与子的关系(多个父与子凑起来就有了,祖先和子孙的关系),从javascript的角度来说,其实就是路由对应的state对象之间存在着某种 引用 的关系。

    ok,接下来就看下是如何定义路由的父子关系的?

    假设有一个父路由,如下:

    
       
         
         
       
    1. $stateProvider

    2. .state('contacts', {});

    ui.router提供了几种方法来定义它的子路由:

    1.点标记法( 推荐 )

    
       
         
         
       
    1. $stateProvider

    2. .state('contacts.list', {});

    通过 状态名 简单明了地来确定父子路由关系,如:状态名为'a.b.c'的路由,对应的父路由就是状态名为'a.b'路由。

    2. parent 属性

    
       
         
         
       
    1. $stateProvider

    2. .state({

    3. name: 'list',// 状态名也可以直接在配置里指定

    4.    parent: 'contacts'// 父路由的状态名

    5. });

    或者:

    1. $stateProvider

    2. .state({

    3. name: 'list',// 状态名也可以直接在配置里指定

    4.     parent: {// parent也可以是一个父路由配置对象(指定路由的状态名即可)

    5. name: 'contacts'

    6. }

    7. });

    通过 parent 直接指定父路由,可以是父路由的状态名(字符串),也可以是一个包含状态名的父路由配置(对象)。

    竟然路由有了 父与子 的关系,那么它们的注册顺序有要求嘛?

    答案是:没有要求,我们可以在父路由存在之前,创建子路由(不过,不是很推荐),因为ui.router在遇到这种情况时,在内部会帮我们先 缓存 子路由的信息,等待它的父路由注册完毕后,再进行子路由的注册。

    模板渲染

    当路由成功跳转到指定的state时,ui.router会触发 '$stateChangeSuccess' 事件通知所有的 ui-view 进行模板重新渲染。

    代码是这样的:

    
       
         
         
       
    1. if (options.notify) {

    2. $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams);

    3. }

    而 ui-view 指令在进行 link 的时候,在其内部就已经监听了这一事件(消息),来随时更新视图:

    
       
         
         
       
    1. scope.$on('$stateChangeSuccess', function() {

    2. updateView(false);

    3. });

    大体的模板渲染过程就是这样的,这里遇到一个问题,就是:每一个 div[ui-view]在重新渲染的时候如何获取到对应视图模板的呢?

    要想知道这个答案,

    首先,我们得先看一下模板如何设置?

    一般在设置 单视图 的时候,我们会这样做:

    
       
         
         
       
    1. $stateProvider

    2. .state('contacts', {

    3.     abstract: true,

    4. url: '/contacts',

    5. templateUrl: 'app/contacts/contacts.html'

    6. });

    在配置对象里面,我们用 templateUrl 指定模板路径即可。

    如果我们需要设置 多视图 ,就需要用到 views字段 ,像这样:

    1. $stateProvider

    2. .state('contacts.detail', {

    3. url: '/{contactId:[0-9]{1,4}}',

    4. views: {

    5.          '' : {

    6. templateUrl: 'app/contacts/contacts.detail.html',

    7. },

    8.         'hint@': {

    9. template: 'This is contacts.detail populating the "hint" ui-view'

    10. },

    11.         'menuTip': {

    12. templateProvider: ['$stateParams', function($stateParams) {

    13.          return '<hr><small class="muted">Contact ID: ' + $stateParams.contactId + '</small>';

    14. }]

    15. }

    16. }

    17. });

    这里我们使用了另外两种方式设置模板:

    1. template :直接指定模板内容,另外也可以是函数返回模板内容

    2. templateProvider :通过依赖注入的调用函数的方式返回模板内容

    上述我们介绍了设置 单视图 和 多视图 模板的方式,其实最终它们在ui.router内部都会被统一格式化成的 views 的形式,且它们的key值会做特殊变化:

    上述的 单视图 会变成这样:

    1. views: {

    2. // 模板内容会被安插在根路由模板(index.html)的匿名视图下

    3. '@': {

    4.     abstract: true,

    5. url: '/contacts',

    6. templateUrl: 'app/contacts/contacts.html'

    7. }

    8. }

    多视图 会变成这样:

    
       
         
         
       
    1. views: {

    2. // 模板内容会被安插在父路由(contacts)模板的匿名视图下

    3.      '@contacts': {

    4. templateUrl: 'app/contacts/contacts.detail.html',

    5. },

    6. // 模板内容会被安插在根路由(index.html)模板的名为hint视图下

    7.  'hint@': {

    8. template: 'This is contacts.detail populating the "hint" ui-view'

    9. },

    10. // 模板内容会被安插在父路由(contacts)模板的名为menuTip视图下

    11.        'menuTip@contacts': {

    12. templateProvider: ['$stateParams', function($stateParams) {

    13.        return '<hr><small class="muted">Contact ID: ' + $stateParams.contactId + '</small>';

    14. }]

    15. }

    16. }

    我们会发现views对象里面的 key 变化了,最明显的是出现了一个 @ 符号,其实这样的key值是ui.router的一个设计,它的原型是: viewName + '@' + stateName ,解释下:

    1. viewName

    2. 指的是 ui-view="status" 中的'status'

    3. 也可以是''(空字符串),因为会有匿名的 ui-view 或者 ui-view=""

    4. stateName 
      -默认情况下是父路由的 state.name ,因为子路由模板一般都安插在父路由的 ui-view 中

    5. 也可以是''(空字符串),表示最顶层rootState

    6. 还可以是任意的祖先 state.name

    这样原型的意思是,表示 该模板将会被安插在名为stateName路由对应模板的viewName视图下 (可以看看上面代码中的注释理解下)。

    其实这也解释了之前我说的:“为什么state.name里面不能存在 @ 符号”?因为 @ 在这里被用于特殊含义了。

    所以,到这里,我们就知道在 ui-view 重新进行模板渲染时,是根据 viewName + '@' + stateName 来获取对应的视图模板内容(其实还有controller等)的。

    其实,由于路由有了 父与子 的关系,某种程度上就有了override(覆盖或者重写)可能。

    父路由和子路由之间就存在着视图的override,像下面这段代码:

    
       
         
         
       
    1. $stateProvider

    2. .state('contacts.detail', {

    3. url: '/{contactId:[0-9]{1,4}}',

    4. views: {

    5.      'hint@': {

    6. template: 'This is contacts.detail populating the "hint" ui-view'

    7. }

    8. }

    9. });


    10. $stateProvider

    11. .state('contacts.detail.item', {

    12. url: '/item/:itemId',

    13. views: {

    14. 'hint@': {

    15. template: ' This is contacts.detail.item overriding the "hint" ui-view'

    16. }

    17. }

    18. });

上面两个路由(state)存在着 父与子 的关系,且他们都对 @hint 定义了视图,那么当子路由被激活时(它的父路由也会被激活),我们应该选择哪个视图配置呢?

答案是:子路由的配置。

controller控制器

有了模板之后,必然不可缺少controller向模板对应的作用域(scope)中填写数据,这样才可以渲染出动态数据。

我们可以为每一个视图添加不同的controller,就像下面这样:


  
    
    
  
  1. $stateProvider

  2. .state('contacts', {

  3. abstract: true,

  4. url: '/contacts',

  5. templateUrl: 'app/contacts/contacts.html',

  6. resolve: {

  7. 'contacts': ['contacts',

  8. function( contacts){

  9. return contacts.all();

  10. }]

  11. },

  12. controller: ['$scope', '$state', 'contacts', 'utils',

  13. function ($scope,   $state,   contacts,   utils) {

  14. // 向作用域写数据

  15. $scope.contacts = contacts;

  16. }]

  17. });

注意:controller是可以进行 依赖注入 的,它注入的对象有两种:

  1. 已经注册的服务(service),如: $state , utils

  2. 上面的 reslove 定义的解决项(这个后面来说),如: contacts

但是不管怎样,目的都是:向作用域里写数据。

reslove解决项

resolve在state配置参数中,是一个对象(key-value),每一个value都是一个可以依赖注入的函数,并且返回的是一个promise(当然也可以是值,resloved defer)。

我们通常会在resolve中,进行数据获取的操作,然后返回一个promise,就像

这样:


  
    
    
  
  1. resolve: {

  2.    'contacts': ['contacts',

  3.     function( contacts){

  4.     return contacts.all();

  5. }]

  6. }

上面有好多contacts,为了不混淆,我改一下代码:


  
    
    
  
  1. resolve: {

  2.     'myResolve': ['contacts',

  3.     function(contacts){

  4.     return contacts.all();

  5. }]

  6. }

这样就看清了,我们定义了resolve,包含了一个myResolve的key,它对应的value是一个函数,依赖注入了一个服务contacts,调用了 contacts.all() 方法并返回了一个promise。

于是我们便可以在controller中引用myResolve,像这样:


  
    
    
  
  1. controller: ['$scope', '$state', 'myResolve', 'utils',

  2.     function ($scope,   $state,   contacts,   utils) {

  3.    // 向作用域写数据

  4. $scope.contacts = contacts;

  5. }]

这样做的目的:

  1. 简化了controller的操作,将数据的获取放在resolve中进行,这在多个视图多个controller需要相同数据时,有一定的作用。

  2. 只有当reslove中的promise全部resolved(即数据获取成功)后,才会触发 '$stateChangeSuccess' 切换路由,进而实例化controller,然后更新模板。

另外,子路由的resolve或者controller都是可以依赖注入父路由的resolve提供的数据服务,就像这样:


  
    
    
  
  1. $stateProvider

  2. .state('parent', {

  3. url: '',

  4. resolve: {

  5. parent: ['$q', '$timeout', function ($q, $timeout) {

  6. var defer = $q.defer();

  7. $timeout(function () {

  8. defer.resolve('parent');

  9. }, 1000);

  10. return defer.promise;

  11. }]

  12. },

  13. template: 'I am parent <div ui-view></div>'

  14. })

  15. .state('parent.child', {

  16. url: '/child',

  17. resolve: {

  18. child: ['parent', function (parent) {// 调用父路由的解决项

  19. return parent + ' and child';

  20. }]

  21. },

  22. controller: ['child', 'parent', function (child, parent) {// 调用自身的解决项,以及父路由的解决项

  23. console.log(child, parent);

  24. }],

  25. template: 'I am child'

  26. })

点评

        文章详细介绍了angular路由的含义以及原理,对于刚刚接触angular路由的初学者是一个不错的参考。


本文选自mdn
作者:

相关阅读推荐





更多内容,请点击左下角阅读原文



以上是关于angular路由详解的主要内容,如果未能解决你的问题,请参考以下文章

angular路由详解四(子路由)

angularJS使用ocLazyLoad实现js延迟加载

angular4.0 路由守卫详解

快速入门!详解 Angular2 路由的分类及使用!

angular路由详解

angularJs-route路由详解