Làm cách nào để nhận thông báo về những thay đổi của lịch sử qua history.pushState?


153

Vì vậy, bây giờ khi HTML5 giới thiệu history.pushStateđể thay đổi lịch sử trình duyệt, các trang web bắt đầu sử dụng kết hợp với Ajax thay vì thay đổi định danh phân đoạn của URL.

Đáng buồn thay, điều đó có nghĩa là những cuộc gọi đó không thể được phát hiện nữa onhashchange.

Câu hỏi của tôi là: Có cách nào đáng tin cậy (hack ?;)) Để phát hiện khi một trang web sử dụng history.pushState? Thông số kỹ thuật không nêu bất cứ điều gì về các sự kiện được nêu ra (ít nhất là tôi không thể tìm thấy bất cứ điều gì).
Tôi đã cố gắng tạo mặt tiền và thay thế window.historybằng đối tượng JavaScript của riêng mình, nhưng nó không có tác dụng gì cả.

Giải thích thêm: Tôi đang phát triển một tiện ích bổ sung Firefox cần phát hiện những thay đổi này và hành động tương ứng.
Tôi biết có một câu hỏi tương tự vài ngày trước đã hỏi liệu việc nghe một số sự kiện DOM có hiệu quả không nhưng tôi không muốn dựa vào điều đó bởi vì những sự kiện này có thể được tạo ra vì nhiều lý do khác nhau.

Cập nhật:

Đây là một jsfiddle (sử dụng Firefox 4 hoặc Chrome 8) cho thấy onpopstatekhông được kích hoạt khi pushStateđược gọi (hoặc tôi đang làm gì đó sai? Hãy thoải mái cải thiện nó!).

Cập nhật 2:

Một vấn đề (phụ) khác window.locationlà không được cập nhật khi sử dụng pushState(nhưng tôi đã đọc về điều này đã có ở đây trên SO tôi nghĩ).

Câu trả lời:


194

5.5.9.1 Định nghĩa sự kiện

Sự kiện popstate được kích hoạt trong một số trường hợp nhất định khi điều hướng đến một mục lịch sử phiên.

Theo đó, không có lý do gì để popstate bị sa thải khi bạn sử dụng pushState. Nhưng một sự kiện như pushstatesẽ có ích. Vì historylà một đối tượng máy chủ, bạn nên cẩn thận với nó, nhưng Firefox có vẻ tốt trong trường hợp này. Mã này hoạt động tốt:

(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 của bạn trở thành :

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

Bạn có thể vá khỉ window.history.replaceStatetheo cách tương tự.

Lưu ý: tất nhiên bạn có thể thêm onpushstateđơn giản vào đối tượng toàn cầu và thậm chí bạn có thể khiến nó xử lý nhiều sự kiện hơn thông quaadd/removeListener


Bạn nói bạn đã thay thế toàn bộ historyđối tượng. Điều đó có thể là không cần thiết trong trường hợp này.
gblazex

@galambalazs: Có lẽ. Có thể (không biết) window.historylà chỉ đọc nhưng các thuộc tính của đối tượng lịch sử thì không ... Cảm ơn rất nhiều vì giải pháp này :)
Felix Kling

Theo thông số kỹ thuật có một sự kiện "pagetransition", có vẻ như nó chưa được triển khai.
Mohamed Mansour

2
@ user280109 - Tôi sẽ cho bạn biết nếu tôi biết. :) Tôi nghĩ không có cách nào để làm điều này trong Opera atm.
gblazex

1
@cprcrack Đó là để giữ cho phạm vi toàn cầu sạch sẽ. Bạn phải lưu pushStatephương thức riêng để sử dụng sau. Vì vậy, thay vì một biến toàn cục, tôi đã chọn đóng gói toàn bộ mã vào IIFE: en.wikipedia.org/wiki/Immediately-invoking_feft_expression
gblazex 16/11/13

4

Tôi đã từng sử dụng điều này:

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

Nó gần giống như galambalaz đã làm.

Nó thường là quá mức cần thiết mặc dù. Và nó có thể không hoạt động trong tất cả các trình duyệt. (Tôi chỉ quan tâm đến phiên bản trình duyệt của mình.)

(Và nó để lại một var _wr, vì vậy bạn có thể muốn bọc nó hoặc một cái gì đó. Tôi không quan tâm đến điều đó.)


4

Cuối cùng tìm thấy cách "chính xác" để làm điều này! Nó yêu cầu thêm một đặc quyền vào tiện ích mở rộng của bạn và sử dụng trang nền (không chỉ là tập lệnh nội dung) mà còn hoạt động.

Sự kiện bạn muốn là browser.webNavigation.onHistoryStateUpdated, sự kiện được kích hoạt khi một trang sử dụng historyAPI để thay đổi URL. Nó chỉ kích hoạt các trang web mà bạn có quyền truy cập và bạn cũng có thể sử dụng bộ lọc URL để tiếp tục cắt giảm thư rác nếu bạn cần. Nó đòi hỏiwebNavigation cho phép (và tất nhiên cho phép của máy chủ đối với (các) tên miền liên quan).

Cuộc gọi lại sự kiện nhận được ID tab, URL đang được "điều hướng" đến và các chi tiết khác như vậy. Nếu bạn cần thực hiện một hành động trong tập lệnh nội dung trên trang đó khi sự kiện bắt đầu, hãy nhập tập lệnh có liên quan trực tiếp từ trang nền hoặc để tập lệnh nội dung mở ra porttrang nền khi tải, hãy lưu trang nền cổng đó trong bộ sưu tập được lập chỉ mục bởi ID tab và gửi tin nhắn qua cổng có liên quan (từ tập lệnh nền đến tập lệnh nội dung) khi sự kiện bắt đầu.


2

Bạn có thể liên kết với window.onpopstatesự kiện này?

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

Từ các tài liệu:

Một trình xử lý sự kiện cho sự kiện popstate trên cửa sổ.

Một sự kiện popstate được gửi đến cửa sổ mỗi khi mục lịch sử hoạt động thay đổi. Nếu mục lịch sử đang được kích hoạt được tạo bởi một lệnh gọi history.pushState () hoặc bị ảnh hưởng bởi lệnh gọi history.replaceState (), thuộc tính trạng thái của sự kiện popstate chứa một bản sao của đối tượng trạng thái của mục lịch sử.


6
Tôi đã thử điều này rồi và sự kiện chỉ được kích hoạt khi người dùng quay lại lịch sử (hoặc bất kỳ chức năng nào) history.go, .backđược gọi. Nhưng không phải trên pushState. Đây là nỗ lực của tôi để kiểm tra nó, có lẽ tôi đã làm gì đó sai: jsfiddle.net/fkling/vV9vd Dường như chỉ liên quan theo cách mà nếu lịch sử bị thay đổi pushState, đối tượng trạng thái tương ứng được chuyển đến xử lý sự kiện khi các phương thức khác được gọi là.
Felix Kling

1
Ah. Trong trường hợp đó, điều duy nhất tôi có thể nghĩ đến là đăng ký thời gian chờ để xem xét độ dài của ngăn xếp lịch sử và kích hoạt một sự kiện nếu kích thước ngăn xếp đã thay đổi.
stef

Ok đây là một ý tưởng thú vị. Điều duy nhất là thời gian chờ sẽ phải kích hoạt đủ thường xuyên để người dùng không nhận thấy bất kỳ độ trễ (dài) nào (tôi phải tải và hiển thị dữ liệu cho URL mới). Tôi luôn cố gắng tránh thời gian chờ và bỏ phiếu khi có thể, nhưng cho đến bây giờ đây dường như là giải pháp duy nhất. Tôi vẫn sẽ chờ đợi các đề xuất khác. Nhưng cảm ơn bạn rất nhiều vì bây giờ!
Felix Kling

Nó có thể thậm chí đủ để kiểm tra độ dài của thanh lịch sử trên mỗi lần nhấp.
Felix Kling

1
Vâng - điều đó sẽ làm việc. Tôi đã có một cuộc chơi với điều này - một vấn đề là cuộc gọi thay thế sẽ không tổ chức sự kiện vì kích thước của ngăn xếp sẽ không thay đổi.
stef

1

Vì bạn đang hỏi về một addon Firefox, đây là mã mà tôi phải làm việc. Sử dụng unsafeWindowkhông còn khuyến cáo , và các lỗi ra khi pushState được gọi từ một kịch bản khách hàng sau khi được sửa đổi:

Quyền bị từ chối truy cập thuộc tính history.pushState

Thay vào đó, có một API gọi là exportFunction cho phép chức năng được đưa vàowindow.history như thế này:

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

Trên dòng cuối cùng, bạn nên thay đổi unsafeWindow.history,thành window.history. Nó không hoạt động với tôi với không an toànWindow.
Lindsay-Nhu cầu-Ngủ

0

câu trả lời khỉ của galambalazswindow.history.pushStatewindow.history.replaceState, nhưng vì một số lý do, nó đã ngừng hoạt động đối với tôi. Đây là một giải pháp thay thế không hay vì nó sử dụng bỏ phiếu:

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

Có một sự thay thế cho bỏ phiếu?
SuperUberDuper

@SuperUberDuper: xem các câu trả lời khác.
Flimm

@Flimm Bỏ phiếu không đẹp, nhưng xấu. Nhưng tốt, bạn đã không có sự lựa chọn bạn nói.
Yairopro

Điều này tốt hơn nhiều so với việc vá khỉ, việc vá khỉ có thể được hoàn tác bởi các tập lệnh khác và bạn không nhận được thông báo nào về nó.
Ivan Castellanos

