如何让后退按钮与AngularJS ui-router状态机一起使用?

时间:2014-03-07 10:16:45

标签: javascript angularjs coffeescript angular-ui-router

我使用ui-router实现了angularjs单页面应用程序。

最初我使用一个不同的URL来识别每个州,但这是为了不友好的GUID打包网址。

所以我现在将我的网站定义为一个更简单的状态机。状态不是由URL标识的,而是根据需要简单地转换为:

定义嵌套状态

angular
.module 'app', ['ui.router']
.config ($stateProvider) ->
    $stateProvider
    .state 'main', 
        templateUrl: 'main.html'
        controller: 'mainCtrl'
        params: ['locationId']

    .state 'folder', 
        templateUrl: 'folder.html'
        parent: 'main'
        controller: 'folderCtrl'
        resolve:
            folder:(apiService) -> apiService.get '#base/folder/#locationId'

过渡到定义的状态

#The ui-sref attrib transitions to the 'folder' state

a(ui-sref="folder({locationId:'{{folder.Id}}'})")
    | {{ folder.Name }}

这个系统工作得非常好,我喜欢它干净的语法。但是,由于我没有使用URL,后退按钮不起作用。

如何保持我的整洁ui-router状态机,但启用后退按钮功能?

8 个答案:

答案 0 :(得分:74)

注意

建议使用$window.history.back()变体的答案都错过了问题的关键部分:如何将应用程序的状态恢复到正确的状态位置历史跳跃(后退/前进/刷新)。考虑到这一点;请继续阅读。


是的,可以在运行纯ui-router状态机的同时让浏览器返回/转发(历史记录)并刷新,但需要做一些工作。

您需要几个组件:

  • 唯一网址。浏览器仅在您更改网址时启用后退/前进按钮,因此您必须为每个访问状态生成唯一网址。这些网址不需要包含任何州信息。

  • 会话服务。每个生成的URL都与特定状态相关联,因此您需要一种方法来存储您的URL状态对,以便在通过后退/前进或刷新点击重新启动角度应用程序后检索状态信息。

  • 州历史。由唯一网址键入的ui-router状态的简单字典。如果您可以依赖HTML5,那么您可以使用HTML5 History API,但如果像我一样,那么您可以在几行代码中自行实现它(见下文)。

  • 位置服务。最后,您需要能够管理由代码内部触发的ui-router状态更改,以及通常由用户单击浏览器按钮或在浏览器栏中键入内容触发的正常浏览器URL更改。这一切都有点棘手,因为很容易对引发什么的事情感到困惑。

以下是我对这些要求的实施。我已将所有内容捆绑到三个服务中:

会话服务

class SessionService

    setStorage:(key, value) ->
        json =  if value is undefined then null else JSON.stringify value
        sessionStorage.setItem key, json

    getStorage:(key)->
        JSON.parse sessionStorage.getItem key

    clear: ->
        @setStorage(key, null) for key of sessionStorage

    stateHistory:(value=null) ->
        @accessor 'stateHistory', value

    # other properties goes here

    accessor:(name, value)->
        return @getStorage name unless value?
        @setStorage name, value

angular
.module 'app.Services'
.service 'sessionService', SessionService

这是javascript sessionStorage对象的包装器。为了清楚起见,我已将其剪下来了。有关详细说明,请参阅:How do I handle page refreshing with an AngularJS Single Page Application

州历史服务

class StateHistoryService
    @$inject:['sessionService']
    constructor:(@sessionService) ->

    set:(key, state)->
        history = @sessionService.stateHistory() ? {}
        history[key] = state
        @sessionService.stateHistory history

    get:(key)->
        @sessionService.stateHistory()?[key]

angular
.module 'app.Services'
.service 'stateHistoryService', StateHistoryService

StateHistoryService负责存储和检索由生成的唯一网址键入的历史状态。它实际上只是字典样式对象的便利包装器。

州位置服务

class StateLocationService
    preventCall:[]
    @$inject:['$location','$state', 'stateHistoryService']
    constructor:(@location, @state, @stateHistoryService) ->

    locationChange: ->
        return if @preventCall.pop('locationChange')?
        entry = @stateHistoryService.get @location.url()
        return unless entry?
        @preventCall.push 'stateChange'
        @state.go entry.name, entry.params, {location:false}

    stateChange: ->
        return if @preventCall.pop('stateChange')?
        entry = {name: @state.current.name, params: @state.params}
        #generate your site specific, unique url here
        url = "/#{@state.params.subscriptionUrl}/#{Math.guid().substr(0,8)}"
        @stateHistoryService.set url, entry
        @preventCall.push 'locationChange'
        @location.url url

