如何通过history. pushstate获得历史变化的通知?

所以现在HTML5引入history.pushState来改变浏览器的历史记录,网站开始将其与Ajax结合使用,而不是改变URL的片段标识符。

遗憾的是,这意味着onhashchange不能再检测到这些调用。

我的问题是:是否有可靠的方法(hack?);))来检测网站何时使用history.pushState?规范没有说明引发的任何事件(至少我没有找到任何事件)。< br / > 我试图创建一个门面,并用我自己的JavaScript对象替换window.history,但它根本没有任何效果 我正在开发一个Firefox插件,需要检测这些变化并相应地采取行动。< br / > 我知道几天前有一个类似的问题,问监听一些DOM事件是否有效,但我宁愿不依赖于它,因为这些事件可以出于许多不同的原因而生成

更新:

这是一个jsfiddle(使用Firefox 4或Chrome 8),显示在调用pushStateonpopstate没有被触发(或者我做错了什么?请随意改进!)。

更新2:

另一个(侧面)问题是,在使用pushState时,window.location不会更新(但我已经在这里读到过这个,所以我认为)。

129032 次浏览

你可以绑定到window.onpopstate事件?

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

从文档中可以看出:

popstate事件处理程序

一个popstate事件被分派到 窗口每次激活历史记录 输入的变化。如果历史记录条目 被激活是由呼叫创建的 到history.pushState()或被影响 通过调用history.replaceState(), popstate事件的状态属性 包含历史记录项的副本 状态对象。< / p >

5.5.9.1事件定义

popstate事件在导航到会话历史条目时的某些情况下被触发。

根据这一点,当你使用pushState时,没有理由触发popstate。但是像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使它处理更多的事件

我曾经用过这个:

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

它几乎和galambalazs一样。

但这通常是过度的。而且它可能并不适用于所有浏览器。(我只关心我的浏览器版本。)

(它留下了一个变量_wr,所以你可能想要将它包装或其他东西。我不关心这个。)

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

既然你问的是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});

我认为这个话题需要一个更现代的解决方案。

我确信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 /模块/ WebNavigationContent.js

标准状态如下:

注意,仅仅调用history.pushState()或history.replaceState()不会触发popstate事件。popstate事件仅由浏览器操作触发,例如单击后退按钮(或在JavaScript中调用history.back())

我们需要调用history.back()来触发WindowEventHandlers.onpopstate

所以不是:

history.pushState(...)

做的事:

history.pushState(...)
history.pushState(...)
history.back()

好吧,我看到了很多替换historypushState属性的例子,但我不确定这是否是一个好主意,我更喜欢创建一个基于与历史类似的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.jsutils.inherits的实现可以在这里找到:https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

终于找到了“正确的”;(没有monkeypatching,没有破坏其他代码的风险)这样做的方法!它需要为你的扩展添加一个特权(是的,有人在评论中指出了这一点,它是为扩展API 这是我们要求的吗)和使用后台页面(不仅仅是一个内容脚本),但它确实工作。

您想要的事件是browser.webNavigation.onHistoryStateUpdated,当页面使用history API更改URL时触发该事件。它只会触发你有权限访问的网站,如果需要的话,你还可以使用URL过滤器来进一步减少垃圾邮件。它需要webNavigation权限(当然还有相关域的主机权限)。

事件回调获取标签ID,被“导航”的URL;To,以及其他诸如此类的细节。如果你需要在事件触发时在该页的内容脚本中采取操作,要么直接从后台页面注入相关脚本,要么让内容脚本在加载时打开port到后台页面,让后台页面将该端口保存在一个按选项卡ID索引的集合中,并在事件触发时通过相关端口(从后台脚本到内容脚本)发送消息。

我宁愿不覆盖本机历史方法,所以这个简单的实现创建了我自己的函数,称为eventtedpush 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");

基于@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}`);
}

因为我只是想要新的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);

我不认为这是一个好主意,即使你可以修改本机函数,你应该始终保持你的应用程序范围,所以一个好的方法是不使用全局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函数,则不会获得事件触发器(但如果发生这种情况,则应该检查代码)

除了其他答案。我们可以使用History接口,而不是存储原始函数。

history.pushState = function()
{
// ...


History.prototype.pushState.apply(history, arguments);
}

我用简单的代理来做到这一点。这是原型的替代方案

window.history.pushState = new Proxy(window.history.pushState, {
apply: (target, thisArg, argArray) => {
// trigger here what you need
return target.apply(thisArg, argArray);
},
});

感谢@KalanjDjordjeDjordje他的回答。我试图让他的想法成为一个完整的解决方案:

const onChangeState = (state, title, url, isReplace) => {
// define your listener here ...
}


// set onChangeState() listener:
['pushState', 'replaceState'].forEach((changeState) => {
// store original values under underscored keys (`window.history._pushState()` and `window.history._replaceState()`):
window.history['_' + changeState] = window.history[changeState]
    

window.history[changeState] = new Proxy(window.history[changeState], {
apply (target, thisArg, argList) {
const [state, title, url] = argList
onChangeState(state, title, url, changeState === 'replaceState')
            

return target.apply(thisArg, argList)
},
})
})