停止 angular-ui-router 导航,直到 promise 解决

Posted

技术标签:

【中文标题】停止 angular-ui-router 导航,直到 promise 解决【英文标题】:stop angular-ui-router navigation until promise is resolved 【发布时间】:2013-12-04 08:20:00 【问题描述】:

我想防止在 Rails 设计超时发生时发生一些闪烁,但 Angular 直到资源的下一个授权错误才知道。

发生的情况是模板被渲染,一些对资源的ajax调用发生,然后我们被重定向到rails devise登录。我宁愿在每次状态更改时对 rails 执行 ping 操作,如果 rails 会话已过期,那么我将在模板渲染之前立即重定向。

ui-router 具有可以放在每条路由上的解析,但它看起来一点也不干。

我拥有的是这个。但是在状态已经转换之前,promise 不会得到解决。

$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams)
        //check that user is logged in
        $http.get('/api/ping').success(function(data)
          if (data.signed_in) 
            $scope.signedIn = true;
           else 
            window.location.href = '/rails/devise/login_path'
          
        )

    );

如何在渲染新模板之前根据 promise 的结果中断状态转换?

【问题讨论】:

您可能希望创建一个服务,通过它您可以进行承诺调用并在您的控制器中调用此服务。 可以在路由(状态)配置中使用resolve。控制器和模板在完成之前不会加载 @AdityaSethi,在控制器中执行代码将为时已晚,因为 ui-router 状态已更改,模板已呈现并且不知道何时履行承诺。 @charlietfl,是的,我在最初的问题中提到了解决。那会起作用,但对我来说它根本不是 DRY(不要重复自己)。我将不得不在文件膨胀的每一条路线上都解决。 嗯,你可以有一个父抽象路由,其中​​有一个resolve。它将在子状态启动并保持 DRYability 之前解决。 【参考方案1】:

相信你在找event.preventDefault()

注意:使用 event.preventDefault() 来防止发生过渡。

$scope.$on('$stateChangeStart', 
function(event, toState, toParams, fromState, fromParams) 
        event.preventDefault(); 
        // transitionTo() promise will be rejected with 
        // a 'transition prevented' error
)

虽然我可能会按照@charlietfl 的建议在状态配置中使用resolve

编辑

所以我有机会在状态更改事件中使用 preventDefault(),这就是我所做的:

