Browser cross-page communication scheme

Author: GZHDEV 2021/1/6

Preface: A few days ago, I made some knowledge points about Web worker. Today, I will take a look at another interesting thing, the implementation scheme of cross-page communication. This article is quite long, so the methods are quite complete, if you want to understand the browser across the page communication solution, read this articles. ❤️

Possible solutions:

  • BroadcastChannel
  • postMessage
  • localStorage
  • SharedWorker
  • SSE
  • websocket
  • service worker
  • MQTT
  • cookie
  • cookieStore
  • indexedDB

1. Broadcast Channel

The first is broadcast channel, an API that may not be widely known, but is very easy to use when communicating across pages, with just a few simple methods. However, broadcast channels only support cross-page communication under the same source

<! -- page1.html -->
<h1>page1</h1>
<script>
  const bc = new BroadcastChannel('first-channel');
  bc.onmessage = e= > {
    console.log('page1: ', e);
  }
  setTimeout(() = > {
    bc.postMessage('hello every one, this message from page1.html');
    bc.close();
  }, 2000);
</script>
Copy the code
<! -- page2.html -->
<h1>page2</h1>
<script>
  const bc = new BroadcastChannel('first-channel');
  bc.onmessage = e= > {
    console.log('page2: ', e);

    bc.postMessage('this is message from page2.html');
  }
</script>
Copy the code

Specific steps:

  1. The BroadcastChannel constructor instantiates an instance, passing in the name of the message channel
  2. Create a message communication channel that communicates through two simple receive methods
  3. PostMessge sends messages and onMessage receives messages

It’s so easy, isn’t it? Go ahead

2. postMessage

Next, postMessge, which is super compatible and easy to use if it meets your needs

Look at this compatibility, not stable, look at the grammar ↓

otherWindow.postMessage(message, targetOrigin, [transfer]);

Advantages and disadvantages: the window must be opened by itself, and the window handle can be obtained. But it can communicate across domains.

I am not very good at words, directly on the demo to see it

<! -- page1.html -->
<h1>page1</h1>
<button id="btn">send</button>
<script>
  const child = window.open('page2.html');
  const btn = document.querySelector('#btn');
  btn.addEventListener('click'.() = > {
    // Cross-domain origin is required
    // child.postMessage('hello', 'http://test2.local.com:8080');
    // child.postMessage('hello', '*')

    child.postMessage('hello');
    console.log('send ok! ');
  });
</script>
Copy the code
<! -- page2.html -->
<h1>page2</h1> 
<script>
  window.addEventListener('message'.e= > {
    console.log('page2: ', e);
    // Cannot be retrieved directly, but e.ource. PostMessage can reply to the main window
    // console.log('source: ', e.source);
  })
  window.addEventListener('storage'.e= > {
    console.log('storage: ', e);
  })
</script>
Copy the code

PostMessage can safely communicate across pages (supports cross-domain communication)

Security issues

If you don’t want to get information from other sites or browser plug-ins, don’t listen for messge events on your page.

3. localStorage

Localstorage, sessionStorage, sessionStorage, sessionStorage SessionStorage life cycle only exists during the session, when you close the page only automatically removed.

When localstorage changes, it will trigger the storage event. But it should be noted that the storage event of localstorage can only be monitored across the page. This page is invalid. Want to achieve this page to listen to localstorage is also very simple, rewrite their own localstorage API, manual dispatch event is good, say so much nonsense, not directly to see the demo

<! -- page1.html -->
<h1>page1</h1>
<button class="btn">set</button>
<script>
  const btn = document.querySelector('.btn');
  // Change localstorage page to listen for storage changes to override setItem
  window.addEventListener('storage'.e= > {
    console.log('storage: ', e);
  })
  // Override the setItem method
  localStorage.setItem = (key, value) = > {
    // Custom events
    const event = new CustomEvent('storage', {
      detail: {
        oldData: { [key]: localStorage.getItem(key) },
        newData: { [key]: value },
        localData: { ...localStorage },
      }
    });
    localStorage[key] = value;
    // Manually dispatch events after completing native setItem functions
    window.dispatchEvent(event);
  }
  btn.addEventListener('click'.() = > {
    localStorage.setItem('hello'.'guozhenghong');
  })
