native.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. class ComprehensiveURLObserver {
  2. constructor(options = {}) {
  3. this.options = {
  4. trackHash: true,
  5. trackSearch: true,
  6. trackPathname: true,
  7. debounce: 50,
  8. ...options
  9. }
  10. this.observers = []
  11. this.lastUrl = this.getCurrentURLState()
  12. this.debounceTimer = null
  13. this.init()
  14. }
  15. init() {
  16. // 监听标准事件
  17. window.addEventListener('popstate', this.handleChange.bind(this))
  18. if (this.options.trackHash) {
  19. window.addEventListener('hashchange', this.handleChange.bind(this))
  20. }
  21. // 拦截 History API
  22. this.interceptHistoryMethods()
  23. // 监听点击事件(捕获链接点击)
  24. document.addEventListener('click', this.handleClick.bind(this), true)
  25. // 可选:轮询作为备用方案
  26. if (this.options.polling) {
  27. this.startPolling()
  28. }
  29. }
  30. getCurrentURLState() {
  31. return {
  32. href: window.location.href,
  33. pathname: window.location.pathname,
  34. search: window.location.search,
  35. hash: window.location.hash,
  36. origin: window.location.origin
  37. }
  38. }
  39. interceptHistoryMethods() {
  40. const methods = ['pushState', 'replaceState']
  41. methods.forEach(method => {
  42. const original = history[method]
  43. history[method] = (...args) => {
  44. const oldState = this.getCurrentURLState()
  45. const result = original.apply(history, args)
  46. this.handleChange('history', oldState)
  47. return result
  48. }
  49. })
  50. }
  51. handleClick(event) {
  52. const link = event.target.closest('a')
  53. if (link && link.href) {
  54. const targetUrl = new URL(link.href, window.location.origin)
  55. const currentUrl = new URL(window.location.href)
  56. // 如果是同源导航且不是当前页面
  57. if (targetUrl.origin === currentUrl.origin &&
  58. targetUrl.href !== currentUrl.href) {
  59. event.preventDefault()
  60. this.handleChange('click', this.getCurrentURLState())
  61. // 执行实际导航
  62. setTimeout(() => {
  63. window.location.href = link.href
  64. }, 0)
  65. }
  66. }
  67. }
  68. handleChange(type, oldState = this.lastUrl) {
  69. if (this.debounceTimer) {
  70. clearTimeout(this.debounceTimer)
  71. }
  72. this.debounceTimer = setTimeout(() => {
  73. const newState = this.getCurrentURLState()
  74. if (this.hasUrlChanged(oldState, newState)) {
  75. const changeInfo = {
  76. oldUrl: oldState.href,
  77. newUrl: newState.href,
  78. type: type,
  79. timestamp: Date.now(),
  80. details: {
  81. pathname: { old: oldState.pathname, new: newState.pathname },
  82. search: { old: oldState.search, new: newState.search },
  83. hash: { old: oldState.hash, new: newState.hash }
  84. }
  85. }
  86. this.notifyObservers(changeInfo)
  87. this.lastUrl = newState
  88. }
  89. }, this.options.debounce)
  90. }
  91. hasUrlChanged(oldState, newState) {
  92. if (oldState.href !== newState.href) return true
  93. if (this.options.trackPathname && oldState.pathname !== newState.pathname) return true
  94. if (this.options.trackSearch && oldState.search !== newState.search) return true
  95. if (this.options.trackHash && oldState.hash !== newState.hash) return true
  96. return false
  97. }
  98. startPolling() {
  99. setInterval(() => {
  100. const currentState = this.getCurrentURLState()
  101. if (this.hasUrlChanged(this.lastUrl, currentState)) {
  102. this.handleChange('polling', this.lastUrl)
  103. }
  104. }, this.options.pollingInterval || 1000)
  105. }
  106. subscribe(callback) {
  107. this.observers.push(callback)
  108. return () => this.unsubscribe(callback)
  109. }
  110. unsubscribe(callback) {
  111. this.observers = this.observers.filter(obs => obs !== callback)
  112. }
  113. notifyObservers(changeInfo) {
  114. this.observers.forEach(observer => {
  115. try {
  116. observer(changeInfo)
  117. } catch (error) {
  118. console.error('URL观察者错误:', error)
  119. }
  120. })
  121. }
  122. destroy() {
  123. window.removeEventListener('popstate', this.handleChange)
  124. window.removeEventListener('hashchange', this.handleChange)
  125. document.removeEventListener('click', this.handleClick)
  126. if (this.debounceTimer) {
  127. clearTimeout(this.debounceTimer)
  128. }
  129. }
  130. }
  131. // const unsubscribe = urlWatcher.subscribe((change) => {
  132. // console.group('URL变化详情');
  133. // console.log('类型:', change.type);
  134. // console.log('从:', change.oldUrl);
  135. // console.log('到:', change.newUrl);
  136. // console.log('路径变化:', change.details.pathname);
  137. // console.log('查询参数变化:', change.details.search);
  138. // console.log('哈希变化:', change.details.hash);
  139. // console.groupEnd();
  140. // });
  141. export default {
  142. urlWatcher: new ComprehensiveURLObserver({
  143. trackHash: true,
  144. trackSearch: true,
  145. trackPathname: true,
  146. debounce: 100
  147. })
  148. }