如何让后退按钮与 AngularJS ui-router 状态机一起使用? [英] How do I get the Back Button to work with an AngularJS ui-router state machine?

查看:16
本文介绍了如何让后退按钮与 AngularJS ui-router 状态机一起使用?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have implemented an angularjs single page application using ui-router.

最初我使用不同的 url 来标识每个状态,但这会导致不友好的 GUID 打包 url.

Originally I identified each state using a distinct url however this made for unfriendly, GUID packed urls.

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

So I have now defined my site as a much simpler state-machine. The states are not identified by urls but are simply transitioned to as required, like this:

定义嵌套状态

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,后退按钮不起作用.

This system works very well and I love its clean syntax. However, as I am not using urls the back button does not work.

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

How do I keep my neat ui-router state-machine but enable the back button functionality?

推荐答案

注意

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

Note

The answers that suggest using variations of $window.history.back() have all missed a crucial part of the question: How to restore the application's state to the correct state-location as the history jumps (back/forward/refresh). With that in mind; please, read on.

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

Yes, it is possible to have the browser back/forward (history) and refresh whilst running a pure ui-router state-machine but it takes a bit of doing.

您需要几个组件:

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

  • Unique URLs. The browser only enables the back/forward buttons when you change urls, so you must generate a unique url per visited state. These urls need not contain any state information though.

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

A Session Service. Each generated url is correlated to a particular state so you need a way to store your url-state pairs so that you can retrieve the state information after your angular app has been restarted by back / forward or refresh clicks.

国家历史.一个由唯一 url 键控的 ui-router 状态的简单字典.如果您可以依赖 HTML5,那么您可以使用 HTML5 History API,但如果像我一样,你不能,那么你可以用几行代码自己实现它(见下文).

A State History. A simple dictionary of ui-router states keyed by unique url. If you can rely on HTML5 then you can use the HTML5 History API, but if, like me, you can't then you can implement it yourself in a few lines of code (see below).

定位服务.最后,您需要能够管理由代码内部触发的 ui-router 状态更改,以及通常由用户单击浏览器按钮或在浏览器栏中输入内容触发的正常浏览器 url 更改.这一切都会变得有点棘手,因为很容易混淆触发了什么.

A Location Service. Finally, you need to be able manage both ui-router state changes, triggered internally by your code, and normal browser url changes typically triggered by the user clicking browser buttons or typing stuff into the browser bar. This can all get a bit tricky because it is easy to get confused about what triggered what.

这是我对这些要求中的每一个的实现.我已将所有内容捆绑为三项服务:

Here is my implementation of each of these requirements. I have bundled everything up into three services:

会话服务

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 对象的包装器.为了清楚起见,我在这里将其删减.有关这方面的完整说明,请参阅:如何使用 AngularJS 单页应用程序处理页面刷新

This is a wrapper for the javascript sessionStorage object. I have cut it down for clarity here. For a full explanation of this please see: 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 键控的历史状态.它实际上只是一个字典样式对象的便利包装器.

The StateHistoryService looks after the storage and retrieval of historical states keyed by generated, unique urls. It is really just a convenience wrapper for a dictionary style object.

州定位服务

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 时.如果 StateHistoryService 中存在当前 location.url 的状态,那么它用于通过 ui-router 的 $state.go 恢复状态.

  • locationChange. This is called when the browsers location is changed, typically when the back/forward/refresh button is pressed or when the app first starts or when the user types in a url. If a state for the current location.url exists in the StateHistoryService then it is used to restore the state via ui-router's $state.go.

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

stateChange. This is called when you move state internally. The current state's name and params are stored in the StateHistoryService keyed by a generated url. This generated url can be anything you want, it may or may not identify the state in a human readable way. In my case I am using a state param plus a randomly generated sequence of digits derived from a guid (see foot for the guid generator snippet). The generated url is displayed in the browser bar and, crucially, added to the browser's internal history stack using @location.url url. Its adding the url to the browser's history stack that enables the forward / back buttons.

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

The big problem with this technique is that calling @location.url url in the stateChange method will trigger the $locationChangeSuccess event and so call the locationChange method. Equally calling the @state.go from locationChange will trigger the $stateChangeSuccess event and so the stateChange method. This gets very confusing and messes up the browser history no end.

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

The solution is very simple. You can see the preventCall array being used as a stack (pop and push). Each time one of the methods is called it prevents the other method being called one-time-only. This technique does not interfere with the correct triggering of the $ events and keeps everything straight.

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

Now all we need to do is call the HistoryService methods at the appropriate time in the state transition life-cycle. This is done in the AngularJS Apps .run method, like this:

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

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

With all this in place, the forward / back buttons and the refresh button all work as expected.

这篇关于如何让后退按钮与 AngularJS ui-router 状态机一起使用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