</script>
Copy the code
<! -- page2.html -->
<h1>page2</h1>
<script>
  window.addEventListener('storage'.e= > {
    console.log(e);
  })
</script>
Copy the code

Matters needing attention

  1. Storage listeners under the page that triggered the write operation are not triggered
  2. Storage events are raised only when the same value is changed, that is, repeatedly setting the same value does not trigger storage listening
  3. The localStorage value cannot be set in Safari stealth mode

4.SharedWorker

The fourth cross-page communication scheme to be introduced is this SharedWorker. The principle of cross-page communication is that this API will share a worker and communicate through the same port. Note that this product does not support cross-domain communication. For more details about SharedWorker, you can go to MDN’s official website or read my previous article about Worker. In the demo

<! -- page1.html -->
<h1>page1</h1>
<input type="text" class="value-box">
<h2 class="content"></h2>
<button class="set-message">setMessage</button>
<button class="get-message">getMessage</button>
<script>
  const$=el= > document.querySelector(el);
  const sw = new SharedWorker('worker.js');
  $('.set-message').addEventListener('click'.() = > {
    sw.port.postMessage($('.value-box').value);
    console.log('set ok! ');
  });
  DOM3 level addEventListener is used to listen. Port. Start must be enabled
  $('.get-message').addEventListener('click'.() = > {
    sw.port.postMessage('get');
    console.log('get ok! ');
  });
  sw.port.addEventListener('message'.({data}) = >{$('.content').textContent = `value: ${data}`;
    console.log(data);
  });
  sw.port.start();
</script>
Copy the code
<! -- page2.html -->
<h1>page2</h1>
<input type="text" class="value-box">
<h2 class="content"></h2>
<button class="set-message">setMessage</button>
<button class="get-message">getMessage</button>
<script>
  const$=el= > document.querySelector(el);
  const sw = new SharedWorker('worker.js');
  $('.set-message').addEventListener('click'.() = > {
    sw.port.postMessage($('.value-box').value);
    console.log('set ok! ');
  });
  $('.get-message').addEventListener('click'.() = > {
    sw.port.postMessage('get')
    console.log('get ok! ');
  });
  sw.port.addEventListener('message'.({data}) = > {
    console.log(data);
    $('.content').textContent = `value: ${data}`;
  });
  sw.port.start();
</script>
Copy the code
// worker.js
let store = null;
onconnect = (e) = > {
  const port = e.ports[0];
  handleMessage(port);
}
// Cache data from the page by defining a global variable store
const handleMessage = (port) = > {
  port.onmessage = (e) = > {
    switch(e.data) {
      case 'get': 
        port.postMessage(store);
        break;
      default:
        store = e.data;
        break; }}}Copy the code

The function of the demo above is that the sending and receiving of information between two pages uses a common worker.js to handle the logic.

5. Websocket

As we all know, HTTP request is made by the client, the server responds, HTTP request is stateless, HTTP protocol, the server is not able to take the initiative to push data to the client, wait to say SSE this goods can, first by the client to initiate a request, Websocket is an application protocol that builds on TCP connections. It doesn’t have much to do with HTTP, but in the browser, you need to use HTTP for handshake and disconnection.

Websocket is also part of the HTML5 specification. Browsers that support HTML5 have integrated webSocket APIS, but the demo here uses a rewrapped socket-. IO client. The server uses nodeJS,

// index.js
const fs = require("fs");
const server = require("http").createServer((req, res) = > {
  if (/html$/i.test(req.url.toLowerCase())) {
      fs.createReadStream('./public'+ req.url).pipe(res); }});const io = require('socket.io')(server);

io.on('connection'.client= > {
  console.log('client connect to server! ');
	// Use IO to send broadcast messages that can be listened to by all connected clients
  client.on('broadcast-channel'.data= > {
    io.emit('broadcast-channel', data);
  });

  client.on('disconnect'.e= > {
    console.log('disconnect: ', e);
  });
});

