/ javascript

AngularUI Router

Angular-Ui 对于 AngularJS 开发者来说是一个非常重要的工具,其中 UI-Router 又是重中之重。UI-Router 插件提供了“嵌套作用域等规则”等有用的特性,对于复杂项目开发非常实用,最近在项目中开始使用 UI-Router ,于是决定好好把它学习一遍。

多视图

页面可以显示多个动态变化的不同区块

<div ui-view></div>
<div ui-view="status"></div>
$stateProvider
    .state('home', {
        url: '/',
        views: {
            '': {
                template: 'hello world'
            },
            'status': {
                template: 'home page'
            }
        }
    });

嵌套视图

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

<div ng-view>
    I am parent
    <div ng-view>I am child</div>
</div>
$stateProvider
    .state('parent', {
        abstract: true,
        url: '/',
        template: 'I am parent <div ui-view></div>'
    })
    .state('parent.child', {
        url: '',
        template: 'I am child'
    });

工作原理

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

路由的创建

创建

$stateProvider
    .state('status', {
        url: '/abc',
        template: 'hello world'
    });
首先,创建并存储一个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
  • 路由注册,调用了$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();

在用ui.router在创建路由时:

会实例化一个对应的state对象,并存储起来(states集合里面)
每一个state对象都有一个state.name进行唯一标识(如:’home’)

<a ui-sref="home">通过ui-sref跳转到home state</a>

点击这个a标签时,会直接跳转到home state,而并不需要循环遍历rules,
然后跳转到对应的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

路由配置

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

$stateProvider
    .state('contacts.list', {});

$stateProvider
    .state({
        name: 'list',       // 状态名也可以直接在配置里指定
        parent: 'contacts'  // 父路由的状态名
    });

$stateProvider
    .state({
        name: 'list',       // 状态名也可以直接在配置里指定
        parent: {           // parent也可以是一个父路由配置对象(指定路由的状态名即可)
            name: 'contacts'
        }
    });
$stateProvider.state('parentState.childState', {
//template 
      template: '<h1>My Contacts</h1>',
      templateUrl: 'contacts.html'  ,
      templateUrl: function ($stateParams){
        return '/partials/contacts.' + stateParams.filterBy + '.html';
      },
      templateProvider: function ($timeout, $stateParams) {
        return $timeout(function () {
          return '<h1>' + $stateParams.contactId + '</h1>'
        }, 100);
      },


//controller
      controller: function($scope){
        $scope.title = 'My Contacts';
      },
      // 在模块中已经定义了一个控制器,只需要指定控制器的名称
      controller: 'ContactsCtrl',

      controllerProvider: function($stateParams) {
          var ctrlName = $stateParams.type + "Controller";
          return ctrlName;
      },
      
//resolve      
//### resolve

//简化了controller的操作,将数据的获取放在resolve中进行,这在多个视图多个controller需要相同数据时,有一定的作用。
//只有当reslove中的promise全部resolved(即数据获取成功)后,才会触发'$stateChangeSuccess'切换路由,进而实例化controller,然后更新模板。
      resolve:{
         // Example using function with simple return value.
         // Since it's not a promise, it resolves immediately.
         simpleObj:  function(){
            return {value: 'simple!'};
         },
         // Example using function with returned promise.
         // 这是一种典型使用方式
         // 请给函数注入任何想要的服务依赖,例如 $http
         promiseObj:  function($http){
            // $http returns a promise for the url data
            return $http({method: 'GET', url: '/someUrl'});
         },
      },
      // 控制器将等待上面的解决项都被解决后才被实例化
      controller: function($scope, simpleObj, promiseObj){
          
          $scope.simple = simpleObj.value;
          // 这里可以放心使用 promiseObj 中的对象
          $scope.items = promiseObj.items;

      }
   })

模板渲染

当路由成功跳转到指定的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);
});

question:
每一个 div[ui-view] 在重新渲染的时候,如何获取到对应视图模板

单视图,多视图

单视图多视图,最终它们在ui.router内部都会被统一格式化成的views的形式


//单视图
$stateProvider
    .state('contacts', {
        abstract: true,
        url: '/contacts',
        templateUrl: 'app/contacts/contacts.html'
    });

//格式化为多视图
$stateProvider
    .state('contacts', {
      views: {
          // 模板内容会被安插在根路由模板(index.html)的匿名视图下
          '@': {
              abstract: true,
              url: '/contacts',
              templateUrl: 'app/contacts/contacts.html'
          }
      }
});

$stateProvider
  .state('contacts', {
    // 根状态,对应的父模板则是index.html
    // 所以 contacts.html 将被加载到 index.html 中未命名的 ui-view 中
    templateUrl: 'contacts.html'   
  })
  .state('contacts.detail', {
    views: {
        // 嵌套状态,对应的父模板是 contacts.html
        // 相对命名
        // contacts.html 中 <div ui-view='detail'/> 将对应下面
        "detail" : { },   
         
        // 相对命名
        // 对应 contacts.html 中的未命名 ui-view <div ui-view/>
        "" : { }, 
        // 绝对命名
        // 对应 contacts.detail.html 中 <div ui-view='info'/>
        "info@contacts.detail" : { }
        // 绝对命名
        // 对应 contacts.html 中 <div ui-view='detail'/>
        "detail@contacts" : { }
        // 绝对命名
        // 对应 contacts.html 中的未命名 ui-view <div ui-view/>
        "@contacts" : { }
        // 绝对命名
        // 对应 index.html 中 <div ui-view='status'/> 
        "status@" : { }
        // 绝对命名
        // 对应 index.html 中 <div ui-view/>
        "@" : { } 
  });

views Key

viewName + '@' + stateName

viewName
指的是ui-view="status"中的’status’
也可以是''(空字符串),因为会有匿名的ui-view或者ui-view=""

stateName
默认情况下是父路由的state.name,因为子路由模板一般都安插在父路由的ui-view中
也可以是''(空字符串),表示最顶层rootState
还可以是任意的祖先state.name

该模板将会被安插在名为stateName路由对应模板的viewName视图下

参考链接:
http://bubkoo.com/2014/01/02/angular/ui-router/guide/index/