angular
.module 'app.Services'
.service 'stateLocationService', StateLocationService

StateLocationService处理两个事件:

  • locationChange 。当浏览器位置发生变化时,通常在按下后退/前进/刷新按钮或应用程序首次启动时或用户键入URL时调用此方法。如果当前location.url的状态存在于StateHistoryService中,那么它将用于通过ui-router' $state.go恢复状态。

  • stateChange 。在内部移动状态时调用此方法。当前状态的名称和参数存储在由生成的URL键入的StateHistoryService中。这个生成的url可以是你想要的任何东西,它可能会或可能不会以人类可读的方式识别状态。在我的情况下,我使用状态参数加上从guid派生的随机生成的数字序列(请参阅guid生成器片段的脚)。生成的URL显示在浏览器栏中,并且最重要的是,使用@location.url url将其添加到浏览器的内部历史堆栈中。它将URL添加到浏览器的历史堆栈中,启用了前进/后退按钮。

这种技术的一个大问题是,在@location.url url方法中调用stateChange会触发$locationChangeSuccess事件,因此调用locationChange方法。从@state.go同等地调用locationChange会触发$stateChangeSuccess事件,因此会触发stateChange方法。这变得非常令人困惑,并且彻底弄乱了浏览器历史。

解决方案非常简单。您可以看到preventCall数组被用作堆栈(poppush)。每次调用其中一个方法时,它都会阻止另一个方法被一次性调用。这种技术不会干扰$事件的正确触发,并保持一切顺利。

现在我们需要做的就是在状态转换生命周期中的适当时间调用HistoryService方法。这是在AngularJS Apps .run方法中完成的,如下所示:

Angular app.run

angular
.module 'app', ['ui.router']
.run ($rootScope, stateLocationService) ->

    $rootScope.$on '$stateChangeSuccess', (event, toState, toParams) ->
        stateLocationService.stateChange()

    $rootScope.$on '$locationChangeSuccess', ->
        stateLocationService.locationChange()

生成指南

Math.guid = ->
    s4 = -> Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
    "#{s4()}#{s4()}-#{s4()}-#{s4()}-#{s4()}-#{s4()}#{s4()}#{s4()}"

完成所有这些后,前进/后退按钮和刷新按钮都按预期工作。

答案 1 :(得分:46)

app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>

答案 2 :(得分:23)

在测试了不同的提案之后,我发现最简单的方法往往是最好的。

如果您使用有角度的ui-router并且需要一个按钮才能返回最佳状态:

<button onclick="history.back()">Back</button>

<a onclick="history.back()>Back</a>

//警告不要设置href或路径将被破坏。

说明: 假设一个标准的管理应用程序。 搜索对象 - &gt;查看对象 - &gt;编辑对象

使用角度解决方案 从这个状态:

  

搜索 - &gt;查看 - &gt;修改

致:

  

搜索 - &gt;图

那就是我们想要的东西,除非现在你点击浏览器后退按钮,你会再次出现在那里:

  

搜索 - &gt;查看 - &gt;修改

这不合逻辑

但是使用简单的解决方案

<a onclick="history.back()"> Back </a>

来自:

  

搜索 - &gt;查看 - &gt;修改

单击按钮后

  

搜索 - &gt;图

单击浏览器后退按钮后

  

搜索

尊重一致性。 : - )

答案 3 :(得分:7)

如果您正在寻找最简单的&#34;返回&#34;按钮,然后你可以像这样设置一个指令:

    .directive('back', function factory($window) {
      return {
        restrict   : 'E',
        replace    : true,
        transclude : true,
        templateUrl: 'wherever your template is located',
        link: function (scope, element, attrs) {
          scope.navBack = function() {
            $window.history.back();
          };
        }
      };
    });

请记住,这是一个相当愚蠢的&#34;回来&#34;按钮,因为它正在使用浏览器的历史记录。如果您将其包含在目标网页上,它会将用户发回给您登陆之前的任何网址。

答案 4 :(得分:3)

浏览器的后退/前进按钮解决方案
我遇到了同样的问题,我使用$ window对象<div class="col-lg-12 col-md-12 col-xs-12 padding"> <form action=" " method="post" id="rolesForm"> <label class="col-lg-2 col-md-2 col-x-2"> Name</label> <input type="text" name="name" id="name" class="col-lg-6 col-md-6 col-xs-12 name"> </form> </div> <div class="col-lg-12 col-md-12 col-xs-12 padding"> <label class="col-lg-2 col-md-2 col-x-2">Description</label> <textarea type="text" name="description" id="description" class="col-lg-6 col-md-6 col-xs-12"></textarea> </div> <div class="col-lg-12 col-md-12 col-xs-12 padding pull"> <span class="pull-right"> <button class="btn save " id="save" onclick="submit"><i class="fa fa-floppy-o"></i>Save </button> </span> </div> 中的popstate event解决了它。每当活动历史记录条目更改时,就会将popstate事件分派到窗口 即使地址栏指示新位置,也不会在浏览器按钮上点击ui-router's $state object$stateChangeSuccess事件。
因此,假设您再次从状态$locationChangeSuccess导航到main再到folder,当您在浏览器上点击main时,您应该回到{{ 1}}路线。路径已更新,但视图不会显示,但仍显示back上的所有内容。试试这个:

