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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/11972026/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-24 07:53:50  来源:igfitidea点击:

Delaying AngularJS route change until model loaded to prevent flicker

javascriptangularjsangularjs-routing

提问by Misko Hevery

I am wondering if there is a way (similar to Gmail) for AngularJS to delay showing a new route until after each model and its data has been fetchedusing its respective services.

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

For example, if there were a ProjectsControllerthat listed all Projects and project_index.htmlwhich was the template that showed these Projects, Project.query()would be fetched completely before showing the new page.

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

Until then, the old page would still continue to show (for example, if I were browsing another page and then decided to see this Project index).

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

回答by Misko Hevery

$routeProviderresolveproperty allows delaying of route change until data is loaded.

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

First define a route with resolveattribute like this.

首先定义一个具有这样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'});
}]);

notice that the resolveproperty is defined on route.

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

Notice that the controller definition contains a resolve object which declares things which should be available to the controller constructor. Here the phonesis injected into the controller and it is defined in the resolveproperty.

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

The resolve.phonesfunction is responsible for returning a promise. All of the promises are collected and the route change is delayed until after all of the promises are resolved.

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

Working demo: http://mhevery.github.com/angular-phonecat/app/#/phonesSource: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831

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

回答by mb21

Here's a minimal working example which works for Angular 1.0.2

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

Template:

模板:

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

<div ng-view>

</div>

JavaScript:

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://jsfiddle.net/dTJ9N/3/

Streamlined version:

精简版:

Since $http() already returns a promise (aka deferred), we actually don't need to create our own. So we can simplify MyCtrl. resolve to:

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

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

The result of $http() contains data, status, headersand configobjects, so we need to change the body of MyCtrl to:

$http() 的结果包含datastatusheadersconfig对象,因此我们需要将 MyCtrl 的主体更改为:

$scope.datasets = datasets.data;

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

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

回答by bitwit

I see some people asking how to do this using the angular.controller method with minification friendly dependency injection. Since I just got this working I felt obliged to come back and help. Here's my solution (adopted from the original question and Misko's answer):

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

Since this code is derived from the question/most popular answer it is untested, but it should send you in the right direction if you already understand how to make minification friendly angular code. The one part that my own code didn't requires was an injection of "Phone" into the resolve function for 'phones', nor did I use any 'delay' object at all.

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

I also recommend this youtube video http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp, which helped me quite a bit

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

Should it interest you I've decided to also paste my own code (Written in coffeescript) so you can see how I got it working.

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

FYI, in advance I use a generic controller that helps me do CRUD on several models:

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

回答by Maximilian Hoffmann

This commit, which is part of version 1.1.5 and above, exposes the $promiseobject of $resource. Versions of ngResource including this commit allow resolving resources like this:

此提交,这是1.1.5版的一部分以上,暴露$promise的物体$resource。包含此提交的 ngResource 版本允许解析如下资源:

$routeProvider

$routeProvider

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

controller

控制器

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

    $scope.data = data;

}]);

回答by null

This snippet is dependency injectionfriendly (I even use it in combination of ngminand uglify) and it's a more elegant domain drivenbased solution.

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

The example below registers a Phoneresourceand a constantphoneRoutes, which contains all your routing information for that (phone) domain. Something I didn't like in the provided answer was the location of the resolvelogic -- the mainmodule should not knowanything or be bothered about the way the resource arguments are provided to the controller. This way the logic stays in the same domain.

下面的示例注册了一个Phone资源和一个常量phoneRoutes,其中包含该(电话)域的所有路由信息。在提供的答案中,我不喜欢解析逻辑的位置——模块不应该知道任何事情,也不应该对向控制器提供资源参数的方式感到困扰。这样,逻辑就保持在同一个域中。

Note: if you're using ngmin(and if you're not: you should) you only have to write the resolve functions with the DI array convention.

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

The next piece is injecting the routing data when the module is in the configure state and applying it to the $routeProvider.

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

});

Testing the route configuration with this setup is also pretty easy:

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

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

You can see it in full glory in my latest (upcoming) experiment. Although this method works fine for me, I really wonder why the $injector isn't delaying construction of anythingwhen it detects injection of anythingthat is a promiseobject; it would make things soooOOOOOooOOOOO much easier.

你可以在我最新的(即将进行的)实验中看到它的盛况。尽管这种方法能正常工作对我来说,我真的不知道为什么$喷油器不误事的建设什么当它检测到的注射什么这是一个承诺的对象; 它会让事情变得更容易。

Edit: used Angular v1.2(rc2)

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

回答by jpsimons

Delaying showing the route is sure to lead to an asynchronous tangle... why not simply track the loading status of your main entity and use that in the view. For example in your controller you might use both the success and error callbacks on ngResource:

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

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

Then in the view you could do whatever:

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

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

回答by Justen

I worked from Misko's code above and this is what I've done with it. This is a more current solution since $deferhas been changed to $timeout. Substituting $timeouthowever will wait for the timeout period (in Misko's code, 1 second), then return the data hoping it's resolved in time. With this way, it returns asap.

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

回答by OJ Raque?o

Using AngularJS 1.1.5

使用 AngularJS 1.1.5

Updating the 'phones' function in Justen's answer using AngularJS 1.1.5syntax.

使用AngularJS 1.1.5语法更新 Justen 回答中的“电话”功能。

Original:

原来的:

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

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

    return deferred.promise;
}

Updated:

更新:

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

Much shorter thanks to the Angular team and contributors. :)

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

This is also the answer of Maximilian Hoffmann. Apparently that commit made it into 1.1.5.

这也是马克西米利安霍夫曼的答案。显然,该提交使其成为 1.1.5。

回答by Bohdan Lyzanets

You can use $routeProvider resolveproperty to delay route change until data is loaded.

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

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

Notice that the resolveproperty is defined on route.

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

EntitiesCtrlResolveand EntityCtrlResolveis constantobjects defined in same file as EntitiesCtrland EntityCtrlcontrollers.

EntitiesCtrlResolveandEntityCtrlResolve是在与和控制器相同的文件中定义的常量对象。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..
});

回答by reggoodwin

I like darkporter's idea because it will be easy for a dev team new to AngularJS to understand and worked straight away.

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

I created this adaptation which uses 2 divs, one for loader bar and another for actual content displayed after data is loaded. Error handling would be done elsewhere.

我创建了这个改编版,它使用 2 个 div,一个用于加载栏,另一个用于加载数据后显示的实际内容。错误处理将在别处完成。

Add a 'ready' flag to $scope:

向 $scope 添加“就绪”标志:

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

In html view:

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

See also: Boostrap progress bar docs

另请参阅:Boostrap 进度条文档