Bagaimana cara mendapat pemberitahuan tentang perubahan riwayat melalui history.pushState?

154

Jadi sekarang HTML5 memperkenalkan history.pushStateuntuk mengubah riwayat browser, situs web mulai menggunakan ini dalam kombinasi dengan Ajax alih-alih mengubah pengidentifikasi fragmen URL.

Sayangnya itu berarti bahwa panggilan tersebut tidak dapat dideteksi lagi oleh onhashchange.

Pertanyaan saya adalah: Apakah ada cara yang dapat diandalkan (hack?;)) Untuk mendeteksi kapan situs web menggunakan history.pushState? Spesifikasinya tidak menyatakan apa pun tentang peristiwa yang dimunculkan (setidaknya saya tidak dapat menemukan apa pun).
Saya mencoba membuat fasad dan diganti window.historydengan objek JavaScript saya sendiri, tetapi tidak memiliki efek sama sekali.

Penjelasan lebih lanjut: Saya sedang mengembangkan add-on Firefox yang perlu mendeteksi perubahan ini dan bertindak sesuai.
Saya tahu ada pertanyaan serupa beberapa hari yang lalu yang menanyakan apakah mendengarkan beberapa acara DOM akan efisien tetapi saya lebih suka tidak mengandalkan itu karena peristiwa ini dapat dihasilkan karena banyak alasan berbeda.

Memperbarui:

Berikut ini adalah jsfiddle (gunakan Firefox 4 atau Chrome 8) yang menunjukkan bahwa onpopstatetidak terpicu ketika pushStatedipanggil (atau saya melakukan sesuatu yang salah? Jangan ragu untuk memperbaikinya!).

Pembaruan 2:

Masalah (sisi) lain adalah yang window.locationtidak diperbarui saat menggunakan pushState(tapi saya membaca tentang ini sudah ada di sini pada SO saya pikir).

Felix Kling
sumber

Jawaban:

194

5.5.9.1 Definisi acara

Acara popstate dipecat dalam kasus tertentu ketika menavigasi ke entri riwayat sesi.

Menurut ini, tidak ada alasan popstate untuk dipecat saat Anda menggunakan pushState. Tetapi acara seperti itu pushstateakan berguna. Karena historyini adalah objek host, Anda harus berhati-hati dengannya, tetapi Firefox tampaknya baik dalam hal ini. Kode ini berfungsi dengan baik:

(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 Anda menjadi :

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

Anda bisa menambal monyet window.history.replaceStatedengan cara yang sama.

Catatan: tentu saja Anda dapat menambahkan onpushstatehanya ke objek global, dan Anda bahkan dapat membuatnya menangani lebih banyak acara melaluiadd/removeListener

gblazex
sumber
Anda bilang Anda mengganti seluruh historyobjek. Itu mungkin tidak perlu dalam kasus ini.
gblazex
@alambalazs: Ya mungkin. Mungkin (tidak tahu) window.historyhanya bisa dibaca tetapi properti dari objek sejarah tidak ... Terima kasih banyak untuk solusi ini :)
Felix Kling
Menurut spec ada acara "pagetransition", sepertinya belum diimplementasikan.
Mohamed Mansour
2
@ user280109 - Saya akan memberi tahu Anda jika saya tahu. :) Saya pikir tidak ada cara untuk melakukan ini di Opera atm.
gblazex
1
@cprcrack Ini untuk menjaga ruang lingkup global tetap bersih. Anda harus menyimpan pushStatemetode asli untuk digunakan nanti. Jadi, alih-alih variabel global, saya memilih untuk merangkum seluruh kode menjadi IIFE: en.wikipedia.org/wiki/Immediately-invoked_function_expression
gblazex
4

Saya menggunakan ini:

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

Ini hampir sama dengan galambalaz .

Tapi biasanya itu berlebihan. Dan itu mungkin tidak bekerja di semua browser. (Saya hanya peduli dengan versi browser saya.)

(Dan itu meninggalkan var _wr, jadi kamu mungkin ingin membungkusnya atau sesuatu. Aku tidak peduli tentang itu.)

Rudie
sumber
4

Akhirnya menemukan cara yang "benar" untuk melakukan ini! Perlu menambahkan hak istimewa untuk ekstensi Anda dan menggunakan halaman latar belakang (bukan hanya skrip konten), tetapi itu berfungsi.

