Browsers can open multiple tabs and sometimes need to share the same data in these tabs. How to communicate data in multiple tabs? Let’s look at four ways to communicate in tabs.

When we listen to songs on NetEase Cloud, we can open multiple tabs for playback. But we found that while one TAB was playing, the other tabs, if they were playing, stopped automatically.

It makes sense to think about this, because after all, if multiple tabs are playing at the same time, the sound will interfere, and only one music can be played at a time; So we also tried to implement a requirement for data communication between different browsers:

Let’s first prepare some data, just like NetEase Cloud, prepare an album list, each album list has different songs, you can pass THE URL parameter id to get different album pages:

<div id="app">
  <div class="box">
    <div class="item header">
      <div class="index"></div>
      <div class="name">The song title</div>
      <div class="time">The length</div>
      <div class="singer">singer</div>
      <div class="album">The album</div>
    </div>
    <template v-for="(item, index) in list">
      <div :class="['item','music',activeIndex == index ? 'active':'']" 
      @click="clickMusic(item, index)"
      :key="index">
        <div class="index">{{index+1}}</div>
        <div class="name">{{item.name}}</div>
        <div class="time">{{item.time}}</div>
        <div class="singer">{{item.singer}}</div>
        <div class="album">{{item.album}}</div>
      </div>
    </template>
  </div>
</div>
Copy the code
new Vue({
    el: '#app'.data() {
        return {
          list: [].activeIndex: -1,}},mounted() {
        const {
            id = '1'
        } = Qs.parse(window.location.search, { ignoreQueryPrefix: true })
        axios({
            url: '/api/list'.params: {
                id,
            }
        }).then((res) = > {
            return res.data
        }).then((res) = > {
            const {
                list
            } = res
            this.list = list
        })
    },
})
Copy the code

cookie

To communicate across all tabs, we must store data in a common storage space that all tabs can access and modify; We know that cookies are shared among all browser tabs of the user, so we can try to store selected data in cookies:

new Vue({
    mounted() {
        setInterval(() = > {
            let newValue = Cookies.get('music')
            if (newValue) {
                let parse = {}
                try {
                    parse = JSON.parse(newValue)
                } catch (error) {}
                let {
                    list,
                } = this
                let activeIndex = -1
                list.map((item, index) = > {
                    if (item.name == parse.name) {
                        activeIndex = index
                    }
                })
                this.activeIndex = activeIndex
            }
        }, 1000)},methods: {
        clickMusic(item, index) {
            this.activeIndex = index
            Cookies.set('music'.JSON.stringify(item))
        }
    }
})
Copy the code

Since updating cookies does not trigger any events, we need to use the timer setInterval to actively monitor whether the values in cookies change. The code looks fine, so let’s see how it works:

There are two problems:

  1. Timer time difference, click after a certain delay, the disadvantages of cookie itself
  2. Under the same album ID page, the same music item will also be selected because the selected data does not distinguish between pages

So we need to give each page a unique page ID; The page ID can be retrieved from the background interface. For simplicity, we use the timestamp as the page ID:

// omit other code
new Vue({
    data() {
        return {
            page_id: '0',}},mounted() {
        let timestamp = new Date().getTime()
        this.page_id = timestamp + ' '
        setInterval(() = > {
          let newValue = Cookies.get('music')
          if (newValue) {
            let parse = {}
            try {
                parse = JSON.parse(newValue)
            } catch (error) {}
            let {
                list,
            } = this
            let activeIndex = -1
            list.map((item, index) = > {
                // Yes The current page ID is only selected data
                if (item.name == parse.name 
                    && parse.page_id == page_id) {
                        activeIndex = index
                }
            })
            this.activeIndex = activeIndex
          }
        }, 1000)},methods: {
        clickMusic(item, index) {
          this.activeIndex = index
          let {
              page_id
          } = this
          // Put the page ID into the cookie
          item = Object.assign({
              page_id,
          }, item)
          Cookies.set('music'.JSON.stringify(item))
        }
    }
})
Copy the code

We can solve problem 2 by assigning ids to each page, but the cookie+setInterval scheme has a delay due to the disadvantages of timers.

localStorage

LocalStorage is also the storage space shared by multiple browser pages. And when localStorage is added, modified or deleted in a page, it will passively trigger a storage event in the current page, we can get the value of storage before and after the update by listening to the storage event in other pages:

// omit other code
new Vue({
    mounted() {
        let timestamp = new Date().getTime()
        this.page_id = timestamp + ' '
        window.addEventListener('storage'.(ev) = > {
            const {
                key,
                newValue,
            } = ev
            if (key === 'music' && newValue) {
                let parse = {}
                try {
                    parse = JSON.parse(newValue)
                } catch (error) {}
                let {
                    list,
                    page_id
                } = this
                let activeIndex = -1
                list.map((item, index) = > {
                    if (item.name == parse.name && parse.page_id == page_id) {
                        activeIndex = index
                    }
                })
                this.activeIndex = activeIndex
            }
        })
    },
})
Copy the code

Compared with cookie active listening, the passive trigger of localStorage not only appears more friendly in the code, but also greatly avoids the performance loss caused by timer.

webworker

As we mentioned in understanding JS event loops from an interview Question, Webworkers can only be used to do logical operations that consume CPU, etc. Webworkers are also divided into Worker and SharedWorker. Ordinary workers can be directly created using new Worker() and only used in the current page. As can be seen from the name of SharedWorker, data can be shared in multiple tag pages.