0

Tôi nghĩ chủ đề này cần một giải pháp hiện đại hơn.

tôi chắc chắn nsIWebProgressListener đã quay trở lại sau đó tôi ngạc nhiên không ai đề cập đến nó.

Từ một khung hình (cho khả năng tương thích của e10s):

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

Sau đó lắng nghe onLoacationChange

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

Điều đó rõ ràng sẽ bắt tất cả các PushState. Nhưng có một cảnh báo bình luận rằng nó "CSONG kích hoạt cho PushState". Vì vậy, chúng ta cần thực hiện thêm một số bộ lọc ở đây để đảm bảo nó chỉ là công cụ đẩy.

Dựa trên: https://github.com/jgraham/gecko/blob/55d8d9aa7311386ee2dabfccb481684c8920a527/toolkit/modules/addons/WebNavlation.jsm#L18

Và: resource: //gre/modules/WebNavlationContent.js


Câu trả lời này không liên quan gì đến bình luận, trừ khi tôi nhầm. WebProTHERListener dành cho tiện ích mở rộng FF? Câu hỏi này là về lịch sử HTML5
Kloar

@Kloar cho phép xóa những bình luận này vui lòng
Noitidart

0

Chà, tôi thấy nhiều ví dụ về việc thay thế pushStatetài sản của historynhưng tôi không chắc đó là ý tưởng hay, tôi muốn tạo một sự kiện dịch vụ dựa trên API tương tự với lịch sử theo cách bạn có thể kiểm soát không chỉ trạng thái đẩy mà còn thay thế trạng thái cũng như và nó mở ra cánh cửa cho nhiều triển khai khác không dựa vào API lịch sử toàn cầu. Vui lòng kiểm tra ví dụ sau:

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

Nếu bạn cần EventEmitterđịnh nghĩa, mã ở trên dựa trên trình phát sự kiện NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inheritstriển khai có thể được tìm thấy ở đây: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970


Tôi chỉ chỉnh sửa câu trả lời của tôi với thông tin được yêu cầu, xin vui lòng kiểm tra!
Victor Queiroz

@VictorQueiroz Đó là một ý tưởng hay và gọn gàng để gói gọn các chức năng. Nhưng nếu một số thư viện phần thứ ba gọi cho PushState, HistoryApi của bạn sẽ không được thông báo.
Yairopro

0

Tôi không muốn ghi đè lên phương thức lịch sử gốc để việc triển khai đơn giản này tạo ra hàm riêng của tôi được gọi là trạng thái eventedPush, nó chỉ gửi một sự kiện và trả về history.pushState (). Dù cách nào cũng hoạt động tốt nhưng tôi thấy việc triển khai này sạch hơn một chút vì các phương thức riêng sẽ tiếp tục thực hiện như các nhà phát triển trong tương lai mong đợi.

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

Dựa trên giải pháp được cung cấp bởi @gblazex , trong trường hợp bạn muốn theo cùng một cách tiếp cận, nhưng sử dụng các hàm mũi tên, hãy theo dõi ví dụ dưới đây trong logic javascript của bạn:

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

Sau đó, triển khai một chức năng khác _notifyUrl(url)kích hoạt mọi hành động cần thiết mà bạn có thể cần khi url trang hiện tại được cập nhật (ngay cả khi trang chưa được tải)

  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

Vì tôi chỉ muốn URL mới, tôi đã điều chỉnh mã của @gblazex và @Alberto S. để có được điều này:

(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

Tôi không nghĩ rằng đó là một ý tưởng tốt để sửa đổi các chức năng gốc ngay cả khi bạn có thể và bạn nên luôn giữ phạm vi ứng dụng của mình, vì vậy, một cách tiếp cận tốt là không sử dụng chức năng PushState toàn cầu, thay vào đó, hãy sử dụng một trong các chức năng của riêng bạn:

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>

Lưu ý rằng nếu bất kỳ mã nào khác sử dụng hàm PushState riêng, bạn sẽ không nhận được trình kích hoạt sự kiện (nhưng nếu điều này xảy ra, bạn nên kiểm tra mã của mình)


-5

Theo tiêu chuẩn quốc gia:

Lưu ý rằng chỉ cần gọi history.pushState () hoặc history.replaceState () sẽ không kích hoạt sự kiện popstate. Sự kiện popstate chỉ được kích hoạt bằng cách thực hiện một hành động của trình duyệt, chẳng hạn như nhấp vào nút quay lại (hoặc gọi history.back () trong JavaScript)

chúng ta cần gọi history.back () để trigeer WindowEventHandlers.onpopstate

Vì vậy, bắt nguồn từ:

history.pushState(...)

làm:

history.pushState(...)
history.pushState(...)
history.back()
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.