จะรับการแจ้งเตือนเกี่ยวกับการเปลี่ยนแปลงของประวัติผ่าน 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เริ่มทำงานในบางกรณีเมื่อนำทางไปยังรายการประวัติเซสชัน

ตามที่นี้มีเหตุผล 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" ดูเหมือนว่ายังไม่ได้ใช้งาน
Mohamed Mansour

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

มันเกือบจะเหมือนกับที่กาแลมบาลาซได้ทำ

มันมักจะ overkill แม้ว่า และมันอาจไม่ทำงานในทุกเบราว์เซอร์ (ฉันสนใจเฉพาะเบราว์เซอร์เวอร์ชันของฉัน)

(และมันก็ทิ้ง var _wrไว้ดังนั้นคุณอาจต้องการห่อมันหรืออะไรซักอย่างฉันไม่สนใจเลย)


4

ในที่สุดก็พบวิธี "ถูกต้อง" ในการทำเช่นนี้! มันต้องเพิ่มสิทธิ์ให้กับส่วนขยายของคุณและใช้หน้าพื้นหลัง (ไม่เพียงสคริปต์เนื้อหา) แต่มันจะทำงาน

เหตุการณ์ที่คุณต้องการคือเหตุการณ์ที่เกิดขึ้นbrowser.webNavigation.onHistoryStateUpdatedเมื่อหน้าใช้historyAPI เพื่อเปลี่ยน URL มันเป็นเพียงการยิงสำหรับเว็บไซต์ที่คุณได้รับอนุญาตให้เข้าถึงและคุณยังสามารถใช้ตัวกรอง URL เพื่อลดสแปมหากคุณต้องการ จำเป็นต้องwebNavigationได้รับอนุญาต (และแน่นอนว่าต้องได้รับอนุญาตจากโฮสต์สำหรับโดเมนที่เกี่ยวข้อง)

การเรียกกลับเหตุการณ์จะได้รับ ID แท็บ, URL ที่กำลัง "นำทาง" ไปยังและรายละเอียดอื่น ๆ หากคุณต้องการดำเนินการในสคริปต์เนื้อหาในหน้านั้นเมื่อเหตุการณ์เริ่มขึ้นให้ฉีดสคริปต์ที่เกี่ยวข้องโดยตรงจากหน้าพื้นหลังหรือให้สคริปต์เนื้อหาเปิดportไปยังหน้าพื้นหลังเมื่อโหลดมีการบันทึกหน้าพื้นหลัง พอร์ตนั้นในคอลเลกชันที่จัดทำดัชนีโดย ID แท็บและส่งข้อความข้ามพอร์ตที่เกี่ยวข้อง (จากสคริปต์พื้นหลังไปยังสคริปต์เนื้อหา) เมื่อเหตุการณ์เริ่มทำงาน


3

คุณสามารถผูกกับwindow.onpopstateเหตุการณ์หรือไม่

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

จากเอกสาร:

ตัวจัดการเหตุการณ์สำหรับเหตุการณ์ popstate บนหน้าต่าง

เหตุการณ์ popstate จะถูกส่งไปที่หน้าต่างทุกครั้งที่มีการเปลี่ยนแปลงรายการประวัติที่ใช้งานอยู่ หากรายการประวัติที่ถูกเปิดใช้งานถูกสร้างขึ้นโดยการเรียกร้องให้ history.pushState () หรือได้รับผลกระทบจากการเรียกร้องให้ history.replaceState () ทรัพย์สินสถานะเหตุการณ์ของเหตุการณ์ popstate มีสำเนาของวัตถุสถานะของรายการประวัติ


6
ฉันพยายามนี้แล้วและเหตุการณ์ที่เกิดขึ้นจะถูกเรียกเฉพาะเมื่อผู้ใช้กลับไปในประวัติศาสตร์ (หรือใด ๆ ของhistory.go, .back) ฟังก์ชั่นที่เรียกว่า pushStateแต่ไม่ได้อยู่ใน นี่คือความพยายามของฉันในการทดสอบบางทีฉันอาจทำสิ่งผิดพลาด: jsfiddle.net/fkling/vV9vdดูเหมือนว่าจะเกี่ยวข้องกับวิธีการนั้นเท่านั้นหากประวัติถูกเปลี่ยนโดยpushStateวัตถุสถานะที่สอดคล้องกันจะถูกส่งไปยังตัวจัดการเหตุการณ์เมื่อวิธีอื่น ถูกเรียกว่า
เฟลิกซ์คลิง