server.listen(3000);
Copy the code
<! -- page1.html -->
<h1>page1</h1>
<input type="text" class="value-provider">
<h1>A received left:<p class="content-box" style="color: rebeccapurple;"></p></h1>
<button class="send-button">send message</button>

<! This is socket. IO based on express exposed JS file -->
<script src="/socket.io/socket.io.js"></script>
<script>
  const contentBox = document.querySelector('.content-box');
  const sendButton = document.querySelector('.send-button');
  const valueProvider = document.querySelector('.value-provider');
  // Establish a socket connection
  const socket = io('ws://localhost:3000');

  socket.on('connect'.function() {
    console.log('connect to server.');
		// Listen for broadcast-channel events to receive data from the server
    this.on('broadcast-channel'.data= > contentBox.textContent = data);
		// emit sends data to the server
    sendButton.addEventListener('click'.() = > socket.emit('broadcast-channel', valueProvider.value));
  });
</script>
Copy the code
<! -- page2.html -->
<h1>page2</h1>
<input type="text" class="value-provider">
<h1>A received left:<p class="content-box" style="color: rebeccapurple;"></p></h1>
<button class="send-button">send message</button>

<script src="/socket.io/socket.io.js"></script>
<script>
  const contentBox = document.querySelector('.content-box');
  const sendButton = document.querySelector('.send-button');
  const valueProvider = document.querySelector('.value-provider');
  const socket = io('ws://localhost:3000');

  socket.on('connect'.function() {
    console.log('connect to server.');

    this.on('broadcast-channel'.data= > contentBox.textContent = data);

    sendButton.addEventListener('click'.() = > this.emit('broadcast-channel', valueProvider.value));
  });
</script>
Copy the code

Ok, the main content of this article is cross-page communication, websocket knowledge is very much, I will not talk about it here, later free to write a separate understanding of Websocket, Websocket is widely used, such as real-time bullet screen, Instant messaging, chat, etc., can be implemented with Websocket.

6. Server-sent events(SSE)

In the process of daily data interaction between the front and back ends, in most cases, the client initiates the request to the server, and the server will return the data. The technology of the server pushing messages to the client actively at any time and anywhere, in addition to websocket in the Web, Server-sent Events(SSE) mentioned here is also a different thing from Websocket

Websocket just connects and disconnects when going through HTTP for handshake and wave

Server-sent Events is a complete HTTP, but strictly speaking, HTTP does not support the Server to actively push data, but server-Sent Events is a workaround. After the client initiates a request, The server returns a message telling the client that a Stream is coming. The client receives the message and does not close the connection, waiting for the next Stream from the server.

Okay, so now that we know how it works let’s look at the demo,

// index.js
const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, './public')));

let clients = {};
// Register with /register
app.get('/register'.(req, res) = > {
  const {id = 'un'} = req.query;
  clients[id] || (clients[id] = res);
  // SSE must return the content-type of text/event-stream
  res.setHeader('Content-Type'.'text/event-stream');
  // Return the data format, starting with data
  res.write('data: register success! \n\n');
  req.on('aborted'.() = > clients = []);
});
// Information that needs to be notified
app.get('/notice'.(req, res) = > {
  const {message} = req.query;
  // Notify register's client
  for (let ki in clients) {
    clients[ki].write(`data: ${message}\n\n`);
  }
  res.status(200).end();
})

app.listen(9999.console.log('http://localhost:9999'));
Copy the code
<! -- register.html -->
<h1>sse demo</h1>
<h2></h2>
<script>
  const es = new EventSource('/register? id=one', { withCredentials: true });
  const messageBox = document.querySelector('h2');
  es.onmessage = e= > {
    // register to listen to data sent by SSE
    console.log('onmessage: ', e);
    messageBox.textContent = `message: ${e.data}`;
  }
  es.addEventListener('close'.() = > {
    es.close()
  }, false)
