【问题标题】:Delaying AngularJS route change until model loaded to prevent flicker延迟 AngularJS 路由更改直到模型加载以防止闪烁
【发布时间】:2012-08-11 21:49:00
【问题描述】:

我想知道 AngularJS 是否有办法(类似于 Gmail)延迟显示新路线,直到使用其各自的服务获取每个模型及其数据之后。

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

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

【问题讨论】:

    标签: javascript angularjs angularjs-routing


    【解决方案1】:

    $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'});
    }]);
    

    注意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;
      }
    }
    

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

    resolve.phones 函数负责返回一个承诺。收集所有的 Promise 并延迟路由更改,直到所有的 Promise 都解决后。

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

    【讨论】:

    • @MiskoHevery - 如果您的控制器位于模块内部并且被定义为字符串而不是函数,该怎么办。你怎么能像你一样设置 resolve 属性?
    • 如何在angular.controller() 类型控制器定义中使用它?在$routeProvider 的东西中,我认为您必须使用控制器的字符串名称。
    • 任何使用 angular.controller() 和最新版本 AngularJS 的示例?
    • @blesh,当您使用angular.controller() 时,您可以将此函数的结果分配给一个变量(var MyCtrl = angular.controller(...)),然后进一步使用它(MyCtrl.loadData = function(){..})。查看 egghead 的视频,代码直接显示在那里:egghead.io/video/0uvAseNXDr0
    • 我仍然想要一个很好的方法,而不需要将你的控制器放在全局中。我不想到处乱扔全球垃圾。你可以用一个常量来做,但是如果能够将解析函数放在控制器上/控制器中,而不是其他地方,那就太好了。
    【解决方案2】:

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

    模板:

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

    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
        });
    });​
    ​
    

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

    精简版:

    由于 $http() 已经返回了一个 promise(又名 deferred),我们实际上不需要创建我们自己的。所以我们可以简化MyCtrl。解决:

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

    $http() 的结果包含 data、status、headers 和 config 对象,所以我们需要将 MyCtrl 的正文改为:

    $scope.datasets = datasets.data;
    

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

    【讨论】:

    • 我正在尝试做这样的事情,但在注入“数据集”时遇到了麻烦,因为它没有定义。有什么想法吗?
    • 嘿 mb21,我想你可以帮我解决这个问题:stackoverflow.com/questions/14271713/…
    • 有人可以帮我将此答案转换为 app.controller('MyCtrl') 格式吗? jsfiddle.net/5usya/1 对我不起作用。
    • 我收到一个错误:Unknown provider: datasetsProvider &lt;- datasets
    • 您可以通过将数据集替换为以下内容来简化答案:function($http) { return $http({method: 'GET', url: '/someUrl'}) .then( function(data){ return data;}, function(reason){return 'error value';} ); }
    【解决方案3】:

    我看到有人问如何使用 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';
    }]);
    

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

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

    如果你感兴趣,我决定也粘贴我自己的代码(用咖啡脚本编写),这样你就可以看到它是如何工作的。

    仅供参考,我提前使用了一个通用控制器来帮助我在几个模型上进行 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 ....
        ]
    

    【讨论】:

    • 我是否从您的示例和我的失败尝试中正确推断出,在最新版本的 Angular 中,现在无法在控制器中引用 resolve 函数?所以它必须像这里一样在配置中声明?
    • @XMLilley 我很确定是这样。我相信这个例子是我写的时候从 1.1.2 开始的。我没有看到任何关于将解析放入控制器的文档
    • 酷,谢谢。在 SO 上有很多这样做的例子(比如这里的前两个),但它们都来自 2012 年和 2013 年初。这是一种优雅的方法,但似乎已被弃用。现在最干净的选择似乎是编写作为承诺对象的单个服务。
    • 感谢这对我有用。对于遇到关于 undefined $defer 服务错误的任何其他人,请注意在 AngularJS 的 1.5.7 版本中,您想改用 $timeout。
    【解决方案4】:

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

    $routeProvider

    resolve: {
        data: function(Resource) {
            return Resource.get().$promise;
        }
    }
    

    控制器

    app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {
    
        $scope.data = data;
    
    }]);
    

    【讨论】:

    • 请问哪些版本包含该提交?
    • 最新的不稳定版本(1.1.5)包含这个提交。 ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js
    • 我喜欢这种不那么冗长的方法。从实际的数据对象创建一个 Promise 并直接传递它会很好,但代码太少了,它工作得很好。
    • 资源如何访问 $routeParams?例如:在GET '/api/1/apps/:appId' --> App.get({id: $routeParams.appId}).$promise(); 我不能这样使用
    • @zeronone 你注入$route 到你的解决方案并使用$route.current.params。小心,$routeParams 仍然指向旧路线。
    【解决方案5】:

    这个 sn-p 对 依赖注入 友好(我什至将它结合使用 ngmin 和 uglify),而且它更优雅领域驱动 基于解决方案。

    下面的示例注册了一个 Phone resource 和一个 constant phoneRoutes,其中包含您的所有路由信息那个(电话)域。在提供的答案中我不喜欢的是 resolve 逻辑的位置 - main 模块不应该知道 任何事情或被打扰关于资源参数提供给控制器的方式。这样逻辑就保持在同一个域中。

    注意:如果您使用 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;
            }]
          }
        }
      });
    

    下一部分是在模块处于配置状态时注入路由数据并将其应用到 $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: '/' });
    
    });
    

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

    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);
          });
        });
      });
    });
    

    您可以在my latest (upcoming) experiment 中看到它的全部荣耀。 虽然这种方法对我来说很好,但我真的很想知道为什么 $injector 在检测到 anything 的注入是 promiseanything 的构造/强>对象;它会让事情变得更简单。

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

    【讨论】:

    • 这个出色的答案似乎更符合“Angular”哲学(封装等)。我们都应该有意识地努力阻止逻辑像葛一样在代码库中蔓延。
    • I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object 我猜他们省略了这个功能,因为它可能会鼓励对应用程序的响应能力产生负面影响的设计模式。他们心目中的理想应用是真正异步的应用,因此解析应该是一个边缘案例。
    【解决方案6】:

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

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

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

    <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>
    

    【讨论】:

    • 也许将 HTTP 状态暴露给视图是不对的,就像处理 CSS 类和 DOM 元素属于控制器一样。我可能会使用相同的想法,但在 isValid() 和 isLoaded() 中抽象状态。
    • 这确实不是最好的关注点分离,而且如果你有依赖于特定对象的嵌套控制器,它会崩溃。
    • 这很聪明.. 至于将状态代码暴露给视图,您可以将 http 逻辑粘贴到控制器的范围属性中,然后绑定到它们。此外,如果您在后台进行多个 ajax 调用,无论如何您都希望这样做。
    • 如果问题只是延迟查看问题,那就没问题了。但是如果您需要延迟控制器的实例化——而不仅仅是视图,最好使用 resolve。 (例如:如果您需要确保您的 JSON 已加载,因为您的控制器在连接之前将其传递给指令。)来自文档:“路由器将等待它们全部被解析或一个被拒绝 before控制器被实例化".
    【解决方案7】:

    我在上面的 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;
      }
    }
    

    【讨论】:

      【解决方案8】:

      使用 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;
      }
      

      更新:

      phones: function(Phone) {
          return Phone.query().$promise;
      }
      

      感谢 Angular 团队和贡献者的帮助,时间缩短了很多。 :)

      这也是马克西米利安霍夫曼的答案。显然,该提交已进入 1.1.5。

      【讨论】:

      • 我在the docs 中似乎找不到任何关于$promise 的信息。从 v2.0+ 起,它可能已被删除。
      • 仅在 1.2 中可用
      【解决方案9】:

      您可以使用$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'});
      }]);
      

      注意resolve 属性是在路由上定义的。

      EntitiesCtrlResolve 和 EntityCtrlResolve 是与 EntitiesCtrl 和 EntityCtrl 控制器在同一文件中定义的 constant 对象。

      // 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..
      });
      

      【讨论】:

        【解决方案10】:

        我喜欢 darkporter 的想法,因为它对于刚接触 AngularJS 的开发团队来说很容易理解并立即开始工作。

        我创建了这个适配,它使用 2 个 div,一个用于加载栏,另一个用于加载数据后显示的实际内容。错误处理将在其他地方进行。

        为 $scope 添加一个“就绪”标志:

        $http({method: 'GET', url: '...'}).
            success(function(data, status, headers, config) {
                $scope.dataForView = data;      
                $scope.ready = true;  // <-- set true after loaded
            })
        });
        

        在 html 视图中:

        <div ng-show="!ready">
        
            <!-- Show loading graphic, e.g. Twitter Boostrap progress bar -->
            <div class="progress progress-striped active">
                <div class="bar" style="width: 100%;"></div>
            </div>
        
        </div>
        
        <div ng-show="ready">
        
            <!-- Real content goes here and will appear after loading -->
        
        </div>
        

        另请参阅:Boostrap progress bar docs

        【讨论】:

        • 如果您要加载多条数据,则有点崩溃。你怎么知道所有东西都加载了?
        • 自从我在 2 月添加此答案以来,事情已经发生了变化,此页面上有更多活动。看起来 Angular 现在有比这里建议的更好的支持来解决这个问题。干杯,
        • 我来的有点晚,但处理多条数据并不是什么大问题。您只需为每个请求使用单独的变量(布尔值:isReadyData1、isReadyData2 等),并设置 $scope.ready = isReadyData1 && isReadyData2 ...;很适合我。
        【解决方案11】:

        我喜欢上面的答案,并从中学到了很多,但上面的大多数答案都缺少一些东西。

        我被困在一个类似的场景中,我正在使用从服务器的第一个请求中获取的一些数据来解析 url。 我面临的问题是,如果承诺是 rejected。

        我使用了一个自定义提供程序,该提供程序用于返回一个 Promise,在配置阶段由 $routeProvider 的 resolve 解析。

        我想在这里强调的是when 的概念,它做了这样的事情。

        它会在 url 栏中看到 url,然后在被调用的控制器中看到相应的 when 块,并且视图到目前为止被引用得很好。

        假设我有以下配置阶段代码。

        App.when('/', {
           templateUrl: '/assets/campaigns/index.html',
           controller: 'CampaignListCtr',
           resolve : {
              Auth : function(){
                 return AuthServiceProvider.auth('campaign');
              }
           }
        })
        // Default route
        .otherwise({
           redirectTo: '/segments'
        });
        

        在浏览器的第一个运行块的根 url 上调用,否则调用 otherwise。

        让我们想象一个场景,我在地址栏中点击了 rootUrl AuthServicePrivider.auth() 函数被调用。

        假设返回的 Promise 处于 reject 状态然后呢???

        根本没有渲染任何东西。

        Otherwise 块不会被执行,因为它对于任何未在配置块中定义且 angularJs 配置阶段未知的 url。

        当这个promise没有解决时,我们将不得不处理被触发的事件。失败时$routeChangeErorr 被$rootScope 解雇。

        如下代码所示可以捕获。

        $rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
            // Use params in redirection logic.
            // event is the routeChangeEvent
            // current is the current url
            // previous is the previous url
            $location.path($rootScope.rootPath);
        });
        

        IMO 通常将事件跟踪代码放在应用程序的运行块中是个好主意。此代码在应用程序的配置阶段之后运行。

        App.run(['$routeParams', '$rootScope', '$location', function($routeParams, $rootScope, $location){
           $rootScope.rootPath = "my custom path";
           // Event to listen to all the routeChangeErrors raised
           // by the resolve in config part of application
           $rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
               // I am redirecting to rootPath I have set above.
               $location.path($rootScope.rootPath);
           });
        }]);
        

        这样我们可以在配置阶段处理promise失败。

        【讨论】:

          【解决方案12】:

          我有一个复杂的多级滑动面板界面,屏幕层被禁用。在禁用屏幕层上创建指令,该指令将创建单击事件以执行状态,如

          $state.go('account.stream.social.view');
          

          正在产生闪烁效果。 history.back() 而不是它工作正常,但在我的情况下它并不总是回到历史。所以我发现如果我只是在我的禁用屏幕上创建属性 href 而不是 state.go ,就像一个魅力。

          <a class="disable-screen" back></a>
          

          指令'返回'

          app.directive('back', [ '$rootScope', function($rootScope) {
          
              return {
                  restrict : 'A',
                  link : function(scope, element, attrs) {
                      element.attr('href', $rootScope.previousState.replace(/\./gi, '/'));
                  }
              };
          
          } ]);
          

          app.js 我只是保存之前的状态

          app.run(function($rootScope, $state) {      
          
              $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {         
          
                  $rootScope.previousState = fromState.name;
                  $rootScope.currentState = toState.name;
          
          
              });
          });
          

          【讨论】:

            【解决方案13】:

            一种可能的解决方案可能是将 ng-cloak 指令与我们使用模型的元素一起使用,例如

            <div ng-cloak="">
              Value in  myModel is: {{myModel}}
            </div>
            

            我认为这个最省力。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-08-21
              • 2014-03-15
              • 1970-01-01
              • 2011-11-16
              相关资源
              最近更新 更多