The business scenario

Recently received a optimization requirements, a page is polling request two interface to get the number of unread messages, but when the user at the same time in more than one TAB to open the page, the page requests for polling will have the user to open the TAB page is too much, 1 minute after thousands of requests, triggered the risk control, the need to optimize, more TAB to keep only one page will ask interface, Other pages share data from this page.

Cross-page communication

There are several ways of same-origin cross-page communication, such as BroadCast Channel and Service Worker. This mode is more suitable for the communication that the page actively triggers through fixed events (such as user clicks). If the communication is not actively triggered, the page will not respond. However, the current service needs to ensure that one page is requesting data, so the requested data needs to be saved with a time information so that other pages can verify whether the data has expired. Therefore, LocalStorage is more suitable for communication. You can read this article about the various communication methods.

LocalStorage implements communication

When localStorage is updated, storage events will be triggered on other same-origin pages. Therefore, write localStorage on page A, listen to storage events on page B, and read the changed value from localStorage to achieve communication. Open two tabs, click on one TAB, and see print on the other TAB

window.addEventListener('click', () => {
  localStorage.setItem('number'.Math.random())  // Click the screen to change storage
})
    
window.addEventListener('storage', ({ key, newValue }) => {
  console.log('Listen for localStorage changes', key, newValue)  // Listen for localStorage changes
})
Copy the code

Note that the value of localStorage must actually change to trigger the storage event. The second setItem in the following code does not trigger the storage event.

localStorage.setItem('number'.'123')
localStorage.setItem('number'.'123')
Copy the code

Business implementation process

First of all, when the page is opened, start to request data, perform polling, and start to listen to the change of localStorage, when localStorage changes that other pages are requesting data, save a state otherTabIsGettingData=true. After the request to the data, the obtained data will be stored in the localStorage with the timestamp (this step will make other pages stop network requests). When polling the request again, first determine whether other pages are requesting data (otherTabIsGettingData), if so, If yes, the previous TAB page may be closed. Then set the current otherTabIsGettingData to false and make a network request again.

Code implementation

/** * polling events keep multiple TAB pages with only one execution */
export class SingleLoop {
  /** * @param {string} storageKey - The key used to store data to localStorage * @param {function} loopFn - Async function that gets data returns promise * @param {number} ms - callback */ @param {function} callback
  constructor(storageKey, loopFn, ms, callback) {
    this.key = storageKey // The key used to store data in localStorage
    this.loopFn = loopFn / / event
    this.ms = ms // Time interval ms
    this.callback = callback
    this.otherTabIsGettingData = false // Other tabs are requesting data
    this.timer = null
  }

  /** Start polling */
  start() {
    window.addEventListener('storage'.this.onStorageChange)
    this.loop()
  }

  stop() {
    window.removeEventListener('storage'.this.onStorageChange)
    clearTimeout(this.timer)
  }

  onStorageChange = ({ key }) = > {
    if (key === this.key) {
      const storage = this.getDataFromStorage()
      this.otherTabIsGettingData = true
      console.log('Listen for data changes:', key, data, 'Stop data acquisition for the current page')
      this.callback(storage.data)
    }
  }

  loop() {
    this.getData()
    this.timer = setTimeout((a)= > {
      this.loop()
    }, this.ms)
  }

  /** Fetch data from the interface or localStorage */
  async getData() {
    let data
    if (this.otherTabIsGettingData) {
      // There are already tabs requesting data
      const storageData = this.getDataFromStorage()
      const now = Date.now()
      const storageDataTime = storageData.time || 0
      if (now - storageDataTime > this.ms + 1000) {  // Give 1s buffer time due to delays in data requests and code execution
        // The old data has expired, the TAB that may request data has been closed, and the current page starts requesting data
        console.log('Data out of date'.this.key, now - storageData.time, 'Reboot')
        this.otherTabIsGettingData = false
        data = await this.setDataToStorage()
      } else {
        data = storageData.data
      }
    } else {
      data = await this.setDataToStorage()
      console.log(this.key, 'No other page request data, perform HTTP request')}this.callback(data)
  }

  getDataFromStorage() {
    return JSON.parse(localStorage.getItem(this.key) || '{}')}/** Get data from the interface and store it to localStorage */
  async setDataToStorage() {
    const data = await this.loopFn()
    const value = JSON.stringify({
      data,
      time: Date.now()
    })
    localStorage.setItem(this.key, value)
    return data
  }
}

Copy the code

call

function fetchData() {
  // Simulate an HTTP request
  return new Promise((resolve) = > {
    setTimeout((a)= > {
      resolve({ num: Math.random() })
    }, 300)})}function callback(data) {
  console.log('to get the data, data)
}

const loop = new SingleLoop('count', fetchData, 4 * 1000, callback)
loop.start()

// For react and vue applications, you can use loop.stop() to disable listening on timers and storage events when uninstalling components
Copy the code

Additional notes on timers

In chrome browser timer against doing A performance optimization, when walking TAB page is cut, the timer will be deferred execution (variable delay time), so after executing the code above, open A, B, C, D four pages, assumption is A HTTP request data in the execution, according to the normal logic is as long as A don’t shut down, only to switch between the four pages, A should always request data, but in fact, when the switch to PAGE B stays, the timer task of A is executed after A period of time because of the delayed execution of the timer, and the data obtained from localStorage of page B is detected to be expired, then page B will start requesting data. A page no longer requests data from HTTP (and still maintains A page request as A result). However, since the above buffer was made for 1s before the verification expiration, the effect may not be obvious. Change the buffer time to about 500ms and then switch TAB to see the effect obviously. If you want to solve this problem, you can use the Web Worker to solve it, but I won’t go into details here.