我对此的解决方案最终没有我想要的那么自动化,但至少它是一致的。
这是我保存和恢复的代码。这段代码几乎是从我的尝试转移到我的实际解决方案中的,只是在不同的事件中调用它。“软”是一个标志,它来自浏览器操作(后退、前进或哈希单击),而不是对 Router.navigate() 的“硬”调用。在 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 处理程序期间完全进行滚动保存和恢复。更具体地说,在路由器的回调包装器中,调用您的实际路由处理程序的匿名函数。
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 单击、后退按钮、前进按钮还是 navigate() 调用。
浏览器有一个“功能”,它试图记住你在 hashchange 上的滚动,并在返回到 hashchange 时移动到它。通常这会很棒,并且可以省去我自己实现它的所有麻烦。问题是我的应用程序和许多应用程序一样,会在页面之间更改 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);
},
不幸的是,这也意味着如果我对保存滚动位置感兴趣,我总是必须显式调用 navigate(),而不是仅在模板中使用 href="#myhash"。
那好吧。有用。:-)