頁面間通訊與資料共享解決方案簡析

清夜發表於2018-09-19

最近在看微服務方面的東西,看到關於 多個微服務頁面間通訊和資料共享的解決方案,發現了一些比較陌生的 API,說是陌生其實專門拿出來也能說出個所以然來,也知道是個什麼東西,但就是不熟練,憑空想的話就很難能想到,看了一下覺得有些門道,於是索性擴充套件開來整理了一下

BroadcastChannel

Broadcast 也是“廣播”的意思,將訊號廣播出去,允許其他人接聽。

API允許同一原始域和使用者代理下的所有視窗、iFrames等進行互動,屬於 同源通訊。也就是說,如果使用者開啟了同一個網站的的兩個標籤視窗,如果網站內容發生了變化,那麼兩個視窗會同時得到更新通知。

使用的場景,如,使用者同時依次開啟某個網站的幾個頁面,然後在其中一個頁面 A進行登入操作,那麼其他的頁面就可以通過 BroadcastChannel收到來自頁面 A的登入狀態,從而能夠完成多個頁面自動同步登入狀態的目的。

// A頁面向外廣播訊號
// 建立控制程式碼
const cast = new BroadcastChannel('mychannel')
// data 可以是任何 JS資料型別
const data = 'I\'m from Page A'
// 廣播訊號
cast.postMessage(data)
// 關閉連線
cast.close()
複製程式碼
// B頁面監聽同源下所有頁面傳送出的“廣播”
//  BroadcastChannel的引數,即channel號必須與想要監聽的廣播源相同,這裡是 mychannel
const cast = new BroadcastChannel('mychannel')
// 接收訊號
cast.onmessage = function (e) {
  console.log(e.data) // => I'm from Page A
}
// 關閉連線
cast.close()
複製程式碼

頁面間通訊與資料共享解決方案簡析

用起來很順手,也沒什麼複雜的道道,BroadcastChannel的初始化引數 channel,可以看做是一個廣播頻道,只要同源下加入這個頻道的頁面,都能夠互相收發訊號進行通訊,但是瀏覽器支援度很不樂觀,而且一直也都沒什麼進展,總感覺將來某天就要嗝屁了

postMessage

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

相比於 BroadcastChannel來說, postMessage明顯幸福多了,postMessage支援 跨域通訊,瀏覽器支援度也秒殺 BroadcastChannel,達到了完全可在生產環境使用的地步,說明瀏覽器廠商對這個還是很熱衷的。資本推動技術,沒毛病

頁面間通訊與資料共享解決方案簡析

A頁面通過 window.open獲得 B頁面的控制程式碼,向 B頁面傳送訊號,並監聽 B頁面回傳回來的訊號

<!-- A頁面 -->
<div id="msg"></div>
<script>
  window.onload = () => {
    // 獲取控制程式碼
    var opener = window.open('http://127.0.0.1:9001/b.html')
    // setTimeout 是為了等到真正獲取到 opener的控制程式碼再傳送資料
    setTimeout(() => {
      // 只對 域名為 http://127.0.0.1:9001的頁面傳送資料訊號
      opener.postMessage('red', 'http://127.0.0.1:9001');
    }, 0)
    // 監聽從控制程式碼頁面傳送回來的資料訊號
    window.addEventListener('message', event => {
      if(event.origin === 'http://127.0.0.1:9001'){
        document.getElementById('msg').innerHTML = event.data
      }
    })
  }
</script>
複製程式碼

B頁面接收 A頁面的訊號,並通過事件控制程式碼反向對 A頁面傳送資料訊號

<div id="box">color from a.html</div>
<script type="text/javascript">
  window.addEventListener('message', event => {
    // 通過origin屬性判斷訊息來源地址
    // 只有當資料訊號來源於 http://127.0.0.1:9001的伺服器才接收
    if(event.origin === 'http://127.0.0.1:9001'){
      // 獲取資訊員的資料訊號
      document.getElementById('box').style.color = event.data
      // 通過 event.source向訊號源反向傳送資料
      event.source.postMessage('got your color!', event.origin)
    }
  })
