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

Posted

技术标签:

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

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

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

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

【问题讨论】:

【参考方案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() 的结果包含 datastatusheadersconfig 对象,所以我们需要将 MyCtrl 的正文改为:

$scope.datasets = datasets.data;

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

【讨论】:

我正在尝试做这样的事情,但在注入“数据集”时遇到了麻烦,因为它没有定义。有什么想法吗? 嘿 mb21,我想你可以帮我解决这个问题:***.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","clas-s-rooms","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 对 依赖注入 友好(我什至将它结合使用 ngminuglify),而且它更优雅领域驱动 基于解决方案。

下面的示例注册了一个 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 属性是在路由上定义的。

EntitiesCtrlResolveEntityCtrlResolve 是与 EntitiesCtrlEntityCtrl 控制器在同一文件中定义的 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,在配置阶段由 $routeProviderresolve 解析。

我想在这里强调的是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>

我认为这个最省力。

【讨论】:

以上是关于延迟 AngularJS 路由更改直到模型加载以防止闪烁的主要内容,如果未能解决你的问题,请参考以下文章

angularJS使用ocLazyLoad实现js延迟加载

需要延迟项目更改信号直到我的数据加载

AngularJS:在不完全重新加载控制器的情况下更改哈希和路由

AngularJS UI 路由器 - 在不重新加载状态的情况下更改 url

使用 MEAN 堆栈延迟加载 (AngularJS 1.x)

由于事件导致模型更改后AngularJS不刷新视图