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

Posted

技术标签:

【中文标题】如何让后退按钮与 AngularJS ui-router 状态机一起使用?【英文标题】:How do I get the Back Button to work with an AngularJS ui-router state machine? 【发布时间】:2014-04-10 10:26:47 【问题描述】:

我已经使用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 

这个系统运行良好,我喜欢它简洁的语法。但是,由于我没有使用 url,所以后退按钮不起作用。

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

【问题讨论】:

“url 无法识别状态”——我怀疑是你的问题。后退按钮受到代码的很好保护(否则人们会覆盖它,导致问题)。为什么不让 angular 制作更好的 url,就像 SO 那样(好吧,他们可能没有使用 angular,但他们的 url schemse 是说明性的)? 另外,这个问题可能会有所帮助:***.com/questions/13499040/… 另外,既然您没有使用 URL,这是否意味着要到达 Z 州,人们必须点击 X 和 Y 州才能到达它?这可能会很烦人。 它会与具有不同参数的状态一起使用吗? @jcollum 我不知道,这是很久以前的事了 【参考方案1】:

注意

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


是的,可以在运行纯ui-router 状态机时让浏览器后退/前进(历史记录)和刷新,但这需要做一些工作。

您需要几个组件:

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

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

李>

国家历史。一个简单的 ui-router 状态字典,由唯一的 url 键控。如果你可以依赖 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 负责存储和检索由生成的唯一 URL 键入的历史状态。它实际上只是一个字典样式对象的便捷包装器。

国家定位服务

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 处理两个事件:

位置变化。当浏览器位置改变时调用,通常是按下后退/前进/刷新按钮或应用程序首次启动或用户输入 url 时。如果当前 location.url 的状态存在于StateHistoryService 中,则用于通过ui-router 的$state.go 恢复状态。

状态变化。这在您在内部移动状态时调用。当前状态的名称和参数存储在由生成的 url 键入的 StateHistoryService 中。这个生成的 url 可以是你想要的任何东西,它可能会或可能不会以人类可读的方式识别状态。在我的例子中,我使用了一个状态参数加上一个从 guid 派生的随机生成的数字序列(参见 guid 生成器 sn-p 的脚)。生成的 url 显示在浏览器栏中,并且至关重要的是,使用 @location.url url 添加到浏览器的内部历史堆栈中。它将 url 添加到浏览器的历史堆栈中,以启用前进/后退按钮。

这种技术最大的问题是在stateChange方法中调用@location.url url会触发$locationChangeSuccess事件,所以调用locationChange方法。同样从locationChange 调用@state.go 将触发$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()"

所有这些都到位后,前进/后退按钮和刷新按钮都可以按预期工作。

【讨论】:

在上面SessionService的例子中,我认为accessor:方法应该使用@setStorage@getStorage而不是`@get/setSession'。 本示例使用哪种语言。似乎不是我熟悉的棱角分明。 语言是javascript,语法是coffeescript。 @jlguenego 你有一个功能浏览器历史记录/浏览器来回按钮和 URL,你可以添加书签。 @jlguenego - 建议使用$window.history.back() 变体的答案错过了问题的关键部分。关键是在历史跳转(后退/前进/刷新)时将应用程序的 state 恢复到正确的状态位置。这通常通过通过 URI 提供状态数据来实现。该问题询问如何在状态位置之间跳转没有(显式)URI 状态数据。鉴于此约束,仅复制后退按钮是不够的,因为这依赖于 URI 状态数据来重新建立状态位置。【参考方案2】:

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

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

请记住,这是一个相当不智能的“后退”按钮,因为它使用的是浏览器的历史记录。如果您将其包含在您的目标网页中,它会将用户返回到他们在登陆您之前所来自的任何网址。

【讨论】:

【参考方案3】:
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) 
  $rootScope.goBack = function()
    $window.history.back();
  
]);

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

【讨论】:

喜欢这个! ...大声笑...为了清楚起见$window.history.back 正在做魔术而不是$rootScope ...所以如果你愿意,可以将返回绑定到你的导航栏指令范围。 @BenjaminConant 对于不知道如何实现此功能的人,您只需将$window.history.back(); 放入要调用ng-click 的函数中。 正确的rootScope只是为了让函数在任何模板中都可以访问 鸡肉晚餐。【参考方案4】:

后退按钮对我也不起作用,但我发现问题在于我的主页中有 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"
    )

【讨论】:

【参考方案5】:

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

如果你使用 angular ui-router 并且你需要一个按钮返回最好是这样的:

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

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

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

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

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

搜索 -> 查看 -> 编辑

收件人:

搜索 -> 查看

这就是我们想要的,除非你现在点击浏览器的返回按钮,你会再次出现:

搜索 -> 查看 -> 编辑

这不合逻辑

但是使用简单的解决方案

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

来自:

搜索 -> 查看 -> 编辑

点击按钮后:

搜索 -> 查看

点击浏览器返回按钮后:

搜索

尊重一致性。 :-)

【讨论】:

【参考方案6】:

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;
                );
        ]);

【讨论】:

【参考方案7】:

浏览器的后退/前进按钮解决方案 我遇到了同样的问题,我使用 $window 对象中的popstate eventui-router's $state object 解决了它。每次活动历史记录条目更改时,都会向窗口发送一个 popstate 事件。$stateChangeSuccess$locationChangeSuccess 事件不会在点击浏览器按钮时触发,即使地址栏指示新位置。 因此,假设您再次从状态main 导航到folder 再到main,当您在浏览器上点击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() 的浏览器兼容性以确保

【讨论】:

【参考方案8】:

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

指令用法

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

Angular 指令声明

.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。

【讨论】:

以上是关于如何让后退按钮与 AngularJS ui-router 状态机一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

使用ui-router禁用AngularJS中的后退按钮

使用 AngularJS/Javascript 仅禁用浏览器后退按钮导航(不是应用程序中的链接)

走进AngularJs ng的路由机制

你如何让 VideoCastControllerActivity 上的硬件后退按钮工作?

用iframe后 点后退按钮时总是在潜套框里后退,如何让整个父页面整体后退呢?

如果上一个视图没有标题,如何让导航栏后退按钮总是说回来?