</script>
複製程式碼

postMessage用起來也比較簡單,稍微需要注意一下的是,由於此 API可以跨域通訊,能力越大責任也就越大,所以涉及到安全性問題,一般在傳送訊號和接收訊號的時候,都需要指定訊號源以規避安全問題

相比於 BroadcastChannel的一侷限點是,postMessage訊號的傳遞有點受限,必須要有 其他視窗的一個引用,然後通過這個引用才能繼續下面一系列的操作,這種 引用的來源有 iframecontentWindow屬性、執行 window.open返回的視窗物件、或者是命名過或數值索引的window.frames,場景有限,明顯不如 BroadcastChannel 直接指定一個 channel號來得靈活

SharedWorker

Web worker分為兩種:專用執行緒 dedicated web worker、共享執行緒 shared web worker

Dedicated web worker隨當前頁面的關閉而結束;這意味著 Dedicated web worker只能被建立它的頁面訪問;與之相對應的 Shared web worker可以被多個頁面訪問(包括多個標籤頁和 iframe),不過這些頁面必須是同源的,即 Shared web worker支援的是 同源通訊

下面是一個 SharedWorker

// worker.js
// 共享的資料
let shareData = 0
// 監聽主執行緒的連線
onconnect = function(e) {
  const port = e.ports[0]
  port.onmessage = function(e) {
    if (e.data === 'get') {
      // 向連線的主執行緒傳送訊號
      port.postMessage(shareData)
    } else {
      // 將主執行緒發來的資料設定為 worder內的 共享資料
      shareData = e.data
    }
  }
}
複製程式碼

A頁面設定 SharedWorker中的資料欄位

<input type="text" id="textInput" />
<input type="button" value="設定共享資料" />

<script>
  const worker = new SharedWorker('worker.js')
  const inputEle = document.querySelector('#textInput')

  inputEle.onchange = () => {
    console.log('Message posted to worker')
    // 向 worker 傳送資料訊號
    worker.port.postMessage(inputEle.value)
  }
</script>
複製程式碼

B頁面獲取 SharedWorker中的資料欄位

<div id="result"></div>
<button id="btn">獲取 SharedWorker中的共享資料</button>
<script>
  const worker = new SharedWorker('worker.js')
  var result = document.querySelector('#result')
  // 傳送獲取獲取 SharedWorder 中共享資料的請求
  document.getElementById('btn').addEventListener('click' , () => {
    // 向 worker傳送訊號
    worker.port.postMessage('get')
  })
  // 接收從 SharedWorder傳送來的共享的資料
  worker.port.onmessage = e => {
    console.log('Message received from worker')
    // 在頁面上顯示獲取到的 worker共享資料
    result.textContent = e.data
  }
</script>
複製程式碼

最終,在 A頁面中設定的值,或被 B頁面獲取到

worker.js 這個檔案被 A頁面和 B頁面分別載入,但卻可以共享資料,類似於 單例模式,雖然使用了 new操作符,但最後兩個頁面獲取到的東西卻是一樣的

之前對於這個 SharedWorker並不熟悉,只知道大概是幹什麼用的,但不知道具體細節,一直以為這個東西可以像 BroadcastChannelpostMessge一樣,在一個頁面傳送訊號,另外一個頁面就可以即時自動接收,就像是兩個人打電話,一個人說話,另外一個人什麼都需要做就可以立馬聽到,但是現在弄完了才發現並不是這樣

B頁面確實可以獲取到 A頁面設定的資料,但這種獲取是需要主動的操作,不像是打電話,倒像是儲存,一個頁面在公共區域存了一個資料,另外一個頁面想要了,需要主動去獲取,我是感覺這個東西可能並不是適合於頁面通訊,當然了,SharedWorker本來就不是用於頁面通訊的,所以沒有預期的效果也是情有可原的