SharedWorker differs from Worker in that its second argument can be directly specified as name, or an object argument, so the following three constructs are the same:

new SharedWorker('/public/shared.js'.'musicWorker');
new SharedWorker('/public/shared.js', { name: 'musicWorker' });
new SharedWorker('/public/shared.js'.'musicWorker', { type: 'classic' });
Copy the code

After constructing the SharedWorker instance object, we need to communicate through its port attribute. The main API is as follows:

const sw = new SharedWorker('/public/shared.js');
// Send data
sw.port.postMessage('... ')
// Listen for data
sw.port.onmessage = function (event) {  / /... }
Copy the code

Since the multiple SharedWorker instances constructed form a shared connection, we assign each instance a unique ID upon successful connection:

//main.js
new Vue({
  data() {
      return {
          workder_id: 0.sw:}}, {},mounted() {
    this.sw = new SharedWorker('/public/shared.js');
    this.sw.port.addEventListener('message'.(ev) = > {
      let {
          type,
          data
      } = ev.data
      // Return an ID when initializing the connection
      if (type == 'id') {
          this.workder_id = data
      }
    })
    this.sw.port.start()
  },
  methods: {
    clickMusic(item, index) {
      // Omit some code
      // Take the id with you every time you communicate
      this.sw.port.postMessage({
          type: 'set'.id: this.workder_id,
          data: item
      })
    }
  }
})
Copy the code

We listen for connect events inside the ShareWorker and handle port events inside:

//shared.js
const connectedClients = new Set(a)let id = 1
// Send messages to other connections
function sendMessageToClients(payload, currentClientId = null) {
  connectedClients.forEach(({ id, client }) = > {
    if (currentClientId && currentClientId == id) return;
    client.postMessage(payload);
  });
}
// The current connection is bound to message listening
function setupClient(clientPort) {
  clientPort.onmessage = (event) = > {
    const { type, data, id } = event.data;
    if(type=='set'){
      sendMessageToClients({
        type: 'get'.data: data,
      }, id)
    }
  };
}
self.addEventListener("connect".(event) = > {
  const newClient = event.source;
  // Give the client a unique ID after each connection
  // Store each connection in an array
  connectedClients.add({
    client: newClient,
    id: id,
  });
  setupClient(newClient);
  newClient.postMessage({
    type: 'id'.data: id
  })
  id++
});
Copy the code

How do you debug sharedworker when writing shared.js? Direct console.log does not have output in the TAB page; Open the new TAB chrome://inspect, select Shared Workers and then select the corresponding script to debug happily.

websocket

Websocket as full duplex communication, naturally can realize the communication between multiple tabs; WebSocket is a new HTML5 protocol that aims to establish an unrestricted two-way communication channel between the browser and the server.

Here we use express-WS, a framework for Express, to simulate a Websocket server; Because the server will store many TAB page connection object information, we need to give each user a unique identity to distinguish; We get the user_id from the server and save it.

// omit other code
new Vue({
    el: '#app'.data() {
        return {
            list: [].activeIndex: -1.page_id: '0'.ws: null}},mounted() {
        let timestamp = new Date().getTime()
        this.page_id = timestamp + ' '
        let store_user_id = Cookies.get('user_id')
        if(!!!!! store_user_id) {this.connectWs(store_user_id)
        } else {
            axios({
                url: '/api/get_user_id'
            })
            .then((res) = > {
                return res.data
            })
            .then((res) = > {
                let {
                    user_id
                } = res
                Cookies.set('user_id', user_id)
                this.connectWs(user_id)
            })
        }
    },
})
Copy the code

With user_id we can connect to the WebSocket server and initiate a request.

// omit other code
new Vue({
  methods: {
    clickMusic(item, index) {
      this.activeIndex = index
      let {
        page_id
      } = this
      item = Object.assign({
        page_id,
      }, item)
      this.ws.send(JSON.stringify(item))
    },
    connectWs(user_id) {
      var ws = new WebSocket(`ws://localhost:9010/ws/${user_id}`)
      ws.onmessage = (e) = > {
        let parse = {}
        try {
          parse = JSON.parse(e.data)
        } catch (error) {}
        let {
          list,
          page_id
        } = this
        let activeIndex = -1
        list.map((item, index) = > {
          if (item.name == parse.name && parse.page_id == page_id) {
            activeIndex = index
          }
        })
        this.activeIndex = activeIndex
      };
      this.ws = ws
    }
  }
})
Copy the code

Each time the TAB connects to the WebSocket, it stores the connection object into an array.

// omit other code
const expressWs = require("express-ws")(app);
let clients = [];
let musicNum = null;

app.ws("/ws/:user_id".function (ws, req) {
  let { user_id } = req.params;
  clients.push({
    user_id,
    ws,
  });
  ws.send("Connection successful");

  ws.on("message".function (msg) {
    let parsed = {};
    try {
      parsed = JSON.parse(msg);
    } catch (error) {}
    musicNum = parsed;
    for (let i = 0; i < clients.length; i++) {
      let item = clients[i];
      if(item.user_id === user_id && item ! = =this) { item.ws.send(msg); }}}); ws.on("close".function () {
    for (let i = 0; i < clients.length; i++) {
      if (clients[i].ws === this) {
        clients.splice(i, 1); }}}); });Copy the code

Recommended by author:

Webpack Configuration Complete parsing (Optimization)

Write promises from scratch

I write this summary after interviewing 50 people

For more front-end information, please pay attention to the public number [front-end reading].

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles