延迟AngularJS路由更改,直到加载模型以防止闪烁

Mis*_*ery 319 javascript angularjs angularjs-routing

我想知道是否有一种方法(类似于Gmail)AngularJS 延迟显示新路由,直到每个模型及其数据已使用其各自的服务获取.

例如,如果ProjectsController列出了所有项目以及project_index.html显示这些项目的模板,Project.query()则会在显示新页面之前完全获取.

在此之前,旧页面仍将继续显示(例如,如果我正在浏览另一个页面,然后决定查看此项目索引).

Mis*_*ery 374

$ routeProvider resolve属性允许延迟路由更改,直到加载数据.

首先使用这样的resolve属性定义路由.

angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html', 
        controller: PhoneListCtrl, 
        resolve: PhoneListCtrl.resolve}).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html', 
        controller: PhoneDetailCtrl, 
        resolve: PhoneDetailCtrl.resolve}).
      otherwise({redirectTo: '/phones'});
}]);
Run Code Online (Sandbox Code Playgroud)

注意该resolve属性是在路线上定义的.

function PhoneListCtrl($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}

PhoneListCtrl.resolve = {
  phones: function(Phone, $q) {
    // see: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4
    var deferred = $q.defer();
    Phone.query(function(successData) {
            deferred.resolve(successData); 
    }, function(errorData) {
            deferred.reject(); // you could optionally pass error data here
    });
    return deferred.promise;
  },
  delay: function($q, $defer) {
    var delay = $q.defer();
    $defer(delay.resolve, 1000);
    return delay.promise;
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,控制器定义包含一个解析对象,该对象声明了控制器构造函数可用的内容.这里phones注入控制器并在resolve属性中定义.

resolve.phones函数负责返回一个promise.收集所有承诺并延迟路线更改,直到所有承诺得到解决.

工作演示:http://mhevery.github.com/angular-phonecat/app/#/phones 资料来源:https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831

  • 如何在`angular.controller()`类型控制器定义中使用它?在`$ routeProvider`中,我认为你必须使用控制器的字符串名称. (53认同)
  • @blesh,当你使用`angular.controller()`时,你可以将这个函数的结果赋给变量(`var MyCtrl = angular.controller(...)`),然后再用它(`MyCtrl.loadData = function(){..}`).查看egghead的视频,代码会立即显示在那里:http://www.egghead.io/video/0uvAseNXDr0 (22认同)
  • 如果没有将控制器放在全局中,我仍然会喜欢一种很好的方法.我不想乱扔遍布全球的人.你可以使用常量来完成它,但能够将解析函数放在控制器上/而不是其他地方会很好. (17认同)
  • @MiskoHevery - 如果您的控制器位于模块内并被定义为字符串而不是函数,该怎么办?你怎么能像你一样设置resolve属性? (10认同)
  • 使用angular.controller()和最新版AngularJS的任何例子? (6认同)
  • @MiskoHevery谢谢,但我无法完全理解为什么resolve.phones会返回一个承诺.据我所知[文档](http://docs.angularjs.org/api/ngResource.$resource#Returns),Phone.query()返回空数组,当从中返回数据时将填充实际数据服务器. (3认同)
  • 将其分配给app.controller结果是不对的,因为结果返回相同的对象.`a = angular.module('a',[]); a.controller(function(){})== a.controller(function(){})`因此它不适用于多个控制器. (3认同)
  • @beret好点,我想因为提升和控制器将成为参考类型的事实,这就是你必须要做的所有事情.这是显而易见的,我在问上述问题时感到愚蠢.给你指出明显的+1.哈哈. (2认同)

mb2*_*b21 51

这是一个适用于Angular 1.0.2的最小工作示例

模板:

<script type="text/ng-template" id="/editor-tpl.html">
    Editor Template {{datasets}}
</script>

<div ng-view>

</div>
Run Code Online (Sandbox Code Playgroud)

JavaScript的:

function MyCtrl($scope, datasets) {    
    $scope.datasets = datasets;
}

MyCtrl.resolve = {
    datasets : function($q, $http) {
        var deferred = $q.defer();

        $http({method: 'GET', url: '/someUrl'})
            .success(function(data) {
                deferred.resolve(data)
            })
            .error(function(data){
                //actually you'd want deffered.reject(data) here
                //but to show what would happen on success..
                deferred.resolve("error value");
            });

        return deferred.promise;
    }
};

var myApp = angular.module('myApp', [], function($routeProvider) {
    $routeProvider.when('/', {
        templateUrl: '/editor-tpl.html',
        controller: MyCtrl,
        resolve: MyCtrl.resolve
    });
});?
?
Run Code Online (Sandbox Code Playgroud)

http://jsfiddle.net/dTJ9N/3/

精简版:

由于$ http()已经返回一个promise(也称为deferred),我们实际上不需要创建自己的.所以我们可以简化MyCtrl.决心:

MyCtrl.resolve = {
    datasets : function($http) {
        return $http({
            method: 'GET', 
            url: 'http://fiddle.jshell.net/'
        });
    }
};
Run Code Online (Sandbox Code Playgroud)

$ http()的结果包含数据,状态,标题配置对象,因此我们需要将MyCtrl的主体更改为:

$scope.datasets = datasets.data;
Run Code Online (Sandbox Code Playgroud)

http://jsfiddle.net/dTJ9N/5/


bit*_*wit 32

我看到有些人在使用带有缩小友好依赖注入的angular.controller方法时询问如何执行此操作.由于我刚开始工作,我觉得有必要回来帮忙.这是我的解决方案(从原始问题和Misko的答案中采用):

angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html', 
        controller: PhoneListCtrl, 
        resolve: { 
            phones: ["Phone", "$q", function(Phone, $q) {
                var deferred = $q.defer();
                Phone.query(function(successData) {
                  deferred.resolve(successData); 
                }, function(errorData) {
                  deferred.reject(); // you could optionally pass error data here
                });
                return deferred.promise;
             ]
            },
            delay: ["$q","$defer", function($q, $defer) {
               var delay = $q.defer();
               $defer(delay.resolve, 1000);
               return delay.promise;
              }
            ]
        },

        }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html', 
        controller: PhoneDetailCtrl, 
        resolve: PhoneDetailCtrl.resolve}).
      otherwise({redirectTo: '/phones'});
}]);