另外,在測試 SharedWorker的時候,碰到了幾個坑,這裡提一下:

  • worker.js 指令碼會存在快取

當頁面第一次載入完了 worker.js後,後續再修改 worker.js這個檔案,然後重新整理頁面,會發現 worker.js其實並沒有變化,還是修改之前的那一個,這是因為 worker.js被瀏覽器快取了,強制重新整理瀏覽器也沒用

一個解決方案就是給 worker.js檔案加上 hash,例如:

const worker = new SharedWorker('worker.js?hash=v1')
複製程式碼
  • 載入的 worker.js全名稱要一致

根據上面的方法,頁面就能更新 worker.js了,但還需要注意的是,如果想要 A頁面和 B頁面(或者更多的頁面) new出來的 worker是同一個,也就是說可以共享資料,那麼這些頁面載入的 worker.js不僅需要是同一個檔案,而且全名稱也必須要完全一樣,包括 hash

下面這種情況,A頁面和 B頁面就無法進行資料共享,因為它們載入的 worker.jshash值不同,單例模式無法成立:

// A 頁面,hash值為 v111
const worker = new SharedWorker('worker.js?hash=v111')
// ...

// B頁面,hash值為 v222
const worker = new SharedWorker('worker.js?hash=v222')
複製程式碼

頁面間通訊與資料共享解決方案簡析

相比於 dedicated web worker來說,shared web worker的瀏覽器支援度明顯弱了一截,可能是因為現今 dedicated web worker的應用場景要比 shared web worker多上很多 另外,微軟系的 IE瀏覽器以及 Edge都完全不支援此特性,原因是微軟認為此 API存在安全隱患,估計以後也不太可能支援了

Local Storage

Local Storage用於儲存資料,但由於存在 storage這個事件,所以也可以對儲存狀態進行監聽,從而達到頁面間通訊的目標

// A頁面
window.onstorage = function(e) {
  console.log(e.newValue); // previous value at e.oldValue
};
// B頁面
localStorage.setItem('key', 'value');
複製程式碼

一開始,我一直以為同一個頁面是可以自己監聽自己的 storage事件,誰知試了半天都沒用,MDN文件也翻了好幾遍也沒找出來原因,大眼瞪小眼了半天,後來終於在網上找到原因,原來 ChromeEdge等瀏覽器下的這個 storage事件必須由其他同源頁面觸發,同一個頁面是無法自己監聽自己的 storage事件的(好像 FireFox可以自己監聽自己?沒測過不確定),這種設計簡直就差點沒在自己身上寫個 支援頁面間通訊 的字串了

websocket

WebSocketHTML5開始提供的一種在單個 TCP 連線上進行全雙工通訊的協議,常用的場景是即時通訊

想要使用此項技術,必須瀏覽器端和伺服器端都支援,node.jswebsocket解決方案,比較知名的是 socket.io

伺服器端

// index.js
const server = require('http').createServer()
const io = require('socket.io')(server)

io.on('connection', socket => {
  socket.on('clientMsg', data => {
    // broadcast 直接廣播出去,除了傳送者外,其他所有連線者都可以接收到
    socket.broadcast.emit('serverMsg', data)
  })
})
server.listen(3000)
複製程式碼

上面是伺服器端的全部程式碼,需要安裝 socket.io這個包,為了方便演示,所以去除了其他不必要的邏輯,主要功能就是開啟一個 socket連線,能接收並廣播訊息,類似於一個聊天室伺服器

客戶端程式碼:

