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.