angular.controller("PhoneListCtrl", [ "$scope", "phones", ($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}]);
Run Code Online (Sandbox Code Playgroud)

由于此代码源自问题/最受欢迎的答案,因此未经测试,但如果您已经了解如何制作缩小友好的角度代码,它应该向您发送正确的方向.我自己的代码不需要的那一部分是在"手机"的解析功能中注入"电话",我根本也没有使用任何"延迟"对象.

我也推荐这个youtube视频http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp,这对我帮助很大

如果您感兴趣的话,我决定也粘贴我自己的代码(写在coffeescript中),这样你就可以看到我是如何工作的.

仅供参考,我提前使用通用控制器来帮助我在几个型号上进行CRUD:

appModule.config ['$routeProvider', ($routeProvider) ->
  genericControllers = ["boards","teachers","classrooms","students"]
  for controllerName in genericControllers
    $routeProvider
      .when "/#{controllerName}/",
        action: 'confirmLogin'
        controller: 'GenericController'
        controllerName: controllerName
        templateUrl: "/static/templates/#{controllerName}.html"
        resolve:
          items : ["$q", "$route", "$http", ($q, $route, $http) ->
             deferred = $q.defer()
             controllerName = $route.current.controllerName
             $http(
               method: "GET"
               url: "/api/#{controllerName}/"
             )
             .success (response) ->
               deferred.resolve(response.payload)
             .error (response) ->
               deferred.reject(response.message)

             return deferred.promise
          ]

  $routeProvider
    .otherwise
      redirectTo: '/'
      action: 'checkStatus'
]