<!-- client.html -->
<!-- 訊息列表 -->
<ul id="ul"></ul>
<input type="text" id="textInput" />
<button onclick="btnClick()">傳送</button>
<script>
  const socket = io('http://localhost:3000')
  // 接收伺服器發過來的訊息
  socket.on('serverMsg', data => {
    addLi(`${data.id}: ${data.msg}`)
  })
  
  const ul = document.getElementById('ul')
  const textInput = document.getElementById('textInput')
  const id = new Date().getTime()
  
  // 向伺服器傳送訊息
  function btnClick() {
    socket.emit('clientMsg', { msg: textInput.value, id })
    textInput.value = ''
  }
  function addLi(text) {
    const li = document.createElement('li')
    li.innerText = text
    ul.appendChild(li)
  }
</script>
複製程式碼

上面是客戶端的主體程式碼,為了能與伺服器端配合使用,需要在頁面上引入 socket.io.js這個檔案,從而開啟瀏覽器端的 websocket

<script src="https://cdn.bootcss.com/socket.io/2.1.1/socket.io.js"></script>
複製程式碼

頁面間通訊與資料共享解決方案簡析

socket.io伺服器啟動後,在本地開啟客戶端頁面 client.html,多開啟幾個標籤頁,每個 client.html就是一個訊息接收者,傳送的訊息,其他頁面都能即時接收

websocket技術已經很成熟了,完全可以用於生產環境,除了稍微有點學習成本外,使用起來也沒什麼難度,也沒什麼使用限制,應用場景廣泛,不過如果僅僅是頁面間的通訊,用這個東西似乎就有點殺雞用牛刀的感覺,畢竟無論如何一個專門的 websocket伺服器是跑不了的

indexDB

LocalStorage一樣,indexDB也用於資料儲存,不過更“專業”

IndexedDB 是一種低階 API,用於客戶端儲存大量結構化資料(包括 檔案、blobs),該API使用索引來實現對該資料的高效能搜尋,區別於 LocalStorage只能儲存字串,IndexedDB可以儲存 JS所有的資料型別,包括 nullundefined等,是 HTML5規範裡新出現的 API

IndexedDB 是一種使用瀏覽器儲存大量資料的方法.它創造的資料可以被查詢,並且可以離線使用。IndexedDB對於那些需要儲存大量資料,或者是需要離線使用的程式是非常有效的解決方法

const request = indexedDB.open('dbBox')
request.onsuccess = function(e) {
  console.log('成功開啟 IndexDB')
  const myDB = e.target.result
  // 開啟一個讀寫的事物
  const transaction = myDB.transaction('person', 'readwrite')
  // 拿到 person表格的控制程式碼
  const store = transaction.objectStore('person')
  // 向 person表格中新增兩條資料
  store.add({name: 'jane', email:'jane@gmail.com'})
  store.add({name: 'kangkang', email:'kangkang@gmail.com'})
  // 所有的資料新增成功,觸發事務的 oncomplete事件
  transaction.oncomplete = function(event) {
    // 重新開啟一個查詢事務
    const getResult = myDB.transaction('person', 'readwrite').objectStore('person').get('jane')
    getResult.onsuccess= e => {
      console.log('查詢結果:', e.target.resule)
      // => {name: 'jane', email:'jane@gmail.com'}
    }
  }
}

// 在資料庫首次 open資料庫,或資料庫版本更新時會觸發此事件
request.onupgradeneeded = function(e) {
  const db = e.target.result
  // 如果不存在 person資料表
  if (!db.objectStoreNames.contains('person')) {
    // 新建資料表 person
    const objectStore = db.createObjectStore('person', {
      // 指定主鍵,類似於 primaryKey,後續查詢資料庫,就是通過這個主鍵的值,進行查詢的
      keyPath: "name"
    })
    // 建立資料表欄位 name
    objectStore.createIndex("name", "name", {
      //指定可以被索引的欄位,unique欄位用於指定是否唯一
      unique: true
    })
    // 建立資料表欄位 phone
    objectStore.createIndex("phone", "phone", {
      unique: false
    })
  }
}
複製程式碼

上述簡單示例包括了 連線資料庫、建立表、建立表欄位結構、新增資料、查詢資料等操作,註釋得比較清楚,就不多加解釋了

