如何在Backbone.js应用程序中处理hashchange上的滚动位置?

我已阅读了许多相关的主题,但似乎没有一个提供解决方案。

我想要做的是在我的Backbone.js应用程序中智能地处理滚动条。 像许多其他人一样,我有多个#mypage哈希路由。 其中一些路线是分层的。 例如,我有一个列出一些项目的#list页面,我点击列表中的项目。 然后它打开一个#view / ITEMID页面。

我的页面在HTML布局中共享相同的Content div。 在导航更改中,我将一个新的div注入到Content div中,代替该路径的视图,替换之前的任何内容。

所以现在我的问题:

如果项目在列表中很远,我可能需要滚动才能到达那里。 当我点击它时,“默认”Backbone行为是#view / ITEMID页面显示在#list视图所在的相同滚动位置。 修复很容易; 只要注入新视图,只需添加$(文档).scrollTop(0)。

问题是,如果我按下后退按钮,我想回到之前滚动位置的#list视图。

我试图采取明显的解决方案。 存储路线图以滚动内存中的位置。 我在hashchange事件的处理程序开头写入此映射,但在将新视图实际放入DOM之前。 在新视图位于DOM之后,我从hashchange处理程序末尾的地图中读取。

我注意到的是,至少在Firefox中的某个地方,滚动页面作为hashchange事件的一部分,因此当我的写入地图代码被调用时,文档的滚动位置不稳定。绝对没有用户明确提出。

任何人都知道如何解决这个问题,或者我应该使用的最佳做法?

我仔细检查过,我的DOM中没有与我正在使用的哈希相匹配的锚标签。

我的解决方案最终变得不那么自动了,但至少它是一致的。

这是我保存和恢复的代码。 这段代码几乎从我对实际解决方案的尝试中获得,只是在不同的事件上调用它。 “soft”是一个标志,它来自浏览器操作(后退,前进或哈希点击),而不是对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; } 

我还修改了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; }, }); 

最初我试图在hashchange处理程序中完全保存和恢复滚动。 更具体地说,在Router的回调包装器中,调用实际路由处理程序的匿名函数。

 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; }, 

我想以这种方式处理事情,因为它允许在所有情况下保存,无论是href单击,后退按钮,前进按钮还是导航()调用。

浏览器有一个“function”,它试图记住您在哈希变换上的滚动,并在返回哈希时移动到它。 通常这会很棒,并且会省去我自己实施它的所有麻烦。 问题是我的应用程序与许多人一样,会在页面之间更改DOM的高度。

例如,我在一个很高的#list视图并滚动到底部,然后单击一个项目并转到一个没有滚动条的简短#detail视图。 当我按下后退按钮时,浏览器会尝试将我滚动到我为#list视图的最后位置。 但是文件还不高,所以它无法这样做。 当我的#list路由被调用并且我重新显示列表时,滚动位置将丢失。

所以,无法使用浏览器的内置滚动内存。 除非我将文档设置为固定高度或做了一些DOM技巧,我不想这样做。

此外,内置的滚动行为会扰乱上面的尝试,因为对saveScrollPosition的调用太迟了 – 浏览器已经改变了滚动位置。

应该很明显的解决方案是从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); }, 

不幸的是,这也意味着如果我对保存滚动位置感兴趣,而不是在模板中使用href =“#myhash”,我总是必须显式调用navigate()。

那好吧。 有用。 🙂

一个简单的解决方案:将列表视图的位置存储在变量中的每个滚动事件中:

 var pos; $(window).scroll(function() { pos = window.pageYOffset; }); 

从项目视图返回时,将列表视图滚动到存储位置:

 window.scrollTo(0, pos); 

我有一个稍微穷人的解决方案。 在我的应用程序中,我遇到了类似的问题。 我通过将列表视图和项目视图放入一个容器来解决它:

 height: 100% 

然后我将列表视图和项目视图都设置为:

 overflow-y: auto height: 100% 

然后当我点击一个项目时,我隐藏了列表并显示了项目视图。 这样,当我关闭项目并返回列表时,我将我的位置保留在列表中。 它可以使用后退按钮一次,但显然它不会保留您的历史记录,因此多次后退按钮点击将无法满足您的需求。 仍然是一个没有JS的解决方案,所以如果它足够好……

@ Mirage114感谢您发布解决方案。 它就像一个魅力。 只是一件小事,它假设路线function是同步的。 如果存在异步操作,例如在呈现视图之前获取远程数据,则在将视图内容添加到DOM之前滚动窗口。 就我而言,我是第一次访问路由时缓存数据。 这使得当用户点击浏览器后退/前进按钮时,避免了获取数据的异步操作。 但是,可能无法始终缓存路由所需的每个数据。