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

IT技术 javascript angularjs coffeescript angular-ui-router
2021-01-28 21:09:59

我已经使用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 状态机但启用后退按钮功能?

6个回答

笔记

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


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

您需要几个组件:

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

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

  • 一个国家历史一个由唯一 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

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

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

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

这种技术的最大问题是,在调用@location.url urlstateChange方法会触发$locationChangeSuccess事件,那么调用该locationChange方法。同样调用@state.gofromlocationChange将触发$stateChangeSuccess事件和stateChange方法。这会变得非常混乱,并且会无休止地弄乱浏览器历史记录。

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

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

Angular 应用程序运行

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

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

语言是javascript,语法是coffeescript。
2021-03-16 21:09:59
此示例使用哪种语言。似乎不是我熟悉的角度。
2021-03-18 21:09:59
@jlguenego 您有一个功能齐全的浏览器历史记录/浏览器来回按钮和 URL,您可以将其添加为书签。
2021-03-23 21:09:59
在上面的 SessionService 示例中,我认为 accessor: 方法应该使用@setStorageand@getStorage而不是`@get/setSession'。
2021-03-30 21:09:59
@jlguenego - 建议使用 的变体的答案$window.history.back()错过了问题的关键部分。关键是在历史跳转(后退/前进/刷新)时将应用程序的状态恢复到正确的状态位置。这通常是通过 URI 提供状态数据来实现的。该问题询问如何在没有(显式)URI 状态数据的情况下在状态位置之间跳转鉴于此约束,仅复制后退按钮是不够的,因为这依赖于 URI 状态数据来重新建立状态位置。
2021-04-02 21:09:59
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>
@BenjaminConant 对于不知道如何实现这一点的人,您只需将 放入$window.history.back();一个要调用的函数中即可ng-click
2021-03-25 21:09:59
正确的 rootScope 只是为了使该函数在任何模板中都可以访问
2021-03-30 21:09:59
鸡肉晚餐。
2021-03-30 21:09:59
喜欢这个!...大声笑...为了清楚起见$window.history.back,这不是魔术$rootScope......所以如果你愿意,返回可以绑定到你的导航栏指令范围。
2021-04-06 21:09:59

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

如果您使用 angular ui-router 并且您需要一个按钮来最好地返回:

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

或者

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

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

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

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

搜索 -> 查看 -> 编辑

到 :

搜索 -> 查看

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

搜索 -> 查看 -> 编辑

这不合逻辑

但是使用简单的解决方案

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

从 :

搜索 -> 查看 -> 编辑

点击按钮后:

搜索 -> 查看

单击浏览器后退按钮后:

搜索

一致性受到尊重。:-)

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

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

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

浏览器的后退/前进按钮解决方案
我遇到了同样的问题,我使用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() 的浏览器兼容性以确保