做完上述操作後, F12開啟瀏覽器的控制檯,選中 Application選項卡,選中 IndexeddDB,展開,即可看到儲存的資料

頁面間通訊與資料共享解決方案簡析

IndexDB作為本地儲存 API,沒有全域性監聽事件,所以無法用於頁面通訊,但可用於資料共享,此API涉及到較多的專屬名詞和概念,可能對於純正的前端來說不太好理解,不過只要是計算機專業出身的,對於資料庫的基本概念還是能夠理解的,本質上也沒什麼可說的,就是一個簡化版的本地資料庫

對於 indexedDB,瀏覽器桌面版的支援度還是不錯的

頁面間通訊與資料共享解決方案簡析

webSql

同樣是瀏覽器資料庫的一種,IndexedDB 可以看做是 NoSql資料庫,操作指令(增刪改查等)的呼叫方式更偏向於 “前端化”,Web SQL則更像是 關係型資料庫,無論是諸多概念的定義,還是操作指令都跟後端的一些關係型資料庫,例如 mysqlsqlserver等更像,相比於 IndexexDBWeb SQL更像是一個資料庫

另外,Web SQL 資料庫 API 並不是 HTML5 規範的一部分,但是它是一個獨立的規範,引入了一組使用 SQL 操作客戶端資料庫的 APIs,不過奇怪的是,這東西好像不是持久型儲存,頁面重新整理後,之前儲存的資料,包括資料庫、資料表就完全 drop

// 開啟一個名為 mydb 的資料庫,如果不存在則建立,並指定版本號為 1.0,資料庫的描述文字為 Test DB,大小限制在 2 * 1024 * 1024
var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024)
var msg
// 開啟事務
db.transaction(function (tx) {
  // 建立一個名為 LOGS的表,並且此表存在id 和 log兩個欄位
  tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)')
  // 向資料表中插入兩條資料
  tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "菜鳥教程")')
  tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "www.runoob.com")')
})
// 開始事務
db.transaction(function (tx) {
  // 從 LOGS 資料表查詢出所有的資料
  tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) {
    const len = results.rows.length
    // 格式化查詢出的結果
    const rst = Array(len).fill(1).map((v, index) => results.rows.item(index))
    console.log('查詢到的資料列表為:', rst)
  }, null)
});
複製程式碼

做完上述操作後, F12開啟瀏覽器的控制檯,選中 Application選項卡,選中 Web SQL,展開,即可看到儲存的資料

頁面間通訊與資料共享解決方案簡析

可以看到,上述操作出現了很多 sql,對於不熟悉資料庫的人來說相當於要多學一門 sql語言,雖然如果只是學習基本使用也沒什麼難度,但終歸對前端程式設計師不友好,另外,關係型資料庫出現在靈活到上天的 JavaScript世界,似乎有種不太和諧的感覺,於是對於 Web Sql的定論是:

IndexedDBWebSQL 資料庫的取代品, W3C組織在20101118日廢棄了 webSqlIndexedDBWebSQL的不同點在於 WebSQL 是關係型資料庫(複雜)IndexedDBkey-value型資料庫(簡單好使).

呵呵,在沒看到這句話之前,我一直以為 WebSql更先進,該被替換掉的是 IndexedDB,沒想到皁滑造化弄人,人生處處有驚喜

總結

只是隨便從微服務方面知識中看到的一個點,擴充套件開來就是一篇文章,果然是學無止境啊。以前上大學的時候,每天時間多的是,於是每天都在歡快地學習,新知識出來一個學一個,不亦樂乎,現在工作了,每天業務程式碼都寫不完,然而還是要擠出時間來學習新知識,關注了一大堆的技術公眾號,每天推送的文章看都看不完,對技術瞭解得越多就越感覺技術的無邊無際,只想感慨一句,求求你們別再弄新東西出來了,老子學不下去了 活到老學到老。

相關文章