javascript 如何在 Backbone.js 应用程序中处理 hashchange 的滚动位置?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/11216392/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to handle scroll position on hashchange in Backbone.js application?
提问by schematic
I've read through many related threads but none of them seem to provide a solution.
我已经阅读了许多相关的线程,但它们似乎都没有提供解决方案。
What I'm trying to do is handle the scrollbar intelligently in my Backbone.js app. Like many others, I have multiple #mypage hash routes. Some of these routes are hierarchical. e.g. I have a #list page that lists some items, I click on an item in the list. Then it opens up a #view/ITEMID page.
我想要做的是在我的 Backbone.js 应用程序中智能地处理滚动条。像许多其他人一样,我有多个 #mypage 哈希路由。其中一些路由是分层的。例如,我有一个列出一些项目的#list 页面,我单击列表中的一个项目。然后它打开一个#view/ITEMID 页面。
My pages all share the same Content div in the HTML layout. On a navigation change, I inject a new div representing the view for that route into the Content div, replacing whatever was there before.
我的页面都在 HTML 布局中共享相同的 Content div。在导航更改时,我将一个代表该路线视图的新 div 注入内容 div,替换之前的任何内容。
So now my problem:
所以现在我的问题:
If the item is far down in the list I might have to scroll to get there. When I click on it, the "default" Backbone behavior is that the #view/ITEMID page is displayed at the same scroll position that the #list view was. Fixing that is easy enough; just add a $(document).scrollTop(0) whenever a new view is injected.
如果该项目在列表中很靠后,我可能需要滚动才能到达那里。当我点击它时,“默认”Backbone 行为是 #view/ITEMID 页面显示在与 #list 视图相同的滚动位置。修复它很容易;只需在注入新视图时添加 $(document).scrollTop(0) 即可。
The problem is if I hit the back button I would like to go back to the #list view at the scroll position it was previously.
问题是,如果我点击后退按钮,我想回到以前滚动位置的#list 视图。
I tried to take the obvious solution to this. Storing a map of routes to scroll positions in memory. I write to this map at the beginning of the handler for the hashchange event, but before the new view is actually put into the DOM. I read from the map at the end of the hashchange handler, after the new view is in the DOM.
我试图对此采取明显的解决方案。存储路线图以在内存中滚动位置。我在 hashchange 事件的处理程序开始时写入此映射,但在新视图实际放入 DOM 之前。在新视图在 DOM 中之后,我在 hashchange 处理程序的末尾从地图中读取。
What I'm noticing is that something, somewhere, in Firefox at least, is scrolling the page as part of a hashchange event, so that by the time my write-to-map code gets called, the document has a wonky scroll position that was definitely not explicitly made by the user.
我注意到的是,在某个地方,至少在 Firefox 中,滚动页面作为 hashchange 事件的一部分,所以当我的 write-to-map 代码被调用时,文档有一个不稳定的滚动位置绝对不是用户明确制作的。
Anyone know how to fix this, or a best practice that I should be using instead?
任何人都知道如何解决这个问题,或者我应该使用的最佳实践?
I double checked and there are no anchor tags in my DOM that match the hashes I'm using.
我仔细检查过,我的 DOM 中没有与我使用的哈希匹配的锚标记。
采纳答案by schematic
My solution to this ended up being something less automatic than I wanted, but at least it's consistent.
我对此的解决方案最终没有像我想要的那样自动,但至少它是一致的。
This was my code for saving and restoring. This code was pretty much carried from my attempt over to my actual solution, just called it on different events. "soft" is a flag that this came from a browser action (back, forward, or hash click) as opposed to a "hard" call to Router.navigate(). During a navigate() call I wanted to just scroll to the top.
这是我的保存和恢复代码。这段代码几乎是从我的尝试到我的实际解决方案的,只是在不同的事件上调用它。“软”是一个标志,表示这来自浏览器操作(后退、前进或散列单击),而不是对 Router.navigate() 的“硬”调用。在导航()调用期间,我只想滚动到顶部。
restoreScrollPosition: function(route, soft) {
var pos = 0;
if (soft) {
if (this.routesToScrollPositions[route]) {
pos = this.routesToScrollPositions[route];
}
}
else {
delete this.routesToScrollPositions[route];
}
$(window).scrollTop(pos);
},
saveScrollPosition: function(route) {
var pos = $(window).scrollTop();
this.routesToScrollPositions[route] = pos;
}
I also modified Backbone.History so that we can tell the difference between reacting to a "soft" history change (which calls checkUrl) versus programmatically triggering a "hard" history change. It passes this flag to the Router callback.
我还修改了 Backbone.History 以便我们可以区分对“软”历史更改(调用 checkUrl)做出反应与以编程方式触发“硬”历史更改之间的区别。它将此标志传递给路由器回调。
_.extend(Backbone.History.prototype, {
// react to a back/forward button, or an href click. a "soft" route
checkUrl: function(e) {
var current = this.getFragment();
if (current == this.fragment && this.iframe)
current = this.getFragment(this.getHash(this.iframe));
if (current == this.fragment) return false;
if (this.iframe) this.navigate(current);
// CHANGE: tell loadUrl this is a soft route
this.loadUrl(undefined, true) || this.loadUrl(this.getHash(), true);
},
// this is called in the whether a soft route or a hard Router.navigate call
loadUrl: function(fragmentOverride, soft) {
var fragment = this.fragment = this.getFragment(fragmentOverride);
var matched = _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
// CHANGE: tell Router if this was a soft route
handler.callback(fragment, soft);
return true;
}
});
return matched;
},
});
Originally I was trying to do the scroll saving and restoring entirely during the hashchange handler. More specifically, within Router's callback wrapper, the anonymous function that invokes your actual route handler.
最初,我试图在 hashchange 处理程序期间完全进行滚动保存和恢复。更具体地说,在路由器的回调包装器中,调用实际路由处理程序的匿名函数。
route: function(route, name, callback) {
Backbone.history || (Backbone.history = new Backbone.History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment, soft) {
// CHANGE: save scroll position of old route prior to invoking callback
// & changing DOM
displayManager.saveScrollPosition(foo.lastRoute);
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
// CHANGE: restore scroll position of current route after DOM was changed
// in callback
displayManager.restoreScrollPosition(fragment, soft);
foo.lastRoute = fragment;
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
},
I wanted to handle things this way because it allows saving in all cases, whether an href click, back button, forward button, or navigate() call.
我想以这种方式处理事情,因为它允许在所有情况下进行保存,无论是 href 单击、后退按钮、前进按钮还是navigate() 调用。
The browser has a "feature" that tries to remember your scroll on a hashchange, and move to it when going back to a hash. Normally this would have been great, and would save me all the trouble of implementing it myself. The problem is my app, like many, changes the height of the DOM from page to page.
浏览器有一个“功能”,它会尝试记住您在 hashchange 上的滚动,并在返回 hash 时移动到它。通常这会很棒,并且可以省去我自己实施它的所有麻烦。问题是我的应用程序和许多应用程序一样,会在页面之间更改 DOM 的高度。
For example, I'm on a tall #list view and have scrolled to the bottom, then click an item and go to a short #detail view that has no scrollbar at all. When I press the Back button, the browser will try to scroll me to the last position I was for the #list view. But the document isn't that tall yet, so it is unable to do so. By the time my route for #list gets called and I re-show the list, the scroll position is lost.
例如,我在一个很高的#list 视图上并滚动到底部,然后单击一个项目并转到一个根本没有滚动条的短#detail 视图。当我按下“后退”按钮时,浏览器会尝试将我滚动到 #list 视图的最后一个位置。但是文件还没有那么高,所以做不到。当我的#list 路由被调用并重新显示列表时,滚动位置丢失了。
So, couldn't use the browser's built-in scroll memory. Unless I made the document a fixed height or did some DOM trickery, which I didn't want to do.
因此,无法使用浏览器的内置滚动内存。除非我将文档设置为固定高度或做了一些我不想做的 DOM 技巧。
Moreover that built-in scroll behavior messes up the above attempt, because the call to saveScrollPosition is made too late--the browser has already changed the scroll position by then.
此外,内置滚动行为会破坏上述尝试,因为对 saveScrollPosition 的调用太晚了——那时浏览器已经改变了滚动位置。
The solution to this, which should have been obvious, was calling saveScrollPosition from Router.navigate() instead of the route callback wrapper. This guarantees that I'm saving the scroll position before the browser does anything on hashchange.
这个应该很明显的解决方案是从 Router.navigate() 而不是路由回调包装器调用 saveScrollPosition 。这保证了我在浏览器对 hashchange 执行任何操作之前保存滚动位置。
route: function(route, name, callback) {
Backbone.history || (Backbone.history = new Backbone.History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment, soft) {
// CHANGE: don't saveScrollPosition at this point, it's too late.
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
// CHANGE: restore scroll position of current route after DOM was changed
// in callback
displayManager.restoreScrollPosition(fragment, soft);
foo.lastRoute = fragment;
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
},
navigate: function(route, options) {
// CHANGE: save scroll position prior to triggering hash change
nationalcity.displayManager.saveScrollPosition(foo.lastRoute);
Backbone.Router.prototype.navigate.call(this, route, options);
},
Unfortunately it also means I always have to explicitly call navigate() if I'm interested in saving scroll position, as opposed to just using href="#myhash" in my templates.
不幸的是,这也意味着如果我对保存滚动位置感兴趣,我总是必须显式调用 navigate(),而不是在我的模板中使用 href="#myhash"。
Oh well. It works. :-)
那好吧。有用。:-)
回答by michaelpoltorak
A simple solution: Store the position of the list view on every scroll event in a variable:
一个简单的解决方案:将列表视图在每个滚动事件上的位置存储在一个变量中:
var pos;
$(window).scroll(function() {
pos = window.pageYOffset;
});
When returning from the item view, scroll the list view to the stored position:
从项目视图返回时,将列表视图滚动到存储位置:
window.scrollTo(0, pos);
回答by Saroj Yadav
@Mirage114 Thanks for posting your solution. It works like a charm. Just a minor thing, it assumes the route functions are synchronous. If there is an async operation, for example fetching remote data before rendering a view, then window is scrolled before the view content is added to the DOM. In my case, I cache data when a route is visited for the first time. This is so that when a user hits browser back/forward button, the async operation of fetching data is avoided. However it might not always be possible to cache every data you need for a route.
@Mirage114 感谢您发布您的解决方案。它就像一个魅力。只是一件小事,它假设路由功能是同步的。如果存在异步操作,例如在呈现视图之前获取远程数据,则在将视图内容添加到 DOM 之前滚动窗口。就我而言,我在第一次访问路线时缓存数据。这是为了当用户点击浏览器后退/前进按钮时,避免了获取数据的异步操作。但是,可能并不总是可以缓存路由所需的所有数据。
回答by Jason Walton
I have a slightly poor-man's fix for this. In my app, I had a similar problem. I solved it by putting the list view and the item view into a container with:
我有一个稍微可怜的人解决这个问题。在我的应用程序中,我遇到了类似的问题。我通过将列表视图和项目视图放入一个容器来解决它:
height: 100%
Then I set both the list view and the item view to have:
然后我将列表视图和项目视图都设置为:
overflow-y: auto
height: 100%
Then when I click on an item, I hide the list and show the item view. This way when I close the item and go back to the list, I keep my place in the list. It works with the back button once, although obviously it doesn't keep your history, so multiple back button clicks won't get you where you need to be. Still, a solution with no JS, so if it's good enough...
然后当我点击一个项目时,我隐藏列表并显示项目视图。这样,当我关闭项目并返回列表时,我会保留我在列表中的位置。它与后退按钮一起使用一次,但显然它不会保留您的历史记录,因此多次后退按钮点击不会让您到达需要的位置。尽管如此,一个没有 JS 的解决方案,所以如果它足够好......