|
@@ -0,0 +1,175 @@
|
|
|
|
|
+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
|
|
|
|
|
+ })
|
|
|
|
|
+}
|