AngularJS 的 ui-router 如何从父状态重定向到子状态?

14

使用onEnter重定向到一个状态时,如果新状态是当前状态的子状态,则会导致无限循环。

示例:

$stateProvider
  .state 'inventory',
    url: '/inventory'
    templateUrl: 'views/inventory.html'
    controller: 'InventoryCtrl'
    onEnter: () ->
      $state.go 'inventory.low'
  .state 'inventory.low',
    url: '/low'
    templateUrl: 'views/inventory-table.html'
    controller: 'LowInventoryCtrl'

何时:

$state.go 'inventory.low'

被称为,状态 inventory 被重新初始化,导致它被再次调用 = 无限循环。

然而,如果重定向状态是:

$state.go 'otherStateThatIsNotAChild'

这个问题并没有发生。我猜想父级状态正在被重新初始化,但是为什么呢?

  1. 当在子状态上调用.go时,为什么会重新初始化父状态?
  2. 那么,你如何处理重定向到子状态?

TaylorMac,请看我的编辑。 - Mohammad Sepahvand
7个回答

20

1) 当在子状态上调用.go时,为什么会重新初始化父状态?

在过渡过程中,任何 $state.go/transitionTo 都会导致当前正在处理的过渡被替换。 被替换的正在处理的过渡将立即被取消。由于您最初对 inventory 的转换在所有状态的 onEnter 函数被调用之前尚未完成,因此原始转换被取消,并从先前活动的状态开始了一个到 inventory.low 的新的转换。

请参阅ui-router源码https://github.com/angular-ui/ui-router/blob/master/src/state.js#L897...

2)那么,如何重定向到子状态呢?

你可以...

  • 将 $state.go 包装在 $timeout() 中,以允许原始转换在重定向之前完成。
  • 在控制器中调用 $state.go。 UI-router 从 ui-view 指令之后调用控制器,该指令在转换完成后执行。

无论哪种情况,请非常确信您希望应用程序像这样重定向。 如果用户直接导航到 inventory 的任何其他子状态(例如 inventory.high),则重定向仍将发生,迫使他们转到 inventory.low,这不是他们想要的。


1
对于1),非常好 - 这回答了问题。它不是因为重定向到子状态而重新初始化(这是我认为很奇怪的原因),而是因为状态转换尚未完成。关于在子状态中过渡到.low的观点很好 - 我甚至没有考虑过这一点。也许监听“inventory”上的$stateChangeStart是最好的方法。赞! - TaylorMac
另外,您还可以查看我的个人项目ui-router-extras。它有一个名为DeepStateRedirect的功能,可能可以满足您的需求。它确实会监听$stateChangeStart事件,例如在'inventory'上:http://christopherthielen.github.io/ui-router-extras/example/dsr/index.html#/ - Chris T
除了观察 $stateChangeStart 之外,您还可以在控制器中使用 if 检查 $state.current.name,这样重定向只会在您处于父状态时发生。 - Bill Yang
1
或者您可以在父状态对象上定义一个 redirectTo 属性,将其重定向到子状态。这样做可以确保在重定向之前完成父状态。在 ui-router 的示例 中可以看到一个例子。let appState = { name: 'app', redirectTo: 'welcome', template: template, controller: controller, controllerAs: 'vm' }; - Yiling

9

我遇到了同样的问题。我找到的简单解决方案是监听状态变化并从那里重定向到您的子状态。

这种方法可以让路由默认重定向到子状态。 不需要 进行 url: ''$state.go(),它们无法正常工作。

因此,在config文件中:

$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
  if (toState.redirectTo) {
    event.preventDefault();
    $state.go(toState.redirectTo, toParams);
  }
});

state 文件中:
.state('home', {
  url: '',
  redirectTo: 'home.list'
});

我已经在gist上面做了一个示例:https://gist.github.com/nikoloza/4ab3a94a3c6511bb1dac

这是解决方案。 - Morteza

4
您需要退一步并考虑您想要实现什么。当一个状态只是重定向到一个子状态时,拥有这个状态的意义何在?
关于您的第一个问题,当您到达一个子状态时,父状态总是会被激活,这种行为在状态间共享数据时非常有用,如果没有这种行为,嵌套路由将是不可能的(或者说没有意义)。
至于第二个问题,我在几个大型angular应用程序上工作过,到目前为止我还没有发现自己需要做这个。
好吧,信不信由你,尽管我不愿意承认,但现在我遇到了一个场景,我需要这样做。我有一个profile/userName路由(从技术上讲,这应该是profile/userName/details),还有一个profile/userName/products路由,我想为两个状态都有一个主视图,但同时我希望profile/userName路由有一个干净的url,比如:profile/mike62,而不是profile/mike62/details。所以我最终这样做了:
.state('publicProfile', { url: '/profile/{username}'})//this is the base/shell state
.state('publicProfile.details',{url:null})//I want this to be my de-facto state, with a clean URL, hence the null url
.state('publicProfile.products', {url:'/products/{exceptProductId:/?.*}'})