</script>
Copy the code
<! -- notice.html -->
<script src="./axios.min.js"></script>
<script>
  const sendButton = document.querySelector('.sendButton');
  const input = document.querySelector('.inputBox');
  const sendMessage = async (message) => {
    const res = await axios.get('/notice', {
      params: {
        message,
      }
    });
    console.log('res: ', res);
  }

  sendButton.addEventListener('click'.() = > {
    console.time('send');
    sendMessage(input.value || 'this is message from page2.html');
    console.timeEnd('send');
  })

</script>
Copy the code

Well SSE this thing is part of the HTML5 specification, in the IE series of old browsers is not supported, the use of time must be based on the specific analysis of business scenarios oh.

7. ServiceWorker

Service Workers essentially act as proxy servers between Web applications, browsers, and the network (when available). This API is designed to create an effective offline experience that intercepts network requests and takes appropriate action to update resources from the server based on whether the network is available. It also provides a portal to push notifications and access background synchronization apis.

Rely on

As an advanced feature of modern browsers, Service Worker relies on the basic capabilities of fetch, Promise, CacheStorage, Cache API, etc. Cache provides a storage mechanism for Request/Response object pairs. CacheStorage provides a mechanism for storing Cache objects.

Functions and features:

  • An independent worker thread, independent of the current web process, has its own independent worker context.
  • Once installed, it exists forever unless it is manually unregistered
  • You can wake up directly when you need it and sleep automatically when you don’t need it
  • Programmable interception of proxy requests and returns, cache files, cached files can be retrieved by web processes (including network offline state)
  • Offline content developers can control
  • Can push messages to the client
  • You cannot manipulate the DOM directly
  • It must work in HTTPS environment
  • Asynchronous implementations, mostly implemented internally through promises

Service worker is a part of pWA content. This part mainly discusses how to communicate across pages. The content about service worker will be written in detail later

<! -- page1.html -->
<h1>page1</h1> 
<button>send</button>

<script>
  / / registration service
  navigator.serviceWorker.register('sw.js').then(() = > {
    console.log('register success! ');
  });
  document.querySelector('button').addEventListener('click'.() = > {
    // Send information to the worker via controller
    navigator.serviceWorker.controller.postMessage('hello');
  });
</script>
Copy the code
<! -- page2.html -->
<h1>page2</h1>
  
<script>
  navigator.serviceWorker.register('sw.js').then(() = > {
    console.log('register success! ');
  });
  navigator.serviceWorker.addEventListener('message'.({data}) = > {
    console.log('message: ', data);
  });
</script>
Copy the code
// sw.js
self.addEventListener('message'.async event => {
  // Get all clients registered with sw.js
  const clients = await self.clients.matchAll();
  clients.forEach(client= > client.postMessage('sw message: xx'));
})
Copy the code

In addition to the above approach, there is a more general way to communicate across pages in the service worker –MessageChannel

This is a less commonly used API. Instantiated objects have two channels, port1 and port2, each of which can listen for Message events and both of which can send messages via postMessage.

Messages sent by port1 can be monitored by Port2

Messages sent by port2 can be monitored on Port1

You can do this with MessageChannel in a service worker

const mc = new MessageChannel();
// Listen for messages
mc.port1.onmessage = function(e) {
  console.log(e.data);
}
navigator.serviceWorker.controller.postMessage('hello', [mc.port2]);
Copy the code
self.addEventListener('message'.e= > {
  // Get the MC. part2 channel from E.ports and send the message
  e.ports[0].postMessage('xx');
});
Copy the code

This is the service worker-based approach to cross-page communication, but it is worth noting that service Woker does not support cross-domain communication.

8. MQTT message queue

MQTT (Message Queuing Telemetry Transport) is a lightweight communication protocol based on publish/subscribe mode. MQTT is a reencapsulation of TCP/IP developed by IBM Released in 1999. The biggest advantage of MQTT lies in the realization of network communication with low cost and less flow. MQTT protocol is used more in the Internet of Things. As an ambitious engineer, we have to learn it.

