我如何获得后退button以使用AngularJS UI路由器状态机?

我已经使用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后退button不起作用。

如何保持我整洁的UI路由器状态机,但启用后退buttonfunction?

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

你需要几个组件:

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

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

  • 国家历史 。 ui路由器状态的一个简单的字典由唯一的url键入。 如果你可以依赖HTML5,那么你可以使用HTML5的历史API ,但是,如果像我一样,你不能,那么你可以自己在几行代码中实现它(见下文)。

  • 定位服务 。 最后,您需要能够pipe理UI代码路由器状态更改,由代码在内部触发,以及通常由用户单击浏览器button或在浏览器栏中input内容触发的正常浏览器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对象的包装器。 这里为了清晰起见,我已经把它剪掉了。 有关此的完整说明,请参阅: 如何使用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查看历史状态的存储和检索,这些历史状态由所生成的唯一的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处理两个事件:

  • locationChange 。 当浏览器的位置发生变化时(通常是在按下“后退/前进/刷新”button或应用程序第一次启动时或者用户键入url时),将调用此方法。 如果StateHistoryService存在当前location.url的状态,则通过ui-router的$state.go来恢复状态。

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

这个技术的一个大问题是调用stateChange方法中的@location.url url会触发$locationChangeSuccess事件,所以调用locationChange方法。 同样地,从locationChange调用@state.go将触发$stateChangeSuccess事件,并调用stateChange方法。 这变得非常混乱,弄乱了浏览器的历史没有结束。

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

现在我们只需要在状态转换生命周期的适当时刻调用HistoryService方法。 这是在AngularJS .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() 

生成一个Guid

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

所有这一切就绪,前进/后退button和刷新button都按预期工作。

 app.run(['$window', '$rootScope', function ($window , $rootScope) { $rootScope.goBack = function(){ $window.history.back(); } }]); <a href="#" ng-click="goBack()">Back</a> 

经过testing不同的build议,我发现最简单的方法往往是最好的。

如果您使用angular度ui路由器,并且您需要一个button回到最佳是这样的:

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

要么

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

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

说明:假设一个标准的pipe理应用程序。 search对象 – >查看对象 – >编辑对象

使用angular度解决scheme从这个状态:

search – >查看 – >编辑

至 :

search – >查看

那么这就是我们想要的,除非现在你点击浏览器后退button,你将再次出现在那里:

search – >查看 – >编辑

这是不合逻辑的

但是使用简单的解决scheme

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

来自:

search – >查看 – >编辑

点击button后:

search – >查看

点击浏览器后退button后:

search

一致性受到尊重。 🙂

如果你正在寻找最简单的“后退”button,那么你可以像这样设置一个指令:

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

请记住,这是一个相当不聪明的“后退”button,因为它使用浏览器的历史logging。 如果您将其包含在着陆页上,则会在用户登陆之前将用户返回到任何url。

浏览器的后退/前进button解决scheme
我遇到了同样的问题,我使用$ window对象和ui-router's $state objectpopstate event解决了这个问题。 每当活动的历史logging条目发生变化时,popstate事件就会发送到窗口。
即使地址栏指示新位置,也不会触发浏览器的button点击$stateChangeSuccess$locationChangeSuccess事件。
所以,假设你已经从main状态导航到main folder再次back main界面,当你back浏览器时,你应该回到folder路由。 path被更新,但视图不是,仍然显示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 = {}; // ie- $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} ); } }; } 

后退/前进button应该顺利工作。

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

可以用一个简单的指令“回历史”来解决,这个也是在没有历史的情况下closures的窗口。

指令用法

 <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或不使用。

后退button也不适合我,但我发现问题是我的主页内的html内容在ui-view元素中。

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

所以我把内容移动到一个新的.html文件中,并将其标记为带有path的.js文件中的模板。

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

history.back()和切换到以前的状态往往不是你想要的效果。 例如,如果您使用选项卡来创build表单,并且每个选项卡都有自己的状态,则只需切换上一个选项卡,而不是从表单中返回。 在嵌套状态的情况下,你通常需要考虑想要回滚的父状态的女巫。

这个指令解决了问题

 angular.module('app', ['ui-router-back']) <span ui-back='defaultState'> Go back </span> 

它返回到状态, button显示之前激活。 可选的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; }); }]);