27

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

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

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

所以现在我的问题:

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

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

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

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

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

我仔细检查了一下,我的 DOM 中没有与我正在使用的哈希匹配的锚标记。

4

4 回答 4

14

我对此的解决方案最终没有我想要的那么自动化,但至少它是一致的。

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

那好吧。有用。:-)

于 2012-07-01T18:02:36.847 回答
5

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

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

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

window.scrollTo(0, pos);
于 2013-06-04T19:08:53.857 回答
0

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

height: 100%

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

overflow-y: auto
height: 100%

然后,当我单击一个项目时,我会隐藏列表并显示项目视图。这样,当我关闭项目并返回列表时,我就可以保留我在列表中的位置。它与后退按钮一起工作一次,但显然它不会保留您的历史记录,因此多次单击后退按钮不会让您到达您需要的位置。尽管如此,没有JS的解决方案,所以如果它足够好......

于 2013-04-10T21:06:03.393 回答
0

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

于 2013-12-28T17:13:45.187 回答