最终我是这样实现的,虽然有很多方法:

在我的publicProfile状态控制器中(这是基础状态):

$scope.state = $state;
$scope.$watch('state.current', function(v) {
       if(v.name=='publicProfile') {//want to navigate to the 'preferred' state if not going to publicProfile.products
            $state.go('publicProfile.details');
       };
}, true);

是的,这感觉很hacky,但现在我知道有一些边缘情况需要这样做,虽然我们可以重新思考我们的状态设计。另一种更脏的方法是,在基础状态中使用$timeout稍微延迟后检查当前状态,如果我们不处于publicProfile.products状态,则导航到我们首选/实际状态的publicProfile.details。


我的想法是:1)为什么不直接响应$stateChangeStart事件,而要使模型可观察?我理解在模板中增加$rootScope与$state的有用性,但观察模板上的属性并将其与状态相关联?我不知道这是否比放在控制器中更好,至少在那里它是局部化的。 - TaylorMac
你所描述的情况确实需要这样做,并且在考虑分层模块化时经常使用。 - Bernardo Dal Corno

3

看起来你正在尝试设置默认子状态。

这是关于ui-router的一个常见问题:如何设置默认/索引子状态

简而言之,可以通过在父状态中设置abstract: true来使用抽象状态。如果添加多个子状态,则默认为第一个子状态。

$stateProvider

  .state('inventory', {
    abstract: true,
    url: '/inventory',
    templateUrl: 'views/inventory.html',
    controller: 'InventoryCtrl'
  }).

    .state('inventory.low', {
      url: '/low',
      templateUrl: 'views/inventory-table.html',
      controller: 'LowInventoryCtrl'
    });

嘿 @Atav32 这个还有效吗?我按照常见问题解答的方法尝试了,但是在点击我的 ui-sref="parent" 后仍然出现了 "错误:无法转换到抽象状态 'parent'"。 - Exegesis
@Exegesis 嗯,我还没有尝试过链接到父状态。我只是直接链接到“默认”的子状态。ui-sref="parent.child" - Atav32
哇,这似乎是正确的方法!引用也很好。我唯一担心的是使用gulp,不知道谁会成为第一个子状态.. 有什么想法吗? - Bernardo Dal Corno
@Z.Khullah,你能解释一下Gulp可能会干扰什么吗? - Atav32
1
找到了,根据参考资料,您可以使用空URL来设置默认值,或者如在讨论中所述,您可以使用redirectTo来定义+自动重定向。最后一个也解决了@Exegesis的问题,在这个特定的线程中。 - Bernardo Dal Corno
@Atav32 gulp(使用类似FUSE的库)将把所有js文件按照树形结构和可能的文件和文件夹字母顺序连接成一个文件。因此,模块A将首先声明,然后是模块B,这可能是所需的默认设置。 - Bernardo Dal Corno

2
根据 pdvorchik 在相关的 GitHub 讨论串 的回答,这个问题有一个简单的解决方法,对我来说完美地起作用了,即在 $state.go 调用周围包裹一个 .finally() 调用,以确保第一次转换完成后再开始新的转换。
onEnter: ['$state', function($state){
    if ($state.transition) {
        $state.transition.finally(function() {
            $state.go('home.my.other.state', {})
        });
    }
}]

1

在 @Chris T 的解释之后,似乎最干净的解决方案是监听 $stateChangeSuccess 事件:

redirects =
  inventory: 'inventory.low'

$rootScope.$on '$stateChangeSuccess', (e, toState) ->
  redirect = redirects[toState.name]
  $state.go redirect if redirect

Where redirects将包含可能发生的任何路由重定向。感谢所有人!


2
个人而言,我会将其挂钩到$stateChangeStart中,如果有重定向,则运行e.preventDefault()以在执行重定向之前取消转换。 - tzrlk
是的,我同意这一点,这就是我实现的。 - TaylorMac

-1

不要将低库存状态作为子状态。

$stateProvider
    .state 'inventory',
        url: '/inventory'
        templateUrl: 'views/inventory.html'
        controller: 'InventoryCtrl'
        onEnter: () ->
            $state.go 'lowinventory'
    .state 'lowinventory',
        url: '/inventory/low'
        templateUrl: 'views/inventory-table.html'
        controller: 'LowInventoryCtrl'

3
可以,但这样就无法满足父状态映射对象的承诺或继承数据,并且也无法加载到库存状态模板的ui-view中,这正是重点所在。 - TaylorMac

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接