Acara yang Anda inginkan adalah browser.webNavigation.onHistoryStateUpdated, yang dipecat ketika halaman menggunakan historyAPI untuk mengubah URL. Ini hanya diaktifkan untuk situs yang Anda memiliki izin untuk mengakses, dan Anda juga dapat menggunakan filter URL untuk lebih mengurangi spam jika Anda perlu. Ini memerlukan webNavigationizin (dan tentu saja izin host untuk domain yang relevan).

Callback acara mendapatkan ID tab, URL yang sedang "dinavigasi", dan detail lainnya. Jika Anda perlu mengambil tindakan dalam skrip konten pada halaman tersebut saat acara dimulai, menyuntikkan skrip yang relevan langsung dari halaman latar belakang, atau meminta skrip konten membuka a portke halaman latar belakang saat memuat, minta halaman latar disimpan port itu dalam koleksi yang diindeks oleh ID tab, dan mengirim pesan melintasi port yang relevan (dari skrip latar belakang ke skrip konten) saat acara diaktifkan.

CBHacking
sumber
3

Anda dapat mengikat ke window.onpopstateacara tersebut?

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

Dari dokumen:

Penangan acara untuk acara popstate di jendela.

Acara popstate dikirim ke jendela setiap kali entri riwayat aktif berubah. Jika entri riwayat yang diaktifkan dibuat oleh panggilan ke history.pushState () atau dipengaruhi oleh panggilan ke history.replaceState (), properti keadaan acara popstate berisi salinan objek keadaan entri sejarah.

Stefan
sumber
6
Saya sudah mencoba ini dan acara ini hanya dipicu ketika pengguna kembali dalam sejarah (atau salah satu dari history.go, .back) fungsi dipanggil. Tapi tidak pushState. Ini adalah usaha saya untuk mengujinya, mungkin saya melakukan sesuatu yang salah: jsfiddle.net/fkling/vV9vd Tampaknya hanya terkait dengan cara itu bahwa jika sejarah diubah oleh pushState, objek keadaan terkait diteruskan ke event handler ketika metode lain disebut.
Felix Kling
1
Ah. Dalam hal itu satu-satunya hal yang dapat saya pikirkan adalah mendaftarkan batas waktu untuk melihat panjang tumpukan riwayat dan menjalankan suatu peristiwa jika ukuran tumpukan telah berubah.
Stefan
Ok, ini ide yang menarik. Satu-satunya hal adalah batas waktu harus cukup sering menyala sehingga pengguna tidak akan melihat penundaan (lama) (saya harus memuat dan menampilkan data untuk URL baru). Saya selalu berusaha menghindari batas waktu dan pemungutan suara jika memungkinkan, tetapi sampai sekarang ini tampaknya menjadi satu-satunya solusi. Saya masih akan menunggu proposal lainnya. Tapi terima kasih banyak untuk saat ini!
Felix Kling
Mungkin bahkan cukup untuk memeriksa panjang tongkat sejarah pada setiap klik.
Felix Kling
1
Ya - itu akan berhasil. Saya sudah bermain-main dengan ini - satu masalah adalah panggilan replaceState yang tidak akan melempar peristiwa karena ukuran tumpukan tidak akan berubah.
stef
1

Karena Anda bertanya tentang addon Firefox, inilah kode yang harus saya gunakan. Menggunakan unsafeWindowini tidak lagi direkomendasikan , dan kesalahan ketika pushState dipanggil dari script klien setelah dimodifikasi:

Izin ditolak untuk mengakses history.pushState properti

Sebagai gantinya, ada API yang disebut exportFunction yang memungkinkan fungsi untuk disuntikkan ke dalam window.historyseperti ini:

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
sumber
Di baris terakhir, Anda harus mengubah unsafeWindow.history,ke window.history. Itu tidak bekerja untuk saya dengan Windows tidak aman.
Lindsay-Needs-Sleep
0

jawaban monyet galambalazswindow.history.pushState dan window.history.replaceState, tetapi untuk beberapa alasan berhenti bekerja untuk saya. Inilah alternatif yang tidak sebaik karena menggunakan polling:

(function() {
    var previousState = window.history.state;
    setInterval(function() {
        if (previousState !== window.history.state) {
            previousState = window.history.state;
            myCallback();
        }
    }, 100);
})();
Flimm
sumber
apakah ada alternatif untuk pemungutan suara?
SuperUberDuper
@SuperUberDuper: lihat jawaban lain.
Flimm
@Fim Polling tidak bagus, tapi jelek. Tapi yah, Anda tidak punya pilihan, kata Anda.
Yairopro
Ini jauh lebih baik daripada tambalan monyet, tambalan monyet dapat dibatalkan oleh skrip lain dan Anda TIDAK mendapatkan pemberitahuan tentang hal itu.
Ivan Castellanos
0

