Как получать уведомления об изменениях истории через history.pushState?

Итак, теперь, когда HTML5 вводит history.pushState, чтобы изменить историю браузеров, веб-сайты начинают использовать это в сочетании с Ajax вместо изменения идентификатора фрагмента URL.

К сожалению, это означает, что эти вызовы больше не могут быть обнаружены onhashchange.

Мой вопрос: Есть ли надежный способ (взломать?;)), чтобы обнаружить, когда веб-сайт использует history.pushState? В спецификации ничего не говорится о событиях, которые подняты (по крайней мере, я ничего не мог найти).
Я попытался создать фасад и заменил window.history на свой собственный объект JavaScript, но он вообще не имел никакого эффекта.

Дальнейшее объяснение: Я разрабатываю надстройку Firefox, которая должна обнаруживать эти изменения и действовать соответственно.
Я знаю, что несколько дней назад был аналогичный вопрос, который задал вопрос о том, будет ли прослушивание некоторых событий DOM, но я бы предпочел не полагаться на это, потому что эти события могут быть сгенерированы по разным причинам.

Update:

Вот jsfiddle (используйте Firefox 4 или Chrome 8), который показывает, что onpopstate не запускается при вызове pushState ( или я делаю что-то неправильно? Не стесняйтесь улучшать его!).

Обновление 2:

Другая (побочная) проблема заключается в том, что window.location не обновляется при использовании pushState (но я читал об этом уже здесь, на SO, я думаю).

Ответ 1

5.5.9.1 Определения событий

Событие popstate запускается в некоторых случаях при навигации к записи истории сеанса.

В соответствии с этим нет причин для запуска popstate при использовании pushState. Но событие, такое как pushState, пригодится. Поскольку history - это объект-хост, вы должны быть осторожны с ним, но Firefox кажется приятным в этом случае. Этот код работает очень хорошо:

(function(history){
    var pushState = history.pushState;
    history.pushState = function(state) {
        if (typeof history.onpushstate == "function") {
            history.onpushstate({state: state});
        }
        // ... whatever else you want to do
        // maybe call onhashchange e.handler
        return pushState.apply(history, arguments);
    };
})(window.history);

Ваш jsfiddle станет:

window.onpopstate = history.onpushstate = function(e) { ... }

Вы можете обезьян-патч window.history.replaceState таким же образом.

Примечание. Конечно, вы можете добавить onpushstate просто к глобальному объекту, и вы даже можете обработать больше событий с помощью add/removeListener

Ответ 2

Вы могли привязываться к событию window.onpopstate?

https://developer.mozilla.org/en/DOM%3awindow.onpopstate

Из документов:

Обработчик событий для popstate событие в окне.

Событие popstate отправляется на окно каждый раз, когда активная история изменения записи. Если запись истории активация была создана по вызову to history.pushState() или был затронут по вызову history.replaceState(), свойство состояния события popstate содержит копию записи истории объект состояния.

Ответ 3

Я использовал это:

var _wr = function(type) {
    var orig = history[type];
    return function() {
        var rv = orig.apply(this, arguments);
        var e = new Event(type);
        e.arguments = arguments;
        window.dispatchEvent(e);
        return rv;
    };
};
history.pushState = _wr('pushState'), history.replaceState = _wr('replaceState');

window.addEventListener('replaceState', function(e) {
    console.warn('THEY DID IT AGAIN!');
});

Это почти то же самое, что galambalazs.

Тем не менее, он обычно переполняется. И это может не работать во всех браузерах. (Меня интересует только моя версия моего браузера.)

(И он оставляет var _wr, поэтому вы можете его обернуть или что-то в этом роде. Меня это не волновало.)

Ответ 4

Я думаю, что эта тема нуждается в более современном решении.

Я уверен, что nsIWebProgressListener был вокруг, тогда я удивлен, что никто не упомянул об этом.

Из фреймворка (для совместимости e10s):

let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | Ci.nsIWebProgress.NOTIFY_LOCATION);

Затем прослушивание в onLoacationChange

onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
       if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT

