class ComprehensiveURLObserver { constructor(options = {}) { this.options = { trackHash: true, trackSearch: true, trackPathname: true, debounce: 50, ...options } this.observers = [] this.lastUrl = this.getCurrentURLState() this.debounceTimer = null this.init() } init() { // 监听标准事件 window.addEventListener('popstate', this.handleChange.bind(this)) if (this.options.trackHash) { window.addEventListener('hashchange', this.handleChange.bind(this)) } // 拦截 History API this.interceptHistoryMethods() // 监听点击事件(捕获链接点击) document.addEventListener('click', this.handleClick.bind(this), true) // 可选:轮询作为备用方案 if (this.options.polling) { this.startPolling() } } getCurrentURLState() { return { href: window.location.href, pathname: window.location.pathname, search: window.location.search, hash: window.location.hash, origin: window.location.origin } } interceptHistoryMethods() { const methods = ['pushState', 'replaceState'] methods.forEach(method => { const original = history[method] history[method] = (...args) => { const oldState = this.getCurrentURLState() const result = original.apply(history, args) this.handleChange('history', oldState) return result } }) } handleClick(event) { const link = event.target.closest('a') if (link && link.href) { const targetUrl = new URL(link.href, window.location.origin) const currentUrl = new URL(window.location.href) // 如果是同源导航且不是当前页面 if (targetUrl.origin === currentUrl.origin && targetUrl.href !== currentUrl.href) { event.preventDefault() this.handleChange('click', this.getCurrentURLState()) // 执行实际导航 setTimeout(() => { window.location.href = link.href }, 0) } } } handleChange(type, oldState = this.lastUrl) { if (this.debounceTimer) { clearTimeout(this.debounceTimer) } this.debounceTimer = setTimeout(() => { const newState = this.getCurrentURLState() if (this.hasUrlChanged(oldState, newState)) { const changeInfo = { oldUrl: oldState.href, newUrl: newState.href, type: type, timestamp: Date.now(), details: { pathname: { old: oldState.pathname, new: newState.pathname }, search: { old: oldState.search, new: newState.search }, hash: { old: oldState.hash, new: newState.hash } } } this.notifyObservers(changeInfo) this.lastUrl = newState } }, this.options.debounce) } hasUrlChanged(oldState, newState) { if (oldState.href !== newState.href) return true if (this.options.trackPathname && oldState.pathname !== newState.pathname) return true if (this.options.trackSearch && oldState.search !== newState.search) return true if (this.options.trackHash && oldState.hash !== newState.hash) return true return false } startPolling() { setInterval(() => { const currentState = this.getCurrentURLState() if (this.hasUrlChanged(this.lastUrl, currentState)) { this.handleChange('polling', this.lastUrl) } }, this.options.pollingInterval || 1000) } subscribe(callback) { this.observers.push(callback) return () => this.unsubscribe(callback) } unsubscribe(callback) { this.observers = this.observers.filter(obs => obs !== callback) } notifyObservers(changeInfo) { this.observers.forEach(observer => { try { observer(changeInfo) } catch (error) { console.error('URL观察者错误:', error) } }) } destroy() { window.removeEventListener('popstate', this.handleChange) window.removeEventListener('hashchange', this.handleChange) document.removeEventListener('click', this.handleClick) if (this.debounceTimer) { clearTimeout(this.debounceTimer) } } } // const unsubscribe = urlWatcher.subscribe((change) => { // console.group('URL变化详情'); // console.log('类型:', change.type); // console.log('从:', change.oldUrl); // console.log('到:', change.newUrl); // console.log('路径变化:', change.details.pathname); // console.log('查询参数变化:', change.details.search); // console.log('哈希变化:', change.details.hash); // console.groupEnd(); // }); export default { urlWatcher: new ComprehensiveURLObserver({ trackHash: true, trackSearch: true, trackPathname: true, debounce: 100 }) }