防止在 Backbone 驱动的 SPA 中进行页面导航

Preventing page navigation inside a Backbone-driven SPA

理由

在我的 BB 应用程序中,我允许用户快速输入,这些输入会在后台定期排队并发送到服务器。我目前遇到的问题是,如果用户离开页面,他们实际上会丢弃队列中的任何未决更改。

所以基本上我想做的是在用户他们离开之前通知他们,让他们有机会等待更改被保存,而不是仅仅退出和丢弃。

细节

因此对于用户刷新或尝试导航到 外部 URL 的一般情况,我们可以处理 onbeforeunload 事件。当我们处于 SPA 上下文中时,在页面之间切换不会导致页面刷新时,情况会变得有些棘手。

我的直接想法是对所有锚点使用全局点击事件处理程序并验证我是否要允许点击,这适用于现场 link 导航。然而,这个失败的地方是通过浏览器 Back/Forward 按钮导航。

我也看了Backbone.routefilter, which at first glance appeared to do exactly what I needed. However, using the simple case as described in the docs,路由还在执行

问题

我们如何拦截 Backbone SPA 中所有场景的导航?

直接link导航

使用全局事件处理程序捕获所有 点击事件

$(document).on('click', 'a[href^="/"]', function (e) {
    var href = $(e.currentTarget).attr('href');
    e.preventDefault();
    if (doSomeValidation()) {
        router.navigate(href, { trigger: true });
    }
});

页面刷新/外部URL导航

处理 window

上的 onbeforeunload 事件
$(window).on('beforeunload', function (e) {
    if (!doSomeValidation()) {
        return 'Leaving now will may result in data loss';
    }
});

浏览器back/forward按钮导航

在幕后 Backbone.Router uses the Backbone.history which ultimately leverages the HTML5 pushstate API. Depending on what options you pass to Backbone.history.start,根据您的浏览器的能力,API 将挂接到 onhashchange 事件或 onpopstate 事件。

深入研究 Backbone.history.start 的源代码,很明显无论您是否使用推送状态,都使用相同的事件处理程序,即 checkUrl

if (this._hasPushState) {
    addEventListener('popstate', this.checkUrl, false);
} else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
    addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
    this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}

因此,我们可以覆盖此方法并在其中执行验证

var originalCheckUrl = Backbone.history.checkUrl;
Backbone.history.checkUrl = function (e) {
    if (doSomeValidation()) {
        return originalCheckUrl.call(this, e);
    } else {
        // re-push the current page into the history (at this stage it's been popped)
        window.history.pushState({}, document.title, Backbone.history.fragment);
        // cancel the original event
        return false;
    }
};