history.pushStateを介して履歴の変更について通知を受ける方法は?


154

したがって、HTML5 history.pushStateがブラウザーの履歴を変更することを導入したので、Webサイトは、URLのフラグメントIDを変更する代わりに、これをAjaxと組み合わせて使用​​し始めます。

悲しいことに、これらの呼び出しはによって検出できなくなっていますonhashchange

私の質問は、ウェブサイトがhistory.pushStateいつ使用するかを検出する信頼できる方法(ハック?;))はありますか?仕様では、発生するイベントについては何も述べられていません(少なくとも私は何も見つけることができませんでした)。
ファサードを作成window.historyして自分のJavaScriptオブジェクトに置き換えようとしましたが、まったく効果がありませんでした。

詳細説明:これらの変更を検出して適切に動作する必要があるFirefoxアドオンを開発しています。
数日前に同様の質問があり、いくつかのDOMイベントを聞くことが効率的であるかどうかを尋ねましたが、これらのイベントはさまざまな理由で生成される可能性があるため、それに頼るのは避けたいと思います。

更新:

これは、呼び出されたonpopstateときにトリガーされないことを示すjsfiddle(Firefox 4またはChrome 8を使用)ですpushState(または何か問題がありますか?遠慮なく改善してください!)。

アップデート2:

もう1つの(側面の)問題は、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 2011年

@galambalazs:はい。多分(わからない)window.historyは読み取り専用ですが、履歴オブジェクトのプロパティはそうではありません...この解決策をたくさんありがとう:)
Felix Kling

仕様によると、「ページ遷移」イベントがありますが、まだ実装されていないようです。
Mohamed Mansour 2011年

2
@ user280109-知っていれば教えてくれます。:) Opera atmではこれを行う方法はないと思います。
gblazex 2011年

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

それはgalambalazがしたのとほとんど同じです。

しかし、それは通常はやり過ぎです。また、すべてのブラウザで機能するとは限りません。(私は私のブラウザのバージョンのみを気にします。)

(そして、それは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対応する状態オブジェクトがイベントハンドラに渡され、時に他の方法呼ばれる。
Felix Kling、2011年

1
ああ。その場合、私が考えることができる唯一のことは、履歴スタックの長さを調べるタイムアウトを登録し、スタックサイズが変更された場合にイベントを発生させることです。
stef

これは面白いアイデアです。唯一のことは、ユーザーが(長い)遅延に気付かないように、タイムアウトが頻繁に発生する必要があることです(新しいURLのデータを読み込んで表示する必要があります)。私は常にタイムアウトとポーリングをできるだけ避けようとしていますが、これまではこれが唯一の解決策のようです。私はまだ他の提案を待っています。でも今はどうもありがとう!
Felix Kling、2011年

クリックするたびに履歴スティックの長さを確認するだけで十分かもしれません。
Felix Kling、2011年

1
はい-それはうまくいくでしょう。私はこれで遊んでいます-1つの問題は、スタックのサイズが変更されていなかったためにイベントをスローしないreplaceState呼び出しです。
stef

1

Firefoxのアドオンについて質問しているので、ここで私が機能させるコードを示します。の使用unsafeWindow推奨されなくなりました。変更後にクライアントスクリプトからpushStateを呼び出すとエラーになります。

プロパティhistory.pushStateへのアクセスが拒否されました

代わりに、exportFunctionと呼ばれるAPI があり、関数を次のように注入できます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.pushStatewindow.history.replaceStateが、何らかの理由で動作しなくなりました。ポーリングを使用するため、それほど良くない代替案を次に示します。

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

ポーリングの代替手段はありますか?
SuperUberDuper 2015

@SuperUberDuper:他の回答を参照してください。
Flimm

@Flimmポーリングは良くありませんが、醜いです。しかし、まあ、あなたが言った選択肢はありませんでした。
Yairopro

これはサルのパッチよりもはるかに優れており、サルのパッチは他のスクリプトで元に戻すことができ、通知はありません。
Ivan Castellanos

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をキャッチします。しかし、「ALSOはpushStateをトリガーする」というコメント警告があります。したがって、ここでさらにフィルタリングを実行して、それが単に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 2016年

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 2017年

@VictorQueiroz関数をカプセル化するというすてきでクリーンなアイデア。ただし、一部のライブラリでpushStateが必要な場合、HistoryApiには通知されません。
Yairopro

0

ネイティブの履歴メソッドを上書きしたくないので、この単純な実装は、イベントをディスパッチしてhistory.pushState()を返すだけのeventedPush状態と呼ばれる独自の関数を作成します。どちらの方法でも問題なく動作しますが、ネイティブメソッドは今後の開発者が期待するとおりに機能し続けるため、この実装は少しすっきりしています。

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イベントはトリガーされないことに注意してください。popstateイベントは、戻るボタンをクリックする(またはJavaScriptでhistory.back()を呼び出す)などのブラウザーアクションを実行することによってのみトリガーされます

WindowEventHandlers.onpopstateをトリガーするために、history.back()を呼び出す必要があります。

ので、本能的:

history.pushState(...)

行う:

history.pushState(...)
history.pushState(...)
history.back()
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.