appModule.controller "GenericController", ["$scope", "$route", "$http", "$cookies", "items", ($scope, $route, $http, $cookies, items) ->

  $scope.items = items
      #etc ....
    ]
Run Code Online (Sandbox Code Playgroud)

  • 很酷,谢谢.在SO上有很多这样做的例子(比如这里的前两个),但它们都来自2012年和2013年初.这是一种优雅的方法,但似乎已被弃用.最干净的替代方案现在似乎是编写作为承诺对象的单个服务. (2认同)

Max*_*ann 18

此提交是版本1.1.5及更高版本的一部分,它公开了$promise对象$resource.包含此提交的ngResource版本允许解析如下资源:

$ routeProvider

resolve: {
    data: function(Resource) {
        return Resource.get().$promise;
    }
}
Run Code Online (Sandbox Code Playgroud)

调节器

app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {

    $scope.data = data;

}]);
Run Code Online (Sandbox Code Playgroud)

  • @zeronone你为你的决心注入`$ route`并使用`$ route.current.params`.小心,`$ routeParams`仍指向旧路线. (2认同)

nul*_*ull 16

这个片段是依赖注入友好(我甚至用它在组合ngmin丑化),这是一个更优雅的领域驱动基础的解决方案.

以下示例注册Phone 资源常量 phoneRoutes,其中包含该(电话)域的所有路由信息.在提供的答案中我不喜欢的是解析逻辑的位置- 模块不应该知道任何事情或者对资源参数提供给控制器的方式感到困扰.这样逻辑就会停留在同一个域中.

注意:如果你正在使用ngmin(如果你不是:你应该),你只需要用DI数组约定来编写解析函数.

angular.module('myApp').factory('Phone',function ($resource) {
  return $resource('/api/phone/:id', {id: '@id'});
}).constant('phoneRoutes', {
    '/phone': {
      templateUrl: 'app/phone/index.tmpl.html',
      controller: 'PhoneIndexController'
    },
    '/phone/create': {
      templateUrl: 'app/phone/edit.tmpl.html',
      controller: 'PhoneEditController',
      resolve: {
        phone: ['$route', 'Phone', function ($route, Phone) {
          return new Phone();
        }]
      }
    },
    '/phone/edit/:id': {
      templateUrl: 'app/phone/edit.tmpl.html',
      controller: 'PhoneEditController',
      resolve: {
        form: ['$route', 'Phone', function ($route, Phone) {
          return Phone.get({ id: $route.current.params.id }).$promise;
        }]
      }
    }
  });
Run Code Online (Sandbox Code Playgroud)

下一部分是在模块处于配置状态时注入路由数据并将其应用于$ routeProvider.

angular.module('myApp').config(function ($routeProvider, 
                                         phoneRoutes, 
                                         /* ... otherRoutes ... */) {

  $routeProvider.when('/', { templateUrl: 'app/main/index.tmpl.html' });

  // Loop through all paths provided by the injected route data.

  angular.forEach(phoneRoutes, function(routeData, path) {
    $routeProvider.when(path, routeData);
  });

  $routeProvider.otherwise({ redirectTo: '/' });

});
Run Code Online (Sandbox Code Playgroud)

使用此设置测试路由配置也非常简单:

describe('phoneRoutes', function() {

  it('should match route configuration', function() {

    module('myApp');

    // Mock the Phone resource
    function PhoneMock() {}
    PhoneMock.get = function() { return {}; };

    module(function($provide) {
      $provide.value('Phone', FormMock);
    });

    inject(function($route, $location, $rootScope, phoneRoutes) {
      angular.forEach(phoneRoutes, function (routeData, path) {

        $location.path(path);
        $rootScope.$digest();

        expect($route.current.templateUrl).toBe(routeData.templateUrl);
        expect($route.current.controller).toBe(routeData.controller);
      });
    });
  });
});
Run Code Online (Sandbox Code Playgroud)

我最近(即将开始)的实验中,你可以看到它的全部荣耀.虽然这种方法对我来说很好,但我真的很想知道为什么$ injector注入器在检测到任何作为promise对象的注入时不会延迟构造任何东西.它会让事情变得更加容易.