folder

后退/前进按钮应该在此之后顺利运行。

注意:检查window.onpopstate()的浏览器兼容性以确保

答案 5 :(得分:3)

可以使用简单的指令“go-back-history”来解决,如果没有以前的历史,这个也是关闭窗口。

指令用法

img = PhotoImage(file="G.E.N.E.S\Textures\Ppm's\G.E.N.E.S.-1.ppm")
img.zoom(2,2) # x, y 

Angular指令声明

<a data-go-back-history>Previous State</a>

注意:是否使用ui-router。

答案 6 :(得分:0)

“后退”按钮对我来说也不起作用,但我发现问题在于我的主页内html元素中有ui-view个内容。

即。

<div ui-view>
     <h1> Hey Kids! </h1>
     <!-- More content -->
</div>

因此,我将内容移动到新的.html文件中,并将其标记为.js文件中包含路径的模板。

   .state("parent.mystuff", {
        url: "/mystuff",
        controller: 'myStuffCtrl',
        templateUrl: "myStuff.html"
    })

答案 7 :(得分:-1)

history.back()并切换到以前的状态通常不会产生您想要的效果。例如,如果您有包含制表符的表单,并且每个制表符都有自己的状态,则只选择切换之前的制表符,而不是从表单返回。在嵌套状态的情况下,您通常需要考虑要回滚的父状态的女巫。

该指令解决了问题

angular.module('app', ['ui-router-back'])

<span ui-back='defaultState'> Go back </span>

返回状态,显示按钮之前处于活动状态。可选defaultState是在内存中没有先前状态时使用的状态名称。它还恢复滚动位置

代码

class UiBackData {
    fromStateName: string;
    fromParams: any;
    fromStateScroll: number;
}

interface IRootScope1 extends ng.IScope {
    uiBackData: UiBackData;
}

class UiBackDirective implements ng.IDirective {
    uiBackDataSave: UiBackData;

    constructor(private $state: angular.ui.IStateService,
        private $rootScope: IRootScope1,
        private $timeout: ng.ITimeoutService) {
    }

    link: ng.IDirectiveLinkFn = (scope, element, attrs) => {
        this.uiBackDataSave = angular.copy(this.$rootScope.uiBackData);

        function parseStateRef(ref, current) {
            var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed;
            if (preparsed) ref = current + '(' + preparsed[1] + ')';
            parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
            if (!parsed || parsed.length !== 4)
                throw new Error("Invalid state ref '" + ref + "'");
            let paramExpr = parsed[3] || null;
            let copy = angular.copy(scope.$eval(paramExpr));
            return { state: parsed[1], paramExpr: copy };
        }

        element.on('click', (e) => {
            e.preventDefault();

            if (this.uiBackDataSave.fromStateName)
                this.$state.go(this.uiBackDataSave.fromStateName, this.uiBackDataSave.fromParams)
                    .then(state => {
                        // Override ui-router autoscroll 
                        this.$timeout(() => {
                            $(window).scrollTop(this.uiBackDataSave.fromStateScroll);
                        }, 500, false);
                    });
            else {
                var r = parseStateRef((<any>attrs).uiBack, this.$state.current);
                this.$state.go(r.state, r.paramExpr);
            }
        });
    };

    public static factory(): ng.IDirectiveFactory {
        const directive = ($state, $rootScope, $timeout) =>
            new UiBackDirective($state, $rootScope, $timeout);
        directive.$inject = ['$state', '$rootScope', '$timeout'];
        return directive;
    }
}

angular.module('ui-router-back')
    .directive('uiBack', UiBackDirective.factory())
    .run(['$rootScope',
        ($rootScope: IRootScope1) => {

            $rootScope.$on('$stateChangeSuccess',
                (event, toState, toParams, fromState, fromParams) => {
                    if ($rootScope.uiBackData == null)
                        $rootScope.uiBackData = new UiBackData();
                    $rootScope.uiBackData.fromStateName = fromState.name;
                    $rootScope.uiBackData.fromStateScroll = $(window).scrollTop();
                    $rootScope.uiBackData.fromParams = fromParams;
                });
        }]);