.run(function($rootScope,$state,$timeout) 

$rootScope.$on('$stateChangeStart',
    function(event, toState, toParams, fromState, fromParams)

        // check if user is set
        if(!$rootScope.u_id && toState.name !== 'signin')  
            event.preventDefault();

            // if not delayed you will get race conditions as $apply is in progress
            $timeout(function()
                event.currentScope.$apply(function() 
                    $state.go("signin")
                );
            ,300)
         else 
            // do smth else
        
    
)


编辑

Newer documentation 包含一个示例,说明在调用preventDefault 后用户应该如何使用sync() 继续,但如果使用$locationChangeSuccess 事件,这对我和评论者不起作用,而是使用$stateChangeStart 作为在下面的示例中,取自带有更新事件的文档:

angular.module('app', ['ui.router'])
    .run(function($rootScope, $urlRouter) 
        $rootScope.$on('$stateChangeStart', function(evt) 
            // Halt state change from even starting
            evt.preventDefault();
            // Perform custom logic
            var meetsRequirement = ...
            // Continue with the update and state transition if logic allows
            if (meetsRequirement) $urlRouter.sync();
        );
    );

【讨论】:

event.preventDefault 确实停止了 $stateChangeStart,但是如何根据 $http 成功条件重新启动它? 此问题中涉及文档的最后一次编辑并未指出它使用的是 $locationChangeSuccess 而不是问题所要求的 $stateChangeStart。前者无法访问 toParams 和 toState,这对于身份验证逻辑可能至关重要。此外,带有 urlRouter.sync() 的 $locationChangeSuccess 对我不起作用,$locationChangeStart 对我有用,但仍然不使用 $state。 @CarbonDry 我确实更新了上次编辑,是的,没错,文档中的示例实际上也对我不起作用,我之前确实盲目地复制了它。我确实使用$stateChangeStart @ivarPrudnikov 感谢更新。但是,您确定 $urlRouter.sync() 会恢复应用于 $stateChangeStart 的 event.preventDefault() 吗?当然 urlRouter 在位置级别上工作,而不是 ui-router 提供的状态。我自己尝试过,遇到了一些奇怪的行为,但没有奏效。 $urlRouter.sync() 可能与当前哈希片段同步,因此使用$locationChangeSuccess 的文档我相信在成功更新哈希后会被触发。尝试与$stateChangeStart 一起使用它不起作用,因为evt.preventDefault(); 阻止了哈希更新。【参考方案2】:

您可以从 $stateChangeStart 获取转换参数并将它们存储在服务中,然后在处理完登录后重新启动转换。如果您的安全来自服务器作为 http 401 错误,您也可以查看 https://github.com/witoldsz/angular-http-auth。

【讨论】:

【参考方案3】:

我遇到了同样的问题通过使用它解决了它。

angular.module('app', ['ui.router']).run(function($rootScope, $state) 
    yourpromise.then(function(resolvedVal)
        $rootScope.$on('$stateChangeStart', function(event)
           if(!resolvedVal.allow)
               event.preventDefault();
               $state.go('unauthState');
           
        )
    ).catch(function()
        $rootScope.$on('$stateChangeStart', function(event)
           event.preventDefault();
           $state.go('unauthState');
           //DO Something ELSE
        )

    );

【讨论】:

这肯定行不通,因为您的承诺是异步的并且可以在 stateChangeStart 之后被捕获?【参考方案4】:

由于 $urlRouter.sync() 不适用于 stateChangeStart,这里有一个替代方案:

    var bypass;
    $rootScope.$on('$stateChangeStart', function(event,toState,toParams) 
        if (bypass) return;
        event.preventDefault(); // Halt state change from even starting
        var meetsRequirement = ... // Perform custom logic
        if (meetsRequirement)   // Continue with the update and state transition if logic allows
            bypass = true;  // bypass next call
            $state.go(toState, toParams); // Continue with the initial state change
        
    );

【讨论】:

为此,您需要将 bypass 重置为 false 否则 next 状态转换也将绕过此验证。 @BenFoster 通常这种情况/代码在控制器上运行,在“满足要求”之后被销毁。因此无需专门重置旁路。否则 - 你是对的,需要绕过重置:if (bypass) bypass = false; return; 【参考方案5】:

这是我对这个问题的解决方案。它运作良好,并且符合此处其他一些答案的精神。它只是清理了一点。我在根范围上设置了一个名为“stateChangeBypass”的自定义变量,以防止无限循环。我还在检查状态是否为“登录”,如果是,则始终允许。

function ($rootScope, $state, Auth) 

    $rootScope.$on('$stateChangeStart', function (event, toState, toParams) 

        if($rootScope.stateChangeBypass || toState.name === 'login') 
            $rootScope.stateChangeBypass = false;
            return;
        

        event.preventDefault();

        Auth.getCurrentUser().then(function(user) 
            if (user) 
                $rootScope.stateChangeBypass = true;
                $state.go(toState, toParams);
             else 
                $state.go('login');
            
        );

    );

【讨论】:

请注意,“stateChangeBypass”标志非常重要,否则你会得到一个无限循环,因为 $stateChangeStart 将被一遍又一遍地调用。它不必在根范围内。可以使用外部函数内部的局部变量作为守卫。 另外我使用这种方法遇到了一些奇怪的无限循环条件,所以最终这样做了: 此解决方案有效,但由于 preventDefault();【参考方案6】:

为了在此处添加现有答案,我遇到了完全相同的问题;我们在根范围内使用事件处理程序来侦听$stateChangeStart 以进行我的权限处理。不幸的是,这有一个讨厌的副作用,偶尔会导致无限摘要(不知道为什么,代码不是我写的)。

我想出的解决方案,比较欠缺的是,总是event.preventDefault()阻止转换,然后判断用户是否通过异步调用登录。验证这一点后,然后使用$state.go 转换到新状态。不过,重要的是您将$state.go 中选项的notify 属性设置为false。这将防止状态转换触发另一个$stateChangeStart

 event.preventDefault();
 return authSvc.hasPermissionAsync(toState.data.permission)
    .then(function () 
      // notify: false prevents the event from being rebroadcast, this will prevent us
      // from having an infinite loop
      $state.go(toState, toParams,  notify: false );
    )
    .catch(function () 
      $state.go('login', ,  notify: false );
    );

虽然这不是很理想,但由于这个系统中权限的加载方式,这对我来说是必要的;如果我使用了同步hasPermission,则在请求页面时可能尚未加载权限。 :( 也许我们可以向 ui-router 询问事件的continueTransition 方法?

authSvc.hasPermissionAsync(toState.data.permission).then(continueTransition).catch(function() 
  cancelTransition();
  return $state.go('login', ,  notify: false );
);

【讨论】:

附录:在 ui-router 的当前版本中,notify: false 似乎完全阻止了状态转换。 :\【参考方案7】:

我知道这对游戏来说已经很晚了,但我想发表我的观点并讨论我认为“暂停”状态更改的绝佳方式。根据 angular-ui-router 的文档,状态的“resolve”对象的任何成员都必须在状态完成加载之前解决。所以我的功能性(尽管尚未清理和完善)解决方案是在“$stateChangeStart”上向“toState”的解析对象添加一个承诺:

例如:

$rootScope.$on('$stateChangeStart', function (event, toState, toParams) 
    toState.resolve.promise = [
        '$q',
        function($q) 
            var defer = $q.defer();
            $http.makeSomeAPICallOrWhatever().then(function (resp) 
                if(resp = thisOrThat) 
                    doSomeThingsHere();
                    defer.resolve();
                 else 
                    doOtherThingsHere();
                    defer.resolve();
                
            );
            return defer.promise;
        
    ]
);

这将确保当 API 调用完成并根据 API 的返回做出所有决定时,状态更改将被解决以解决承诺。在允许导航到新页面之前,我已经使用它来检查服务器端的登录状态。当 API 调用解决时,我要么使用“event.preventDefault()”来停止原始导航,然后路由到登录页面(用 if state.name != “login”包围整个代码块)或允许用户继续通过简单地解决延迟承诺而不是尝试使用绕过布尔值和 preventDefault()。

虽然我确信原始发帖人早就发现了他们的问题,但我真的希望这对其他人有所帮助。

编辑

我想我不想误导人们。如果您不确定您的状态是否有解析对象,代码应该如下所示:

$rootScope.$on('$stateChangeStart', function (event, toState, toParams) 
    if (!toState.resolve)  toState.resolve =  ;
    toState.resolve.pauseStateChange = [
        '$q',
        function($q) 
            var defer = $q.defer();
            $http.makeSomeAPICallOrWhatever().then(function (resp) 
                if(resp = thisOrThat) 
                    doSomeThingsHere();
                    defer.resolve();
                 else 
                    doOtherThingsHere();
                    defer.resolve();
                
            );
            return defer.promise;
        
    ]
);

编辑 2

为了使该功能适用​​于没有解析定义的状态,您需要在 app.config 中添加:

   var $delegate = $stateProvider.state;
        $stateProvider.state = function(name, definition) 
            if (!definition.resolve) 
                definition.resolve = ;
            

            return $delegate.apply(this, arguments);
        ;

在 stateChangeStart 中执行 if (!toState.resolve) toState.resolve = ; 似乎不起作用,我认为 ui-router 在初始化后不接受解析字典。

【讨论】:

这当然取决于您的所有状态都有解析对象这一事实。我的应用程序就是这样设置的。这很容易做到,但是在上面的代码中嵌套一个检查解析对象的 if/else 也很简单。 这对我来说是一个很好的起点。就我而言,我要么解决,要么拒绝承诺。如果已解决,则状态更改将照常继续。如果它被拒绝,我会在$stateChangeError 中继续我的逻辑。干燥且非常优雅,谢谢。 另外,toState.resolve.promise 可以是 toState.resolve.myStateChangeCheck 或任何你想要的。 .promise 让我失望了,因为它通常是为 promise 对象保留的。 这似乎只对我有用,如果 toState 有其他解决方案。如果它没有其他解析,则永远不会调用我添加的解析。 您必须为每个状态添加至少一个 resolve: 才能使此工作正常进行。 if (!toState.resolve) toState.resolve = ; 不工作【参考方案8】:

on 方法返回a deregistration function for this listener

所以你可以这样做:

var unbindStateChangeEvent = $scope.$on('$stateChangeStart', 
  function(event, toState, toParams)  
    event.preventDefault(); 

    waitForSomething(function (everythingIsFine) 
      if(everythingIsFine) 
        unbindStateChangeEvent();
        $state.go(toState, toParams);
      
    );
);

【讨论】:

【参考方案9】:

我真的很喜欢TheRyBerg 建议的解决方案,因为您可以在一个地方完成所有操作,而无需太多奇怪的技巧。我发现有一种方法可以进一步改进它,这样你就不需要 rootscope 中的 stateChangeBypass 了。主要思想是您希望在您的应用程序可以“运行”之前在代码中初始化一些内容。然后,如果您只记得它是否已初始化,您可以这样做:

rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState) 

    if (dataService.isInitialized()) 
        proceedAsUsual(); // Do the required checks and redirects here based on the data that you can expect ready from the dataService
     
    else 

        event.preventDefault();

        dataService.intialize().success(function () 
                $state.go(toState, toParams);
        );
    
);

然后你可以只记得你的数据已经在服务中以你喜欢的方式初始化了,例如:

function dataService() 

    var initialized = false;

    return 
        initialize: initialize,
        isInitialized: isInitialized
    

    function intialize() 

        return $http.get(...)
                    .success(function(response) 
                            initialized=true;
                    );

    

    function isInitialized() 
        return initialized;
    
;

【讨论】:

【参考方案10】:
        var lastTransition = null;
        $rootScope.$on('$stateChangeStart',
            function(event, toState, toParams, fromState, fromParams, options)  
                // state change listener will keep getting fired while waiting for promise so if detect another call to same transition then just return immediately
                if(lastTransition === toState.name) 
                    return;
                

                lastTransition = toState.name;

                // Don't do transition until after promise resolved
                event.preventDefault();
                return executeFunctionThatReturnsPromise(fromParams, toParams).then(function(result) 
                    $state.go(toState,toParams,options);
                );
        );

我在 stateChangeStart 期间使用布尔值守卫来避免无限循环时遇到了一些问题,因此采用这种方法来检查是否再次尝试了相同的转换,如果是,则立即返回,因为在这种情况下,promise 仍未解决。

【讨论】:

以上是关于停止 angular-ui-router 导航,直到 promise 解决的主要内容,如果未能解决你的问题,请参考以下文章

angular-ui-router 使用打字稿嵌套命名视图

(Angular-ui-router) 在解析过程中显示加载动画

Angular-ui-router 打字稿定义

如何删除 angular-ui-router URL 中的“#”符号

Angular Webpack ES2015 组件构建中未触发 angular-ui-router $stateChangeStart 事件

Angular-UI-Router:ui-sref 不使用参数构建 href