编辑:使用Angular v1.2(rc2)

  • 这个出色的答案似乎更符合"Angular"哲学(封装等).我们都应该有意识地努力阻止逻辑在像kudzu这样的代码库中蔓延. (2认同)

jps*_*ons 11

延迟显示路线肯定会导致异步纠结......为什么不简单地跟踪主实体的加载状态并在视图中使用它.例如,在您的控制器中,您可以在ngResource上同时使用成功和错误回调:

$scope.httpStatus = 0; // in progress
$scope.projects = $resource.query('/projects', function() {
    $scope.httpStatus = 200;
  }, function(response) {
    $scope.httpStatus = response.status;
  });
Run Code Online (Sandbox Code Playgroud)

然后在视图中你可以做任何事情:

<div ng-show="httpStatus == 0">
    Loading
</div>
<div ng-show="httpStatus == 200">
    Real stuff
    <div ng-repeat="project in projects">
         ...
    </div>
</div>
<div ng-show="httpStatus >= 400">
    Error, not found, etc. Could distinguish 4xx not found from 
    5xx server error even.
</div>
Run Code Online (Sandbox Code Playgroud)

  • 也许将HTTP状态暴露给视图是不对的,不仅仅是处理CSS类和DOM元素属于控制器.我可能会使用相同的想法,但在isValid()和isLoaded()中使用抽象状态. (6认同)

Jus*_*ten 8

我从上面的Misko代码开始工作,这就是我用它做的.这是一个更新的解决方案,因为$defer已经改为$timeout.$timeout然而,替换将等待超时期限(在Misko的代码中,1秒),然后返回数据,希望它及时得到解决.通过这种方式,它尽快返回.

function PhoneListCtrl($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}

PhoneListCtrl.resolve = {

  phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
  }
}
Run Code Online (Sandbox Code Playgroud)


OJ *_*eño 7

使用AngularJS 1.1.5

使用AngularJS 1.1.5语法更新Justen答案中的"手机"功能.

原版的:

phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
}
Run Code Online (Sandbox Code Playgroud)

更新:

phones: function(Phone) {
    return Phone.query().$promise;
}
Run Code Online (Sandbox Code Playgroud)

由于Angular团队和贡献者,所以要短得多.:)

这也是Maximilian Hoffmann的答案.显然,提交使其成为1.1.5.


Boh*_*ets 5

您可以使用$ routeProvider resolve属性来延迟路由更改,直到加载数据.

angular.module('app', ['ngRoute']).
  config(['$routeProvider', function($routeProvider, EntitiesCtrlResolve, EntityCtrlResolve) {
    $routeProvider.
      when('/entities', {
        templateUrl: 'entities.html', 
        controller: 'EntitiesCtrl', 
        resolve: EntitiesCtrlResolve
      }).
      when('/entity/:entityId', {
        templateUrl: 'entity.html', 
        controller: 'EntityCtrl', 
        resolve: EntityCtrlResolve
      }).
      otherwise({redirectTo: '/entities'});
}]);
Run Code Online (Sandbox Code Playgroud)

请注意,该resolve属性是在路由上定义的.

EntitiesCtrlResolve并且EntityCtrlResolve是与控制器在同一文件中定义的常量对象.EntitiesCtrlEntityCtrl

// EntitiesCtrl.js

angular.module('app').constant('EntitiesCtrlResolve', {
  Entities: function(EntitiesService) {
    return EntitiesService.getAll();
  }
});

angular.module('app').controller('EntitiesCtrl', function(Entities) {
  $scope.entities = Entities;

  // some code..
});

// EntityCtrl.js

angular.module('app').constant('EntityCtrlResolve', {
  Entity: function($route, EntitiesService) {
    return EntitiesService.getById($route.current.params.projectId);
  }
});

angular.module('app').controller('EntityCtrl', function(Entity) {
  $scope.entity = Entity;

  // some code..
});
Run Code Online (Sandbox Code Playgroud)