Instead of building the broker yourself, use the free online message queue. If you are interested, use it yourself

activemq, rocketmq… These message queues set up their own brokers, without further ado

There are several key concepts in the MQTT protocol that need to be understood:

  1. A broker is a message queue broker server
  2. Topic topic, subscribe and publish specified
  3. Subscribe Subscribe to a topic
  4. Publish publishes topic content

First we download the mqtt.js package

npm install mqtt
# Or use YARN for installation
yarn add mqtt
Copy the code

And then there’s blah, blah, blah, blah, blah simple demo code

<! -- page1.html -->
<h1>page1</h1>
<input type="text" class="value-provider">
  <h1>content: <p class="content"></p></h1>
    <button class="send-button">send</button>
<script src=".. /node_modules/mqtt/dist/mqtt.js"></script>
<script>
const$=el= > document.querySelector(el);

const publishTopic = 'from-page1-message';
const subscribeTopic = 'from-page2-message';
MQTT can now only be connected via webSocket in the browser
const brokerAddress = 'ws://broker.emqx.io:8083/mqtt';
// Create a Brocker connection
const client = mqtt.connect(brokerAddress);

client.on('connect'.() = > {
  console.log('connected to broker! ');
  // Subscribe to topics. Publish topics
  client.subscribe(subscribeTopic, (err) = > {
    if(! err) {console.log('successfully connected to mqtt server');
      $('.send-button').addEventListener('click'.() = > client.publish(publishTopic, $('.value-provider').value)); }}); });// Listen to message only if you have subscribed to the topic
client.on('message'.(topic, message, packet) = > {
  console.log('received message: ', message.toString(), packet);
  $('.content').textContent = message.toString();
});
</script>
Copy the code
<! -- page2.html -->
<h1>page2</h1>
<input type="text" class="value-provider">
<h1>content: <p class="content"></p></h1>
<button class="send-button">send</button>
<script src=".. /node_modules/mqtt/dist/mqtt.js"></script>
<script>
  const$=el= > document.querySelector(el);

  const publishTopic = 'from-page2-message';
  const subscribeTopic = 'from-page1-message';
  const brokerAddress = 'ws://broker.emqx.io:8083/mqtt';
  const client = mqtt.connect(brokerAddress);

  client.on('connect'.() = > {
    console.log('connected to broker! ');
    client.subscribe(subscribeTopic, (err) = > {
      if(! err) {console.log('successfully connected to mqtt server');
        $('.send-button').addEventListener('click'.() = > client.publish(publishTopic, $('.value-provider').value)); }}); }); client.on('message'.(topic, message, packet) = > {
    console.log('received message: ', message.toString(), packet);
    $('.content').textContent = message.toString();
  });
</script>
Copy the code

A beautiful and friendly MQTT client, MQTTX, can also be recommended

EMQ website www.emqx.io/cn/mqtt/pub…

9. IndexedDB

What is with IndexedDB? Look at the two capital letters, DataBase, which is the website’s own DataBase, what can it be used for? Little fool, the database is of course used to store data, you may say that localstorage can not also be used to do data storage? IndexedDB can store a certain amount of data, depending on the size of your hard drive. IndexedDB is an underlying API for storing large amounts of structured data (including file/binary Large objects (BLObs)) on the client side that uses indexes for high-performance searches of data. While Web Storage is useful for storing smaller amounts of data, it is inadequate for storing larger amounts of structured data. IndexedDB provides a solution to this scenario.

Basic procedure for using indexedDB

  1. Open the database
  2. Create an object store in the database
  3. Start a transaction and send a request to perform some database operation, such as adding or extracting data
  4. Wait for the operation to complete by listening for DOM events of the correct type
  5. Do something on the result of the operation (found in the Request object)

Although this scheme allows for cross-page communication, it is not the primary purpose of indexDB that is a bit tricky.

If you use indexedDB for cross-page communication, the idea is that one page of the same origin writes to the database, and another page of the same origin reads the data in the indexedDB for cross-page data communication, and cookies for cross-page communication are the same principle. It’s low and inefficient.

