如何获得后退按钮以与AngularJS ui路由器状态机一起使用?


87

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

最初,我使用不同的URL来标识每个状态,但这是针对不友好的GUID打包的URL。

因此,我现在将站点定义为一个更简单的状态机。状态不是由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 }}

该系统运行良好,我喜欢它的简洁语法。但是,由于我没有使用网址,因此后退按钮不起作用。

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


1
“状态无法通过url识别”-我怀疑这是您的问题。后退按钮受到代码的保护(否则人们会覆盖它,从而导致问题)。为什么不仅仅让angular像这样做更好的网址(好,他们可能没有使用angular,但是它们的网址方案是说明性的)?
jcollum 2014年

此外,这个问题可能会有所帮助:stackoverflow.com/questions/13499040/...
jcollum

另外,由于您未使用URL,这是否意味着要进入状态Z,人们将必须单击状态X和Y才能到达状态Z?那可能会很烦人。
jcollum 2014年

它会与具有不同参数的状态一起使用吗?@jcollum
VijayVishnu

我不知道,这是很久以前的事
jcollum

Answers:


78

注意

建议使用的变体形式的答案$window.history.back()都忽略了问题的关键部分:如何还原应用程序的状态随着历史记录的跳转(前进/后退/刷新)到正确的状态位置。考虑到这一点; 请继续阅读。


是的,可以在运行纯ui-router状态机时使浏览器前进/后退(历史记录)并刷新,但是这需要花些时间。

您需要几个组件:

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

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

  • 国家历史。简单的ui-router状态字典,由唯一的url键输入。如果可以依靠HTML5,则可以使用HTML5历史记录API,但如果像我一样不能这样做,则可以自己用几行代码实现它(请参见下文)。

  • 定位服务。最后,您需要能够管理由代码内部触发的ui路由器状态更改,以及通常由用户单击浏览器按钮或在浏览器栏中键入内容而触发的正常浏览器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

这是javascriptsessionStorage对象的包装。为了清楚起见,我将其缩减。有关此内容的完整说明,请参见:如何使用AngularJS单页应用程序处理页面刷新

国家历史服务处

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。当您在内部移动状态时会调用此方法。当前状态的名称和参数存储在StateHistoryService由生成的URL键入的键中。生成的url可以是您想要的任何内容,它可以或可以不以人类可读的方式标识状态。就我而言,我使用的是状态参数,以及从guid派生的随机生成的数字序列(有关guid生成器代码段,请参见脚)。生成的url显示在浏览器栏中,并且至关重要的是,使用添加到浏览器的内部历史记录堆栈中@location.url url。它将url添加到浏览器的历史记录堆栈中,从而启用前进/后退按钮。

这种技术的最大问题是,在调用@location.url urlstateChange方法会触发$locationChangeSuccess事件,那么调用该locationChange方法。同样地调用@state.gofromlocationChange将触发$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
在上述SessionService的例子,我认为访问:方法应该是使用@setStorage@getStorage,而不是`@ GET / setSession”。
彼得·惠特菲尔德

3
本示例使用哪种语言。我似乎不熟悉。
Deepak

语言是javascript,语法是coffeescript。
biofractal

1
@jlguenego您可以使用功能强大的浏览器历史记录/浏览器来回按钮和URL进行书签。
Torsten Barthel,2016年

3
@jlguenego-建议使用的变体形式的答案$window.history.back()错过了问题的关键部分。关键是随着历史记录的跳转(后退/前进/刷新)将应用程序的状态恢复到正确的状态位置。通常,这可以通过URI提供状态数据来实现。该问题询问如何在没有(显式)URI状态数据的状态位置之间跳转。给定此约束,仅复制后退按钮是不够的,因为这依赖于URI状态数据来重新建立状态位置。
biofractal

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

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

2
喜欢这个!...大声笑...为了清楚起见$window.history.back,这并不是魔术$rootScope...所以,如果需要,可以将其绑定到您的navbar指令范围。
本杰明·科南

@BenjaminConant对于不知道如何实现此功能的人,您只需将$window.history.back();in放在要调用的函数中ng-click
chakeda '16

正确的rootScope只是使该功能可在任何模板中访问
GuillaumeMassé16年

鸡肉晚餐。
科迪

23

在测试了不同的建议之后,我发现最简单的方法通常是最好的。

如果您使用角度ui路由器,并且需要一个按钮来返回最佳状态,则是这样的:

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

要么

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

//警告不要设置href,否则路径将被破坏。

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

从此状态使用角解

搜索->查看->编辑

至 :

搜索->查看

好吧,这就是我们想要的,除非现在您单击浏览器后退按钮,然后您将再次出现在这里:

搜索->查看->编辑

那是不合逻辑的

但是使用简单的解决方案

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

来自:

搜索->查看->编辑

点击按钮后:

搜索->查看

单击浏览器后退按钮后:

搜索

尊重一致性。:-)


7

如果您正在寻找最简单的“后退”按钮,则可以像这样设置指令:

    .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();
          };
        }
      };
    });

请记住,这是一个相当不智能的“后退”按钮,因为它使用的是浏览器的历史记录。如果您将其包含在着陆页上,它将把用户送回到他们登陆到您之前的任何URL。


3

浏览器的后退/前进按钮解决方案,
我遇到了相同的问题,并使用popstate event了$ window对象和中的来解决了这个问题ui-router's $state object。每当活动历史记录条目更改时,都会将popstate事件调度到窗口。
$stateChangeSuccess$locationChangeSuccess即使在地址栏中显示的新位置的事件不会对浏览器的按钮,点击触发。
因此,假设你从美国导航mainfoldermain再次,当你点击back浏览器,你应该回到folder路线。路径已更新,但视图未更新,仍然显示您所拥有的内容main。试试这个:

angular
.module 'app', ['ui.router']
.run($state, $window) {

     $window.onpopstate = function(event) {

        var stateName = $state.current.name,
            pathname = $window.location.pathname.split('/')[1],
            routeParams = {};  // i.e.- $state.params

        console.log($state.current.name, pathname); // 'main', 'folder'

        if ($state.current.name.indexOf(pathname) === -1) {
            // Optionally set option.notify to false if you don't want 
            // to retrigger another $stateChangeStart event
            $state.go(
              $state.current.name, 
              routeParams,
              {reload:true, notify: false}
            );
        }
    };
}

之后,后退/前进按钮应该可以正常工作。

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


3

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

指令用法

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

角度指令声明

.directive('goBackHistory', ['$window', function ($window) {
    return {
        restrict: 'A',
        link: function (scope, elm, attrs) {
            elm.on('click', function ($event) {
                $event.stopPropagation();
                if ($window.history.length) {
                    $window.history.back();
                } else {
                    $window.close();  
                }
            });
        }
    };
}])

注意:是否使用ui-router。


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"
    })

-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;
                });
        }]);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.