一.使用場景
某項目某接口撩荣,請求返回的數(shù)據(jù)量較大椭更,消耗服務(wù)器資源嚴重哪审。如果在web端打開多個頁簽,同時請求該接口虑瀑,會造成服務(wù)端負荷過重湿滓。這時畏腕,共享工作者線程(SharedWorker)就派上了用場。根據(jù)SharedWorker的特性茉稠,第一個頁面打開線程描馅,就會創(chuàng)建一個SharedWorker,第二個頁面也打開而线,就會使用已經(jīng)創(chuàng)建的同一個線程铭污。如此,只要在線程中將數(shù)據(jù)進行一定的緩存處理膀篮,多個頁簽打開時嘹狞,就可以共用這一份數(shù)據(jù)。
二.SharedWorker的一些特性
首先誓竿,SharedWorker的機制在不同瀏覽器的實現(xiàn)方式可能是不一樣的磅网,所以多頁面共享線程只限于同一瀏覽器,而不能跨瀏覽器筷屡。chrome打開的頁面只能與chrome頁面共享涧偷,而不能與火狐瀏覽器共享。
如果使用火狐瀏覽器毙死,可以直接在控制臺查看ShareWorker中打印的信息燎潮。但如果是Chrome瀏覽器或者sougou瀏覽器,ShareWorker文件中打印的信息在主線程頁面是無法查看的扼倘。Chrome瀏覽器查看SharedWorker的方式是在瀏覽器地址欄訪問 Chrome://inspect => SharedWorker=>inspect确封, sougou的查看方式請自行搜索。
關(guān)于SharedWorker的瀏覽器兼容性再菊,可查詢MDN相關(guān)文檔爪喘。https://developer.mozilla.org/zh-CN/docs/Web/API/SharedWorker
三.SharedWorker的引入與一般使用
如果在傳統(tǒng)的js頁面,可以通過new SharedWorker(src)創(chuàng)建線程
var myWorker = new SharedWorker("worker.js");
如果用vite + vue3技術(shù)棧纠拔,需要這樣引入
import sharedWorker from './worker?sharedworker';
在vite的開發(fā)模式下秉剑,它實際生成的是這樣一個文件。其中type類型為module绿语,則允許在開發(fā)的使用import 秃症,export的es6模塊語法。但在生產(chǎn)構(gòu)建后吕粹,worker文件會按照引入鏈會被構(gòu)建成一個文件种柑。
export default function WorkerWrapper() { return new Worker("/apps/home/worker.ts?worker_file", { "type": "module" }) }
需要注意的是,worker內(nèi)部并沒有window對象匹耕,因此如果你所引入的包(例如axios登)中包含window的引用聚请,構(gòu)建后在運行時大概率會報錯。我通過在worker文件開頭增加了兩行代碼修復(fù)了這個問題。所以能夠這樣做驶赏,是因為self是window的一個子集炸卑,它具備了大部分window的對象。
self.window = self;
window = self;
以下是常用方法
worker.js
// 與專用工作者線程不同煤傍,共享線程通過port來使用盖文,port是一個MessagePort對象,包含了onmessage,postMessage等方法蚯姆。
// 連接后通過port操作
self.onconnect = ({ ports }) => {
ports.forEach((port) => {
port.onmessage = (ev) => {
const someData = {}
port.postMessage(someData);
};
port.onmessageerror = (ev) => {
console.log('error', ev);
};
});
};
四.進一步對調(diào)用方法進行封裝
由于SharedWorker是用postMessage發(fā)送消息五续,用onmessage接受消息,在使用是不如調(diào)用一個方法那么直接方便龄恋「砑荩可以考慮將其封裝成返回Promise方法。
import sharedWorker from './worker?sharedworker';
import { useUserStore } from '/@/store/modules/user';
export function useWorker() {
const userStore = useUserStore();
const worker = new sharedWorker();
const onMessageResolveMap = {};
const onMessageRejectMap = {};
worker.port.onmessage = ({ data }) => {
if (typeof onMessageResolveMap[data.funcName] === 'function') {
if (data.data.statusCode === 200) {
onMessageResolveMap[data.funcName](data.data?.data);
} else {
onMessageRejectMap[data.funcName](data.data.descript);
}
}
};
function getWorkerData(funcName, params, cacheTime = 2 * 60 * 1000) {
return new Promise((resolve, reject) => {
onMessageResolveMap[funcName] = resolve;
onMessageRejectMap[funcName] = reject;
worker.port.postMessage({ token: userStore.getToken, funcName, params, cacheTime });
});
}
function stopWorker() {
if (worker?.port) {
worker?.port?.close();
}
}
return {
getWorkerData,
stopWorker,
};
}
如上getWorkerData方法郭毕,當方法調(diào)用時它碎,返回一個promise對象,但promise對象不會馬上變?yōu)閞esolve狀態(tài)显押,而是將promise的resolve和reject 先保存起來扳肛,在onmessage中在通過方法名返回響應(yīng)的數(shù)據(jù)。當然煮落,這個方法有一個潛在的問題敞峭,如果只用方法名做區(qū)分,當調(diào)用一個方法還沒返回數(shù)據(jù)接著又調(diào)一次蝉仇,resolve就會被覆蓋掉,它只能等返回數(shù)據(jù)再進行下一次調(diào)用殖蚕。解決的辦法也很簡單轿衔,只要在傳遞的參數(shù)中增加一個隨機數(shù)作為識別即可。
下面是在worker中的處理
import * as api from './api/api';
import { handleApi } from './api/utils';
self.window = self;
window = self;
const cacheApi = handleApi(api);
self.onconnect = ({ ports }) => {
ports.forEach((port) => {
port.onmessage = (ev) => {
const { token, funcName, params, cacheTime } = ev.data;
cacheApi[funcName](token, params, cacheTime).then((res) => {
if (res?.data) {
port.postMessage({ funcName, data: res.data });
}
});
};
port.onmessageerror = (ev) => {
console.log('error', ev);
};
});
};
cacheApi是一個包含需要調(diào)用的所有接口方法的對象睦疫,通過主線程傳來的函數(shù)名funcName調(diào)用相關(guān)接口害驹。獲得數(shù)據(jù)后通過postMessage將數(shù)據(jù)返回給主線程。
如果想對接口數(shù)據(jù)根據(jù)cacheTime設(shè)置的時間進行緩存蛤育,需要怎么處理呢宛官?這里我用到了閉包的結(jié)構(gòu)。api這變量包含了api文件中定義的所有接口瓦糕。通過handleApi這個函數(shù)進行處理底洗,返回一系列內(nèi)部包含了緩存變量的閉包方法。
export function handleApi(apis) {
const apiMap = {};
for (const name in apis) {
const originFunc = apis[name];
const func = (() => {
const pendings: any = {};
// 根據(jù)不同的條件保存結(jié)果
const results: any = {};
const timeStamps: any = {};
function waitResult(resolve, resultKey) {
if (!results[resultKey]) {
setTimeout(() => {
waitResult(resolve, resultKey);
}, 300);
} else {
resolve(results[resultKey]);
}
}
function cacheValid(ret, resultKey, cacheTime) {
const _timeStamp = new Date().getTime();
// 檢查是否過了緩存時間
if (_timeStamp - timeStamps[resultKey] > cacheTime || !ret) {
return false;
} else {
return true;
}
}
return (token, params, cacheTime) => {
const resultKey = JSON.stringify(params);
// 正在請求數(shù)據(jù)咕娄,等待
if (pendings[resultKey]) {
return new Promise((resolve) => {
waitResult(resolve, resultKey);
});
// 緩存是否還生效
} else if (cacheValid(results[resultKey], resultKey, cacheTime)) {
return new Promise((resolve) => {
resolve(results[resultKey]);
});
} else {
// 開始一個新的請求
pendings[resultKey] = true;
return new Promise((resolve) => {
originFunc(params, {
headers: {
Authorization: token,
},
}).then((res) => {
if (res?.data?.statusCode === 200) {
timeStamps[resultKey] = new Date().getTime();
results[resultKey] = res;
}
pendings[resultKey] = false;
resolve(results[resultKey]);
});
});
}
};
})();
apiMap[name] = func;
}
return apiMap;
}
這里面有幾個關(guān)鍵點:
如果有兩個頁面在同時請求一個接口亥揖,并且條件都一樣,后請求的不應(yīng)當再請求一次,而應(yīng)當?shù)却彺鏀?shù)據(jù)的到來费变。
因此摧扇,當?shù)谝粋€頁面在開始請求接口之前,就設(shè)置pending 為true, 這樣第二個頁面進入方法后挚歧,得知緩存中無數(shù)據(jù)并且接口在請求扛稽,會不斷調(diào)用自身得waitResult方法進行等待。
當接口返回數(shù)據(jù)后滑负,pending為false, 等待結(jié)束在张,返回緩存的數(shù)據(jù)。
過期時間的處理:cacheTime通過方法參數(shù)傳入橙困,因此不同的方法在調(diào)用時可以設(shè)置不同的過期時間瞧掺。當接口返回數(shù)據(jù)后,將當前時間戳記錄在timeStamps中凡傅,下次調(diào)同一個接口辟狈,則通過cacheValid方法檢查是否超過這個時間。為了保證數(shù)據(jù)優(yōu)先夏跷,如果上一次沒緩存到正確的數(shù)據(jù)哼转,也應(yīng)當作為過期情況重新調(diào)用。