UI 遍历多视图状态树时路由器转换被取代

UI Router transition superseded when traversing multi-views state-tree

我正在尝试通过以下多视图状态管理我的管理页面:admin、admin.header、admin.leftPanel、admin.main、admin.tail。在 header、leftPanel、main 和 tail 中,我分别使用 $state.go 到它们的子状态来渲染它们的内容。我写了下面的简单代码来演示这个问题。

演示状态模型:

state1:
  state2view
    controller: $state.go(state1.state2) <---superseded
  state3view
    controller: $state.go(state1.state3)

代码(plunker):

<!DOCTYPE html>
<html ng-app="demo">

  <head>
    <meta charset="utf-8" />
    <title>Demo</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.6/angular.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.3/angular-ui-router.js"></script>
    <script>
      let app = angular.module('demo', ['ui.router']);
    
      app.config(['$urlRouterProvider', '$stateProvider', function ($up, $sp) {
        $sp.state('state1', state1);
        $sp.state('state1.state2', new SubState('state2view'));
        $sp.state('state1.state3', new SubState('state3view'));
        $up.otherwise('/');
      }]);
      
      let state1 = {
        url: '/',
        views: {
          "state1view1": {
            controller: ['$transition$', '$state', function ($tr, $st) {
              this.stateName = $st.current.name;
              $st.go('state1.state2', {message: 'message from ' + $st.current.name + ' to ' + $st.current.name + '.state2'});
            }],
            controllerAs: '$ctrl',
            template: `<div>
              {{$ctrl.stateName}} begin<br>
              <ui-view name="state2view"></ui-view>
              {{$ctrl.stateName}} end
            </div>`
          },
          
          "state1view2": {
            controller: ['$transition$', '$state', function ($tr, $st) {
              this.stateName = $st.current.name;
              $st.go('state1.state3', {message: 'message from ' + $st.current.name + ' to ' + $st.current.name + '.state3'});
            }],
            controllerAs: '$ctrl',
            template: `<div>
              {{$ctrl.stateName}} begin<br>
              <ui-view name="state3view"></ui-view>
              {{$ctrl.stateName}} end
            </div>`
          }
        }
      };
      
      function SubState(view1Name) {
        this.params = {message: ''};
        this.views = {};
        this.views[view1Name] = {
          controller: ['$transition$', '$state', function ($tr, $st) {
            this.parentMessage = $tr.params().message;
            this.stateName = $st.current.name;
          }],
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}} begin<br>
            {{$ctrl.parentMessage}}<br>
            {{$ctrl.stateName}} end
          </div>`
        };
      }
      
      app.run(function($transitions) {
        $transitions.onStart({}, function($tr) {
            console.log("trans begin: " + $tr.from().name + " -> " + $tr.to().name);
          }
        );
        $transitions.onSuccess({}, function($tr) {
            console.log("trans done: " + $tr.from().name + " -> " + $tr.to().name);
          }
        );
      });

    </script>
    <style>
    div{border-style: solid;}
    </style>
  </head>

  <body>
    <ui-view name="state1view1"></ui-view>
    <br>
    <ui-view name="state1view2"></ui-view>
  </body>

</html>

预期结果:

state1 begin
state1.state2 begin
message from state1 to state1.state2
state1.state2 end
state1.state3 begin
message from state1 to state1.state3
state1.state3 end
state1 end

实际结果:

state1 begin
state1 end
state1 begin
state1.state3 begin
message from state1 to state1.state3
state1.state3 end
state1 end

控制台输出:

您应该使用由@marklagendijk 创建的 stateHelper 模块。

如果您不想使用上述模块,请阅读下面 link 中有关嵌套状态的这篇文章以获得更多选项

Nested States

plunkr

angular.module('app').config([
'$urlRouterProvider',
 'stateHelperProvider',
 function($urlRouterProvider, stateHelperProvider) {
stateHelperProvider.state({
  name: 'state1',
  template: '<ui-view/>',
  abstract: true,
  resolve: {
    // haven't got to this point yet :-/
  },
  children: [{
      name: 'state2',
      controller:['$state','$q', function ( $st,$q) {
          this.stateName = $st.current.name;
         // $st.transitionTo('state1.state2', {message: 'message from ' + $st.current.name + ' to ' + $st.current.name + '.state2'});
        }], controllerAs: '$ctrl',
      url : '/',
      template: `<br>{{$ctrl.stateName}} begin
      <br>message from {{$ctrl.stateName}} to {{$ctrl.stateName}}.state3
      <br>{{$ctrl.stateName}} end`
  },{
      name: 'state3',
      url : '/',
      controller:['$state','$q', function ( $st,$q) {
          this.stateName = $st.current.name;

         // $st.transitionTo('state1.state2', {message: 'message from ' + $st.current.name + ' to ' + $st.current.name + '.state2'});
        }], controllerAs: '$ctrl',
      template: `<br>{{$ctrl.stateName}} begin
      <br>message from {{$ctrl.stateName}} to {{$ctrl.stateName}}.state3
      <br>{{$ctrl.stateName}} end`
  }]
});}]);

原来是撞墙了

通过编程一个一个地访问路由器状态和布局,将页面内容包含在UI中的想法是完全错误的。 特别是不能同时显示两个兄弟状态的视图。

UI 路由器设计用于通过鼠标点击进行路由。尽管拼凑的文档强烈暗示我们可以在状态树上中继我们的整个页面来布局所有内容,但情况并非总是如此。只要应用程序逻辑转换到另一个不是起始状态的后代的状态,起始状态就会在进入终止状态之前退出,并且它的 运行 时间生成退出的视图状态已完全删除。

下面的代码本来是想证明我的概念下面(原始问题中设计的改进,因为我意识到状态中没有 break-point/resume 设计)并解决我的问题,但是它原来是一个揭示对立面的例子——错误想法下的不可能。

概念

  1. 定义状态及其层次结构和模板以构建页面布局;
  2. 制作状态树;
  3. 制作遍历路径;
  4. 遍历路径 $state.go 到每个状态以展开并呈现布局。

代码 (plnkr)

<!DOCTYPE html>
<html ng-app="demo">

  <head>
    <meta charset="utf-8" />
    <title>Demo</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.6/angular.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.3/angular-ui-router.js"></script>

    <script>
      let app = angular.module('demo', ['ui.router']);

      app.config(['$urlRouterProvider', function ($up) {
        $up.otherwise('/');
      }]);

      app.provider('runtimeStates', ['$stateProvider', function ($stateProvider) {
        this.$get = function () {
          return {
            newState: function (name, param) {
              $stateProvider.state(name, param);
              return name;
            }
          };
        };
      }]);

      app.factory('sharingSpace', function () {
        return {
          stateTree: [],
          traversePath: []
        };
      });

      app.run(['sharingSpace', '$transitions', '$state' ,function(ss, $trs, $st) {
        $trs.onStart({}, function($tr) {
            console.log("trans begin: " + $tr.from().name + " -> " + $tr.to().name);
          }
        );
        $trs.onSuccess({}, function($tr) {
            nextHop(ss, $st);
            console.log("trans succeeded: " + $tr.from().name + " -> " + $tr.to().name);
          }
        );
      }]);

      app.run(['runtimeStates', 'sharingSpace', function(rt, ss) {
        makeStateTree(rt, ss);
      }]);

      function StateParam(stateName) {
        let me = this;
        me.name = stateName;
        me.params = {
          message : {
            value: '',
            dynamic: true
          }
        };
        me.views = {};
        //me.sticky = true; <---does not prevent the view port from removed when exit.
        me.onExit = ['$state', function($state){
          let goodByeMsg = 'Goodbye ' + $state.current.name;
          console.log(goodByeMsg);
          alert(goodByeMsg);
        }];
        me.addView = function(viewParam) {
          me.views[viewParam.name] = {
            controller: viewParam.controller,
            controllerAs: viewParam.controllerAs,
            template: viewParam.template,
          };
          return me;
        };
        return me;
      }

      function makeStateTree(rt, ss) {

        let state1view1param = {
          name: 'state1view1',
          controller: ['sharingSpace', '$transition$', '$state', function (ss, $tr, $st) {
            this.stateName = $st.current.name;
            this.viewName = 'state1view1';
            makeTraversePath(ss);
            //do something ...
          }],
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} begin<br>
            let's start ...<br>
            <ui-view name="state2view"></ui-view>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} end
          </div>`
        }

        let trivialCtrl = function(viewName) {
          return ['sharingSpace', '$transition$', '$state', function (ss, $tr, $st) {
            this.parentMessage = $tr.params().message;
            this.stateName = $st.current.name;
            this.viewName = viewName;
            //do something ...
            console.log('this.stateName = ' + this.stateName);
          }];
        };

        let state1view2param = {
          name: 'state1view2',
          controller: trivialCtrl('state1view2'),
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} begin<br>
            <ui-view name="state3view"></ui-view>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} end
          </div>`
        }

        let state2viewParam = {
          name: 'state2view',
          controller: trivialCtrl('state2view'),
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} begin<br>
            parentMessage: {{$ctrl.parentMessage}}<br>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} end
          </div>`
        }

        let state3viewParam = {
          name: 'state3view',
          controller: trivialCtrl('state3view'),
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} begin<br>
            parentMessage: {{$ctrl.parentMessage}}<br>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} end
          </div>`
        }
        let mainStateParam = new StateParam('state1');
        mainStateParam.url = "/";
        mainStateParam.addView(state1view1param).addView(state1view2param);
        let subStateParam1 = (new StateParam('state1.state2')).addView(state2viewParam);
        let subStateParam2 = (new StateParam('state1.state3')).addView(state3viewParam);
        
        rt.newState(mainStateParam.name, mainStateParam);
        ss.stateTree.push(rt.newState(subStateParam1.name, subStateParam1));
        ss.stateTree.push(rt.newState(subStateParam2.name, subStateParam2));

      }

      function makeTraversePath(ss) {
        for(let i = 0; i<ss.stateTree.length; i++){
          ss.traversePath.push(ss.stateTree[i]); //trivial example
        };
      }

      function nextHop(ss, $st){
        if(ss.traversePath[0] != undefined) {
          let nextHop = ss.traversePath[0];
          ss.traversePath.splice(0, 1);
          console.log('nextHop = ' + nextHop);
          $st.go(nextHop, {message: 'message from ' + $st.current.name});
        }
      }

    </script>

    <style>
      div{border-style: solid;}
    </style>
  </head>

  <body>
    <ui-view name="state1view1"></ui-view>
    <br>
    <ui-view name="state1view2"></ui-view>
  </body>

</html>

结果 (Firefox 57.0.1)

进入页面时:

单击并关闭警报后:

以上过程显示 state1.state2 已执行并布局(但尚未 evaluated/rendered 被 angular 执行),正如我们在第一张图片中看到的那样。那时退出还没有发生,因为 onExit 警报弹出阻止了这个过程。弹出警报关闭后,状态退出,视图被完全删除。

有一个 sticky-state 是为页内标签的特定目的而开发的,但正如我所尝试的那样,它在这里不起作用。它会记住最后访问的状态,但退出状态的视图总是被删除。

我现在尝试将 UI 路由器仅用作路由符号工具。但我必须非常注意不要 运行 认为 UI 路由器可以用作布局页面的通用工具,如 angular 组件的扩展。但这可能很困难:目前我想不出使用 UI 路由器的正确模式。在多视图的情况下,如果任何两个同级视图都有自己的子状态,我必须非常小心,因为访问一个会退出另一个——它们是排他的。这让我觉得不值得它的复杂性。

虽然在大多数情况下导航期间需要在退出时删除视图,但我建议更改 UI 路由器并提供保留视图的机会以提供更大的灵活性。它可能比最初想象的更复杂,但它应该是可能的。

还希望为每个状态(不仅仅是粘性状态)缓存所有 "last seen" 参数,以便我们可以轻松地 return 到它们。你可能会争论用例,但我们无法想象人们将如何使用工具,不应该限制可能性。

还希望为每个状态库提供完整生命周期挂钩的工具(现在只有 onEnter 和 onExit)。