1. 書接上回
【JS 】SharedWorker 優(yōu)化前端輪詢請求
經(jīng)過一頓改造,性能是上去了,但是代碼卻還是不夠簡潔,所以繼續(xù)封裝

2. 思路
目標(biāo):使用一個js文件完成所有輪詢請求,封裝調(diào)用方法,簡化代碼
一個
js文件判斷是web環(huán)境還是work環(huán)境web環(huán)境返回封裝的函數(shù):判斷兼容性,創(chuàng)建worker,傳入?yún)?shù),訂閱廣播work環(huán)境訪問指定的webapi并發(fā)送廣播
3. 代碼
3.1. 一個js文件判斷是web環(huán)境還是work環(huán)境
判斷當(dāng)前環(huán)境的this是window還是SharedWorkerGlobalScope
(function(scope){
// 這里簡化一下 只要不是 window 就認(rèn)為是 work
const isWorker = !(scope.constructor && scope.constructor.name === "Window");
// ...
})(this)
3.2. web環(huán)境返回封裝的函數(shù):判斷兼容性,創(chuàng)建worker,傳入?yún)?shù),訂閱廣播
3.2.1. 獲取當(dāng)前js文件路徑
由于在創(chuàng)建 new SharedWorker(aURL, name) 時需要傳入js文件路徑,所以要獲取當(dāng)前js的路徑備用
<script src="~/Scripts/SharedIntervalRequest.js"></script>
// 加載js文件時執(zhí)行這段代碼,可以獲取當(dāng)前js文件的完整路徑
const jsurl = (() => {
try {
return window.document.currentScript.src || null.toString();
} catch (e) {
return /(?:http|https|file):\/\/.*?\/.+?.js/.exec(e.stack || e.sourceURL || e.stacktrace)[0];
}
})();
// jsurl = 'http://localhost:12345/Scripts/SharedIntervalRequest.js'
3.2.2. 封裝方法 setPostInterval,setGetInterval
/**
* 發(fā)送請求
* @param {String} command 如:"POST http://domain.com/api"
*/
function request(command) {
const method = command.split(' ')[0];
const path = command.substring(method.length + 1);
return fetch(path, { method }).then(r => r.text())
}
/**
* 設(shè)置輪詢請求
* @param {String} command 如:"POST http://domain.com/api"
* @param {Function} callback 請求完成后的回調(diào)函數(shù)
* @param {Number|null} [timeout] 輪詢間隔時間
*/
function setHttpInterval(command, callback, timeout) {
if (typeof scope.SharedWorker === "undefined") {
// 不支持 SharedWorker 時使用 setInterval
request(command).then(callback);
return setInterval(() => request(command).then(callback), timeout);
}
// 開啟共享任務(wù) ...
}
window.setPostInterval = (apipath, callback, timeout) => setHttpInterval("POST " + apipath, callback, timeout);
window.setGetInterval = (apipath, callback, timeout) => setHttpInterval("GET " + apipath, callback, timeout);
3.2.3. Work傳參,close指令
const worker = new SharedWorker(jsurl, command); // 將請求command作為name傳給work
worker.port.start();
// 發(fā)送消息傳遞 timeout 參數(shù)
worker.port.postMessage({ type: "timeout", timeout });
worker.port.onmessage = msg => callback(msg.data);
// 發(fā)送消息執(zhí)行 close 指令
const close = () => worker.port.postMessage({ type: "close" }) || worker.port.close();
window.addEventListener("beforeunload", close);
return close;
3.2.4. 任務(wù)可取消
如果要實現(xiàn)這個效果,就必須要重寫
clearInterval
const timer = setPostInterval("/Admin/Dashboard/GetAppRelaseNotice", callback, 1000 * 60);
clearInterval(timer);
if (typeof window.__clearInterval__ === "undefined") {
window.__clearInterval__ = scope.clearInterval;
window.clearInterval = function (id) {
if (id instanceof Function) {
id();
} else {
scope.__clearInterval__.apply(window, arguments);
}
}
}
3.2.5. 完整web環(huán)境代碼
(function (scope) {
const isWorker = !(scope.constructor && scope.constructor.name === "Window");
/**
* 發(fā)送請求
* @param {String} command 如:"POST http://domain.com/api"
*/
function request(command) {
const method = command.split(' ')[0];
const path = command.substring(method.length + 1);
return fetch(path, { method }).then(r => r.text())
}
if (isWorker) {
// ... WORK 部分代碼 ...
return;
}
const jsurl = (() => {
try {
return scope.document.currentScript.src || null.toString();
} catch (e) {
return /(?:http|https|file):\/\/.*?\/.+?.js/.exec(e.stack || e.sourceURL || e.stacktrace)[0];
}
})();
if (!jsurl) {
throw Error("獲取js文件路徑失敗");
}
/**
* 設(shè)置輪詢請求
* @param {String} command 如:"POST http://domain.com/api"
* @param {Function} callback 請求完成后的回調(diào)函數(shù)
* @param {Number|null} [timeout] 輪詢間隔時間
*/
function setHttpInterval(command, callback, timeout) {
if (typeof scope.SharedWorker === "undefined") {
// 不支持 SharedWorker 時使用 setInterval
request(command).then(callback);
return setInterval(() => request(command).then(callback), timeout);
}
// 開啟共享任務(wù)
const worker = new SharedWorker(jsurl, command);
worker.port.start();
worker.port.postMessage({ type: "timeout", timeout });
worker.port.onmessage = msg => callback(msg.data);
const close = () => worker.port.postMessage({ type: "close" }) || worker.port.close();
scope.addEventListener("beforeunload", close);
return close;
}
if (typeof scope.__clearInterval__ === "undefined") {
scope.__clearInterval__ = scope.clearInterval;
scope.clearInterval = function (id) {
if (id instanceof Function) {
id();
} else {
scope.__clearInterval__.apply(scope, arguments);
}
}
}
scope.setPostInterval = (apipath, callback, timeout) => setHttpInterval("POST " + apipath, callback, timeout);
scope.setGetInterval = (apipath, callback, timeout) => setHttpInterval("GET " + apipath, callback, timeout);
})(this);
3.3. work環(huán)境訪問指定的webapi并發(fā)送廣播
3.3.1. 啟動work
回顧代碼:
const worker = new SharedWorker(jsurl, command); // 將請求command作為name傳給work
const command = this.name; // 將 name 還原為 command
let timer;
onconnect = function (e) {
request(command).then(x => broadcast(x);
clearInterval(timer);
timer = setInterval(() => request(command).then(x => broadcast(x)), 60000);
}
3.3.2. 關(guān)閉work
SharedWorker 無法由發(fā)起方主動關(guān)閉,所以需要自己實現(xiàn)管理連接的方法
回顧代碼:
const close = () => worker.port.postMessage({ type: "close" }) || worker.port.close();
window.addEventListener("beforeunload", close);
// 管理接連端口
const connectionPorts = new Set();
scope.onconnect = function (e) {
const port = e.ports[0];
connectionPorts.add(port); // 加入端口
port.onmessage = msg => {
if(msg.data.type === "close"){
connectionPorts.delete(port); // 處理 close 指令
}
}
};
3.3.3. 處理指令和傳參
回顧代碼:
worker.port.postMessage({ type: "timeout", timeout: 60*1000 });
worker.port.postMessage({ type: "close" })
// 定義指令處理程序
const handlers = {
timeout(port, value) { ... }
close(port) { connectionPorts.delete(port); }
};
// 訂閱消息處理程序
scope.onconnect = function (e) {
const port = e.ports[0];
port.onmessage = msg => {
const handler = handlers[msg.data && msg.data.type && msg.data.type.toLowerCase()];
if (handler) {
handler(port, msg.data.value, msg.data);
}
};
}
3.3.4. 處理廣播
由于已經(jīng)自己管理了連接端口,所以就可以直接點對點發(fā)到所有端口,不再使用廣播對象 BroadcastChannel
function broadcast(msg) {
connectionPorts.forEach(port => port.postMessage(msg))
}
回顧代碼:訂閱
worker.port.onmessage = msg => callback(msg.data);
3.3.5. 處理超時時間
let timer;
let minimumTimeout = null;
const connectionPorts = new Set();
// 重設(shè)輪詢
function resetInterval(timeout) {
clearInterval(timer);
// 獲取 minimumTimeout 與 timeout 中大于1000的較小的一個
minimumTimeout = [minimumTimeout, timeout].filter(x => x >= 1000).sort()[0];
// 如果 connectionPorts 中已經(jīng)沒有連接端口了,則不再執(zhí)行 request 函數(shù)
timer = setInterval(() => connectionPorts.size && request(command).then(broadcast), minimumTimeout || 60000);
}
3.3.6. 完整work環(huán)境代碼
(function (scope) {
const isWorker = !(scope.constructor && scope.constructor.name === "Window");
/**
* 發(fā)送請求
* @param {String} command 如:"POST http://domain.com/api"
*/
function request(command) {
const method = command.split(' ')[0];
const path = command.substring(method.length + 1);
return fetch(path, { method }).then(r => r.text())
}
if (isWorker) {
// 作為Work
const command = scope.name;
let timer;
let minimumTimeout = null;
const connectionPorts = new Set();
function resetInterval(timeout) {
clearInterval(timer);
minimumTimeout = [minimumTimeout, timeout].filter(x => x >= 1000).sort()[0];
timer = setInterval(() => connectionPorts.size && request(command).then(broadcast), minimumTimeout || 60000);
}
function broadcast(msg) {
connectionPorts.forEach(port => port.postMessage(msg))
}
const handlers = {
timeout(_, data) {
resetInterval(data.timeout);
},
close(port) {
connectionPorts.delete(port);
}
};
scope.onconnect = function (e) {
const port = e.ports[0];
connectionPorts.add(port);
request(command).then(broadcast);
resetInterval(minimumTimeout);
port.onmessage = msg => {
const handler = handlers[msg.data && msg.data.type && msg.data.type.toLowerCase()];
if (handler) {
handler(port, msg.data);
}
};
}
return;
}
// ... web 環(huán)境代碼 ...
})(this);
4. 完整work.js代碼
5. 頁面代碼變化
封裝之后,頁面代碼較原始版本幾乎沒有增長
而且可以快速復(fù)用于其他API接口的輪詢操作
