我如何获得后退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
数组( pop
和push
)。 每次调用其中一个方法时,都会阻止另一个方法被一次性调用。 这种技术不会干扰$事件的正确触发,并保持一切正常。
现在我们只需要在状态转换生命周期的适当时刻调用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 object
的popstate 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; }); }]);
- 当我使用async / awaitfunction时,Angular UI路由器不处理parsing函数?
- 在AngularJS中使用UI-Router将状态redirect到默认子状态
- ionic framework$ state.go('app.home'); 在页面上添加我想要去(如何删除它)的返回button?
- Angular js – route-ui添加默认参数
- 当用户使用UI-Router转换到其父状态时,将用户引导到子状态
- Angular UI路由器 – 如何访问从父模板传递的嵌套命名视图中的参数?
- Angular-ui-router获得之前的状态
- 清除历史logging和使用Ionic Frameworklogin/注销重新加载页面
- 带有requirejs的angular-ui-router,延迟加载控制器