IndexedDB indexedDB indexedDB indexedDB

<! -- demo.html -->
<script>
  const dbName = 'demo_db';
  // Open the database, if not, it will be automatically created, the version is specified as 2, do not specify the default value 1, do not fill floating point
  const request = indexedDB.open(dbName, 2);
  // Listen on events
  request.onerror = e= > console.error('error: ', e);
  request.onsuccess = e= > console.log('success: ', e);
  request.onupgradeneeded = e= > {
    console.log('upgradeneeded: ', e);
    // Get the IDBOpenDBRequest object after the connection is established
    const db = e.target.result;
    // Create the objectStore storage space
    const objectStore = db.createObjectStore('firstCollection', {keyPath: 'id'.autoIncrement: true});
    /** * createIndex * createIndex can set an index for the current storage space. It takes three arguments: * The first argument, the name of the index. * The second parameter, which specifies which attribute of the stored data is used to build the index. * The third property, the Options object, where the value of the unique property is true to indicate that index values are not allowed to be equal. * /
    objectStore.createIndex('age'.'age', {unique: false});
    objectStore.createIndex('name'.'name', {unique: true});
    Transaction supports three modes: * readOnly, read-only. * Readwrite. * versionchange, database versionchange. * /
    objectStore.transaction.oncomplete = e= > {
      console.log('transaction complete: ', e);

      const collectionTransaction = db.transaction(['firstCollection'].'readwrite').objectStore('firstCollection');
      // Add data
      const awaitInsertData = [
        {name: 'gzh'.age: 18},
        {name: 'xx'.age: 19},
        {name: 'hello'.age: 3},
      ]
      awaitInsertData.forEach(data= > collectionTransaction.add(data));
      // Query data from components
      const queryByKey = collectionTransaction.get(2);
      queryByKey.onsuccess = e= > {
        const queryResult = e.target.result;
        console.log('queryResult(key): ', queryResult);
      }
      // Query data by index
      const index = collectionTransaction.index('name');
      console.log('index: ', index);
      const queryByIndex = index.get('gzh');
      queryByIndex.onsuccess = e= > {
        const queryResult = e.target.result;
        console.log('queryResult(index): ', queryResult);
        // Modify the data
        queryResult.age = 19;
        collectionTransaction.put(queryResult);
        // Delete data
        collectionTransaction.delete(3); }}}</script>
Copy the code

The demo code is a bit long, but it is reasonable, after all, indexedDB—– frontend its own database, database is so powerful, many things are normal.

10. Cookie & cookieStore

Like indexedDB, cross-page communication is implemented on a rotational basis, which is mostly trivial, so I won’t mention it. Cookies also operate weirdly in browsers, and have long been despised and ridiculed by developers. However, I recently noticed that Chrome has introduced an API called cookieStore to solve this problem, and cookieStore changes across the page when the cookie changes, also mentioned here, cookieStore is very simple to learn. This compatibility is currently poor, but I believe that under the leadership of Chrome, it will definitely be popular in the future, as the new cookie management API is just around the corner.

Note:

This parameter can be used only in HTTPS mode

Talk is cheap. Look at this little demo I wrote

<! -- page1.html -->
<h1>page1</h1>
<button class="btn">set cookie</button>
<script>
  const btn = document.querySelector('.btn');
  cookieStore.addEventListener('change'.e= > {
    console.log("page1: ", e);
  });
  btn.addEventListener('click'.function() {
    cookieStore.set({name: 'xx'.value: 'xx'});
  });
</script>
Copy the code
<! -- page2.html -->
<h1>page2</h1> 
<script>
  cookieStore.addEventListener('change'.e= > {
    console.log("page2: ", e);
  });
</script>
Copy the code

11. A summary

The essays of more, writing time is quite long, but the process of writing learned a lot, and deepened the knowledge of a lot of old memories and understanding, free will write technical articles, summing up and ascend in the output, hope on technology can go higher and further. ❤ ️ first write in Denver post a little bit nervous! And that interested in learning together can pay attention to my wechat public ha!