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

154

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

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

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

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

Обновить:

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

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

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

Феликс Клинг
источник

Ответы:

194

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

gblazex
источник
Вы сказали, что заменили весь historyобъект. Это может быть ненужным в этом случае.
gblazex
@galambalazs: Да, наверное. Возможно (не знаю) только для window.historyчтения, но свойства объекта истории нет ... Спасибо большое за это решение :)
Феликс Клинг
Согласно спецификации, существует событие "pagetransition", которое, кажется, еще не реализовано.
Мохамед Мансур
2
@ user280109 - Я бы сказал тебе, если бы знал. :) Я думаю, что нет никакого способа сделать это в Opera atm.
gblazex
1
@cprcrack Это для поддержания чистоты глобального охвата. Вы должны сохранить нативный pushStateметод для последующего использования. Поэтому вместо глобальной переменной я решил инкапсулировать весь код во IIFE: en.wikipedia.org/wiki/Immediately-invoked_function_expression
gblazex
4

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

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!');
});

Это почти так же, как галамбалазы .

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

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

Rudie
источник
4

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

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

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

CBHacking
источник
3

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

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

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

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

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

Stef
источник
6
Я попытался это уже и событие срабатывает только тогда , когда пользователи уходят в истории (или любые из history.go, .back) функций называются. Но не вкл pushState. Вот моя попытка проверить это, может быть, я что-то не так делаю : jsfiddle.net/fkling/vV9vd Кажется, что это связано только с тем, что если история была изменена pushState, соответствующий объект состояния передается в обработчик событий, когда другие методы называются.
Феликс Клинг
1
Ах. В этом случае единственное, о чем я могу думать, - это зарегистрировать тайм-аут, чтобы посмотреть на длину стека истории и запустить событие, если размер стека изменился.
Стеф
Хорошо, это интересная идея. Единственное, что тайм-аут должен срабатывать достаточно часто, чтобы пользователь не заметил (долгой) задержки (мне нужно загрузить и показать данные для нового URL). Я всегда стараюсь избегать таймаутов и опросов, где это возможно, но до сих пор это кажется единственным решением. Я все еще буду ждать других предложений. Но большое спасибо сейчас!
Феликс Клинг
Этого может быть даже достаточно, чтобы проверить длину истории на каждом клике.
Феликс Клинг
1
Да, это будет работать. У меня была игра с этим - одна проблема - это вызов replaceState, который не вызовет событие, потому что размер стека не изменился бы.
Stef
1

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

В доступе отказано в доступе к свойству 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});
nathancahill
источник
В последней строке вы должны изменить unsafeWindow.history,на window.history. Это не работает для меня с unsafeWindow.
Линдсей-Потребность-Сон
0

ответ галамбалаза и обезьяна исправляет 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);
})();
Флимм
источник
есть ли альтернатива для голосования?
SuperUberDuper
@SuperUberDuper: см. Другие ответы.
Flimm
@Flimm Polling - это не красиво, но ужасно. Но у тебя не было выбора, который ты сказал.
Яиропро
Это намного лучше, чем исправление обезьяны, исправление обезьяны может быть отменено другим скриптом, и вы НЕ получите уведомлений об этом.
Иван Кастелланос
0

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

Я уверен, что 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's. Но есть комментарий, предупреждающий, что он «ТАКЖЕ запускает pushState». Поэтому нам нужно сделать еще несколько фильтров, чтобы убедиться, что это просто pushstate.

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

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

Noitidart
источник
Этот ответ не имеет ничего общего с комментарием, если я не ошибаюсь. WebProgressListeners для расширений FF? Этот вопрос об истории HTML5
Kloar
@Kloar, давайте удалим эти комментарии, пожалуйста
Noitidart
0

Ну, я вижу много примеров замены pushStateсвойства, historyно я не уверен, что это хорошая идея, я бы предпочел создать сервисное событие, основанное на API, похожем на историю, чтобы вы могли контролировать не только состояние push, но и состояние замены а также он открывает двери для многих других реализаций, не опирающихся на 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

Виктор Кейроз
источник
Я только что отредактировал свой ответ с запрашиваемой информацией, пожалуйста, проверьте!
Виктор
@VictorQueiroz Это хорошая и чистая идея для инкапсуляции функций. Но если какая-то третья часть библиотеки вызывает pushState, ваш HistoryApi не будет уведомлен.
Яиропро
0

Я бы предпочел не переписывать собственный метод истории, поэтому эта простая реализация создает мою собственную функцию под названием eventedPush state, которая просто отправляет событие и возвращает history.pushState (). В любом случае, все работает хорошо, но я считаю, что эта реализация немного чище, поскольку нативные методы будут продолжать работать так, как ожидают будущие разработчики.

function eventedPushState(state, title, url) {
    var pushChangeEvent = new CustomEvent("onpushstate", {
        detail: {
            state,
            title,
            url
        }
    });
    document.dispatchEvent(pushChangeEvent);
    return history.pushState(state, title, url);
}

document.addEventListener(
    "onpushstate",
    function(event) {
        console.log(event.detail);
    },
    false
);

eventedPushState({}, "", "new-slug"); 
jopfre
источник
0

Основываясь на решении, предоставленном @gblazex , если вы хотите следовать тому же подходу, но с использованием функций стрелок, следуйте приведенному ниже примеру в вашей логике javascript:

private _currentPath:string;    
((history) => {
          //tracks "forward" navigation event
          var pushState = history.pushState;
          history.pushState =(state, key, path) => {
              this._notifyNewUrl(path);
              return pushState.apply(history,[state,key,path]); 
          };
        })(window.history);

//tracks "back" navigation event
window.addEventListener('popstate', (e)=> {
  this._onUrlChange();
});

Затем реализуйте другую функцию, _notifyUrl(url)которая запускает любое необходимое действие, которое может вам понадобиться при обновлении URL текущей страницы (даже если страница вообще не была загружена)

  private _notifyNewUrl (key:string = window.location.pathname): void {
    this._path=key;
    // trigger whatever you need to do on url changes
    console.debug(`current query: ${this._path}`);
  }
Альберто С.
источник
0

Так как я просто хотел новый URL, я адаптировал коды @gblazex и @Alberto S., чтобы получить это:

(function(history){

  var pushState = history.pushState;
    history.pushState = function(state, key, path) {
    if (typeof history.onpushstate == "function") {
      history.onpushstate({state: state, path: path})
    }
    pushState.apply(history, arguments)
  }
  
  window.onpopstate = history.onpushstate = function(e) {
    console.log(e.path)
  }

})(window.history);
Андре Лопес
источник
0

Я не думаю, что это хорошая идея - модифицировать нативные функции, даже если вы можете, и вы всегда должны сохранять область своего приложения, поэтому хороший подход - не использовать глобальную функцию pushState, вместо этого используйте одну из ваших собственных:

function historyEventHandler(state){ 
    // your stuff here
} 

window.onpopstate = history.onpushstate = historyEventHandler

function pushHistory(...args){
    history.pushState(...args)
    historyEventHandler(...args)
}
<button onclick="pushHistory(...)">Go to happy place</button>

Обратите внимание, что если любой другой код использует встроенную функцию pushState, вы не получите триггер события (но если это произойдет, вам следует проверить свой код)

Maxwell sc
источник
-5

Стандартно говорится:

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

нам нужно вызвать history.back () для запуска WindowEventHandlers.onpopstate

Итак, о

history.pushState(...)

делать:

history.pushState(...)
history.pushState(...)
history.back()
user2360102
источник