1
อา. ในกรณีนั้นสิ่งเดียวที่ฉันคิดได้ก็คือการลงทะเบียนการหมดเวลาเพื่อดูความยาวของสแต็คประวัติและเริ่มเหตุการณ์หากขนาดสแต็คเปลี่ยนไป
stef

ตกลงนี่เป็นแนวคิดที่น่าสนใจ สิ่งเดียวคือการหมดเวลาจะต้องเกิดขึ้นบ่อยครั้งพอที่ผู้ใช้จะไม่สังเกตเห็นความล่าช้า (ยาว) ใด ๆ (ฉันต้องโหลดและแสดงข้อมูลสำหรับ URL ใหม่) ฉันพยายามหลีกเลี่ยงการหมดเวลาและการสำรวจที่เป็นไปได้เสมอ แต่จนถึงตอนนี้ดูเหมือนจะเป็นทางออกเดียว ฉันจะรอข้อเสนออื่น ๆ แต่ขอบคุณมากตอนนี้!
เฟลิกซ์คลิง

มันอาจจะเพียงพอที่จะตรวจสอบความยาวของแท่งประวัติในทุก ๆ การคลิก
เฟลิกซ์คลิง

1
ใช่ - นั่นจะใช้ได้ ฉันได้เล่นกับเรื่องนี้ - หนึ่งปัญหาคือการโทร replaceState ซึ่งจะไม่โยนเหตุการณ์เพราะขนาดของสแต็คจะไม่เปลี่ยนแปลง
stef

1

เมื่อคุณถามเกี่ยวกับ addon 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});

ในบรรทัดสุดท้ายที่คุณควรเปลี่ยนไปunsafeWindow.history, window.historyมันไม่ได้ผลสำหรับฉันกับ unsafeWindow
Lindsay-Needs-Sleep

0

คำตอบของ galambalazs เป็นตัวแก้ไข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 ไม่ดี แต่น่าเกลียด แต่คุณไม่มีทางเลือกที่คุณพูด
Yairopro

สิ่งนี้ดีกว่าการปะแก้ลิงการปะแก้ลิงสามารถยกเลิกได้โดยสคริปต์อื่นและคุณจะไม่ได้รับการแจ้งเตือน
Ivan Castellanos

0

ฉันคิดว่าหัวข้อนี้ต้องการโซลูชันที่ทันสมัยกว่า

ฉันแน่ใจว่าnsIWebProgressListenerประมาณหลังแล้วฉันประหลาดใจไม่มีใครพูดถึงมัน

จาก framescript (สำหรับความเข้ากันได้ของ 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 ทั้งหมด แต่มีคำเตือนความคิดเห็นว่า "ALSO ทริกเกอร์สำหรับ pushState" ดังนั้นเราจึงต้องทำการกรองเพิ่มเติมที่นี่เพื่อให้แน่ใจว่ามันเป็นเพียงสิ่งที่ผลักดัน

อ้างอิงจาก: https://github.com/jgraham/gecko/blob/55d8d9aa7311386ee2dabfccb481684c8920a527/toolkit/modules/addons/WebNavigation.jsm#L18

และ: resource: //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


ฉันเพิ่งแก้ไขคำตอบของฉันด้วยข้อมูลที่ร้องขอโปรดตรวจสอบ!
Victor Queiroz

@VictorQueiroz นั่นเป็นความคิดที่ดีและสะอาดเพื่อแค็ปซูลฟังก์ชั่น แต่ถ้าห้องสมุดส่วนที่สามเรียก pushState, HistoryApi ของคุณจะไม่ได้รับแจ้ง
Yairopro

0

ฉันไม่ควรเขียนทับวิธีประวัติศาสตร์ดั้งเดิมเพื่อให้การใช้งานอย่างง่ายนี้สร้างฟังก์ชั่นของฉันเองที่เรียกว่าสถานะ eventedPush ซึ่งเพิ่งยื้อเหตุการณ์และส่งกลับ 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ในกรณีที่คุณต้องการทำตามวิธีการเดียวกัน แต่ใช้ฟังก์ชั่นลูกศรติดตามตัวอย่างด้านล่างในตรรกะจาวาสคริปต์ของคุณ:

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 นั้นถูกกระตุ้นโดยการกระทำของเบราว์เซอร์เช่นการคลิกที่ปุ่มย้อนกลับ (หรือการเรียก history.back () ใน JavaScript)

เราจำเป็นต้องเรียกใช้ history.back () เพื่อเรียกใช้ WindowEventHandlers.onpopstate

ดังนั้นติดตั้งจาก:

history.pushState(...)

ทำ:

history.pushState(...)
history.pushState(...)
history.back()
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.