最近在看微服務方面的東西,看到關於 多個微服務頁面間通訊和資料共享的解決方案,發現了一些比較陌生的 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
訊號的傳遞有點受限,必須要有 其他視窗的一個引用
,然後通過這個引用才能繼續下面一系列的操作,這種 引用
的來源有 iframe
的 contentWindow
屬性、執行 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
並不熟悉,只知道大概是幹什麼用的,但不知道具體細節,一直以為這個東西可以像 BroadcastChannel
或 postMessge
一樣,在一個頁面傳送訊號,另外一個頁面就可以即時自動接收,就像是兩個人打電話,一個人說話,另外一個人什麼都需要做就可以立馬聽到,但是現在弄完了才發現並不是這樣
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.js
的 hash
值不同,單例模式無法成立:
// 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
文件也翻了好幾遍也沒找出來原因,大眼瞪小眼了半天,後來終於在網上找到原因,原來 Chrome
、Edge
等瀏覽器下的這個 storage
事件必須由其他同源頁面觸發,同一個頁面是無法自己監聽自己的 storage
事件的(好像 FireFox
可以自己監聽自己?沒測過不確定),這種設計簡直就差點沒在自己身上寫個 支援頁面間通訊 的字串了
websocket
WebSocket
是 HTML5
開始提供的一種在單個 TCP
連線上進行全雙工通訊的協議,常用的場景是即時通訊
想要使用此項技術,必須瀏覽器端和伺服器端都支援,node.js
的 websocket
解決方案,比較知名的是 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
所有的資料型別,包括 null
、undefined
等,是 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
則更像是 關係型資料庫
,無論是諸多概念的定義,還是操作指令都跟後端的一些關係型資料庫,例如 mysql
、sqlserver
等更像,相比於 IndexexDB
,Web 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
的定論是:
IndexedDB
是WebSQL
資料庫的取代品,W3C
組織在2010
年11
月18
日廢棄了webSql
。IndexedDB
和WebSQL
的不同點在於WebSQL
是關係型資料庫(複雜)IndexedDB
是key-value
型資料庫(簡單好使).
呵呵,在沒看到這句話之前,我一直以為 WebSql
更先進,該被替換掉的是 IndexedDB
,沒想到皁滑造化弄人,人生處處有驚喜
總結
只是隨便從微服務方面知識中看到的一個點,擴充套件開來就是一篇文章,果然是學無止境啊。以前上大學的時候,每天時間多的是,於是每天都在歡快地學習,新知識出來一個學一個,不亦樂乎,現在工作了,每天業務程式碼都寫不完,然而還是要擠出時間來學習新知識,關注了一大堆的技術公眾號,每天推送的文章看都看不完,對技術瞭解得越多就越感覺技術的無邊無際,只想感慨一句,求求你們別再弄新東西出來了,老子學不下去了 活到老學到老。