Як отримувати сповіщення про зміни історії через 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(але я читаю про це вже тут, так я думаю).

Відповіді:


194

5.5.9.1 Визначення подій

Подія " 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


Ви сказали, що замінили весь 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, тож ви можете загорнути його чи щось таке. Мені це було не байдуже.)


4

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

Ви хочете подія browser.webNavigation.onHistoryStateUpdated, яка запускається, коли сторінка використовує historyAPI для зміни URL-адреси. Він запускається лише для сайтів, на які ви маєте дозвіл на доступ, і ви також можете використовувати фільтр URL-адрес для подальшого скорочення спаму, якщо вам потрібно. Він вимагає webNavigationдозволу (і, звичайно, дозволу хоста для відповідного домену).

Зворотний виклик події отримує ідентифікатор вкладки, URL-адресу, до якої "переходить", та інші подібні дані. Якщо вам потрібно здійснити дію у скрипті вмісту на цій сторінці, коли подія запускається, або введіть відповідний скрипт безпосередньо з фонової сторінки, або відкрийте скрипт вмісту a portна фоновій сторінці, коли він завантажується, збережіть фонову сторінку цей порт у колекції, індексований ідентифікатором вкладки, і надсилає повідомлення через відповідний порт (від фонового сценарію до сценарію вмісту), коли подія запускається.


3

Ви могли прив’язатись до window.onpopstateподії?

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

З документів:

Обробник події для popstate події на вікні.

Подія події передається у вікно щоразу, коли змінюється активний запис історії. Якщо активована запис історії була створена викликом на history.pushState () або на неї вплинув виклик до history.replaceState (), властивість стану поп-посту містить копію об'єкта стану історії запису.


6
Я спробував це вже й подія спрацьовує тільки тоді , коли користувачі йдуть в історії (або будь-які з history.go, .back) функцій називаються. Але не на pushState. Ось моя спроба протестувати це, можливо, я щось неправильно роблю : jsfiddle.net/fkling/vV9vd Здається, пов’язане лише таким чином, що якщо історія була змінена на pushState, відповідний об'єкт стану передається обробнику подій, коли інші методи називаються.
Фелікс Клінг

1
Ага. У цьому випадку єдине, про що я можу придумати, - це зареєструвати тайм-аут, щоб переглянути довжину стека історії та запустити подію, якщо розмір стека змінився.
stef

Добре, це цікава ідея. Єдине, що час очікування повинен запускатись досить часто, щоб користувач не помітив будь-якої (тривалої) затримки (мені доведеться завантажувати та показувати дані для нової URL-адреси). Я завжди намагаюся уникати тайм-аутів та опитування, де це можливо, але дотепер це здається єдиним рішенням. Я ще буду чекати інших пропозицій. Але дуже дякую зараз!
Фелікс Клінг

Це може бути навіть достатньо, щоб перевірити довжину палички історії при кожному натисканні.
Фелікс Клінг

1
Так - це спрацювало б. У мене з цим було грати - одне питання - це виклик substituState, який би не кидав подію, оскільки розмір стека не змінився б.
stef

1

Оскільки ви запитуєте про додаток Firefox, ось код, який я почав працювати. Використання не unsafeWindowє більше не рекомендується , і помилки поза коли PushState викликається з клієнтського сценарію після того , зміни:

Немає дозволу на доступ до історії ресурсів.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});

В останньому рядку слід змінити unsafeWindow.history,на window.history. Для мене це не працює з небезпечним вікном.
Ліндсей-Нудс-Сон

0

відповідь мавпи на відповідь galambalazswindow.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 Опитування не приємне, але потворне. Але добре, у вас не було іншого вибору.
Яїпропро

Це набагато краще, ніж виправлення мавп, виправлення мавпи можна скасувати іншим сценарієм, і ви не отримаєте сповіщень про нього.
Іван Кастельянос,

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. Але є коментар з попередженням про те, що "ТАКОЖ спрацьовує для pushState". Тому нам потрібно зробити ще одну фільтрацію, щоб переконатись, що це просто тісно.

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

І: ресурс: //gre/modules/WebNavigationContent.js


Ця відповідь не має нічого спільного з коментарем, якщо я не помиляюся. WebProgressListeners призначені для розширень FF? Це питання стосується історії HTML5
Kloar

@Kloar дозволяє видалити ці коментарі, будь ласка
Noitidart

0

Ну, я бачу багато прикладів заміни 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


Я щойно відредагував свою відповідь із запитаною інформацією, будь ласка, перевірте!
Віктор Кейроз

@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"); 

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, ви не отримаєте тригер події (але якщо це станеться, слід перевірити свій код)


-5

Як стандартні штати:

Зауважте, що лише виклик history.pushState () або history.replaceState () не спричинить поп-державну подію. Подія "popstate" викликається лише виконанням дій браузера, таких як натискання кнопки "Назад" (або виклик історії.back () у JavaScript)

нам потрібно викликати history.back () до тригера WindowEventHandlers.onpopstate

Тож наполягали на:

history.pushState(...)

робити:

history.pushState(...)
history.pushState(...)
history.back()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.