Saya pikir topik ini membutuhkan solusi yang lebih modern.

Saya yakin nsIWebProgressListenerada sekitar saat itu saya terkejut tidak ada yang menyebutkannya.

Dari sebuah skrip frame (untuk kompatibilitas e10s):

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

Kemudian mendengarkan di onLoacationChange

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

Itu tampaknya akan menangkap semua pushState. Tetapi ada peringatan komentar bahwa itu "JUGA memicu untuk pushState". Jadi kita perlu melakukan lebih banyak penyaringan di sini untuk memastikan itu hanya hal-hal pushstate.

Berdasarkan pada: https://github.com/jgraham/gecko/blob/55d8d9aa7311386ee2dabfccb481684c8920a527/toolkit/modules/addons/WebNavigation.jsm#L18

Dan: sumber daya: //gre/modules/WebNavigationContent.js

Noitidart
sumber
Jawaban ini tidak ada hubungannya dengan komentar, kecuali saya salah. WebProgressListeners adalah untuk ekstensi FF? Pertanyaan ini tentang sejarah HTML5
Kloar
@Kloar mari kita hapus komentar ini
Noitidart
0

Yah, saya melihat banyak contoh untuk mengganti pushStateproperti historytetapi saya tidak yakin itu ide yang bagus, saya lebih suka untuk membuat acara layanan berbasis dengan API yang mirip dengan sejarah dengan cara itu Anda dapat mengontrol tidak hanya push state tetapi ganti state juga dan itu membuka pintu bagi banyak implementasi lainnya yang tidak mengandalkan API sejarah global. Silakan periksa contoh berikut:

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];
});

Jika Anda memerlukan EventEmitterdefinisi, kode di atas didasarkan pada emitor acara NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inheritsimplementasi dapat ditemukan di sini: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

Victor Queiroz
sumber
Saya baru saja mengedit jawaban saya dengan informasi yang diminta, silakan periksa!
Victor Queiroz
@ ViktorQueiroz Itu ide bagus & bersih untuk merangkum fungsi. Tetapi jika beberapa pustaka bagian ketiga menyerukan pushState, HistoryApi Anda tidak akan diberitahukan.
Yairopro
0

Saya lebih suka tidak menimpa metode sejarah asli sehingga implementasi sederhana ini membuat fungsi saya sendiri yang disebut stateedPush state yang hanya mengirimkan suatu peristiwa dan mengembalikan history.pushState (). Either way berfungsi dengan baik tetapi saya menemukan implementasi ini sedikit lebih bersih karena metode asli akan terus berkinerja seperti yang diharapkan pengembang di masa depan.

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
sumber
0

Berdasarkan solusi yang diberikan oleh @gblazex , jika Anda ingin mengikuti pendekatan yang sama, tetapi menggunakan fungsi panah, ikuti contoh di bawah ini dalam logika javascript Anda:

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();
});

Kemudian, terapkan fungsi lain _notifyUrl(url)yang memicu tindakan apa pun yang diperlukan yang mungkin Anda perlukan saat url halaman saat ini diperbarui (bahkan jika halaman belum dimuat sama sekali)

  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}`);
  }
Alberto S.
sumber
0

Karena saya hanya menginginkan URL baru, saya telah mengadaptasi kode @gblazex dan @Alberto S. untuk mendapatkan ini:

(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);
Andre Lopes
sumber
0

Saya tidak berpikir itu ide yang baik untuk memodifikasi fungsi asli bahkan jika Anda bisa, dan Anda harus selalu menjaga ruang lingkup aplikasi Anda, jadi pendekatan yang baik tidak menggunakan fungsi pushState global, sebaliknya, gunakan salah satu dari Anda sendiri:

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>

Perhatikan bahwa jika ada kode lain yang menggunakan fungsi pushState asli, Anda tidak akan mendapatkan pemicu acara (tetapi jika ini terjadi, Anda harus memeriksa kode Anda)

Maxwell sc
sumber
-5

Sebagai negara standar:

Perhatikan bahwa hanya memanggil history.pushState () atau history.replaceState () tidak akan memicu peristiwa popstate. Acara popstate hanya dipicu dengan melakukan tindakan browser seperti mengklik tombol kembali (atau memanggil history.back () dalam JavaScript)

kita perlu memanggil history.back () ke trigeer WindowEventHandlers.onpopstate

Jadi insted dari:

history.pushState(...)

melakukan:

history.pushState(...)
history.pushState(...)
history.back()
pengguna2360102
sumber