Это, очевидно, поймает все pushState. Но есть комментарий, предупреждающий, что он "ТАКЖЕ запускает pushState". Поэтому нам нужно сделать еще несколько фильтров, чтобы убедиться, что это просто файл pushstate.

На основе: https://github.com/jgraham/gecko/blob/55d8d9aa7311386ee2dabfccb481684c8920a527/toolkit/modules/addons/WebNavigation.jsm#L18

И: resource://gre/modules/WebNavigationContent.js

Ответ 5

galambalazs answer обезьянские патчи window.history.pushState и window.history.replaceState, но по какой-то причине он перестает работать для меня. Вот альтернатива, которая не так хороша, потому что использует опрос:

(function() {
    var previousState = window.history.state;
    setInterval(function() {
        if (previousState !== window.history.state) {
            previousState = window.history.state;
            myCallback();
        }
    }, 100);
})();

Ответ 6

Поскольку вы спрашиваете о дополнении Firefox, вот код, который я должен работать. Использование unsafeWindow больше не рекомендуется и ошибки при выводе pushState из клиента script после изменения:

Отказано в доступе к ресурсу history.pushState

Вместо этого существует API под названием exportFunction, который позволяет вводить функцию в window.history следующим образом:

var pushState = history.pushState;

function pushStateHack (state) {
    if (typeof history.onpushstate == "function") {
        history.onpushstate({state: state});
    }

    return pushState.apply(history, arguments);
}

history.onpushstate = function(state) {
    // callback here
}

exportFunction(pushStateHack, unsafeWindow.history, {defineAs: 'pushState', allowCallbacks: true});

Ответ 7

Ну, я вижу много примеров замены свойства pushState history, но я не уверен, что хорошая идея, я бы предпочел создать событие службы, основанное на аналогичном API, для истории так, как вы может управлять не только нажатием состояния, но и заменой состояния, и он открывает двери для многих других реализаций, не полагающихся на глобальный API истории. Пожалуйста, проверьте следующий пример:

function HistoryAPI(history) {
    EventEmitter.call(this);
    this.history = history;
}

HistoryAPI.prototype = utils.inherits(EventEmitter.prototype);

const prototype = {
    pushState: function(state, title, pathname){
        this.emit('pushstate', state, title, pathname);
        this.history.pushState(state, title, pathname);
    },

    replaceState: function(state, title, pathname){
        this.emit('replacestate', state, title, pathname);
        this.history.replaceState(state, title, pathname);
    }
};

Object.keys(prototype).forEach(key => {
    HistoryAPI.prototype = prototype[key];
});

Если вам нужно определение EventEmitter, приведенный выше код основан на эмитенте событий NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js. utils.inherits можно найти здесь: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

Ответ 8

Наконец-то нашел "правильный" способ сделать это! Требуется добавить привилегию к вашему расширению и использовать фоновую страницу (не только скрипт контента), но это работает.

Требуемое событие - browser.webNavigation.onHistoryStateUpdated, которое вызывается, когда страница использует API history для изменения URL-адреса. Он срабатывает только для сайтов, к которым у вас есть разрешение, и вы также можете использовать URL-фильтр для дальнейшего сокращения спама, если вам это нужно.

Обратный вызов события получает идентификатор вкладки, URL, по которому осуществляется переход, и другие подобные сведения. Если вам нужно выполнить действие в сценарии содержимого на этой странице при возникновении события, либо вставьте соответствующий сценарий непосредственно с фоновой страницы, либо попросите сценарий содержимого открыть port для фоновой страницы при загрузке, создайте фоновую страницу. сохраните этот порт в коллекции, индексируемой по идентификатору вкладки, и отправьте сообщение через соответствующий порт (от фонового сценария к сценарию содержимого) при возникновении события.

Ответ 9

В качестве стандартных состояний:

Обратите внимание, что просто вызов history.pushState() или history.replaceState() не вызывает событие popstate. Событие popstate запускается только с помощью действия браузера, такого как нажатие на кнопку возврата (или вызов history.back() в JavaScript)

нам нужно вызвать history.back() для триггера WindowEventHandlers.onpopstate

Так insted из:

history.pushState(...)

делать:

history.pushState(...)
history.pushState(...)
history.back()