【JS 】SharedWorker 优化前端轮询请求(续)
1. 书接上回
经过一顿改造,性能是上去了,但是代码却还是不够简洁,所以继续封装
2. 思路
目标:使用一个js
文件完成所有轮询请求,封装调用方法,简化代码
-
一个
js
文件判断是web
环境还是work
环境 -
web
环境返回封装的函数:判断兼容性,创建worker
,传入参数,订阅广播 -
work
环境访问指定的webapi
并发送广播
3. 代码
3.1. 一个js
文件判断是web
环境还是work
环境
判断当前环境的this
是window
还是SharedWorkerGlobalScope
(function(scope){
// 这里简化一下 只要不是 window 就认为是 work
const isWorker = !(scope.constructor && scope.constructor.name === "Window");
// ...
})(this)
3.2. web
环境返回封装的函数:判断兼容性,创建worker
,传入参数,订阅广播
3.2.1. 获取当前js文件路径
由于在创建 new SharedWorker(aURL, name) 时需要传入js文件路径,所以要获取当前js的路径备用
<script src="~/Scripts/SharedIntervalRequest.js"></script>
// 加载js文件时执行这段代码,可以获取当前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
/**
* 发送请求
* @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())
}
/**
* 设置轮询请求
* @param {String} command 如:"POST http://domain.com/api"
* @param {Function} callback 请求完成后的回调函数
* @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);
}
// 开启共享任务 ...
}
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();
// 发送消息传递 timeout 参数
worker.port.postMessage({ type: "timeout", timeout });
worker.port.onmessage = msg => callback(msg.data);
// 发送消息执行 close 指令
const close = () => worker.port.postMessage({ type: "close" }) || worker.port.close();
window.addEventListener("beforeunload", close);
return close;
3.2.4. 任务可取消
如果要实现这个效果,就必须要重写
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环境代码
(function (scope) {
const isWorker = !(scope.constructor && scope.constructor.name === "Window");
/**
* 发送请求
* @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文件路径失败");
}
/**
* 设置轮询请求
* @param {String} command 如:"POST http://domain.com/api"
* @param {Function} callback 请求完成后的回调函数
* @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);
}
// 开启共享任务
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
环境访问指定的webapi
并发送广播
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. 关闭work
SharedWorker 无法由发起方主动关闭,所以需要自己实现管理连接的方法
回顾代码:
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. 处理广播
由于已经自己管理了连接端口,所以就可以直接点对点发到所有端口,不再使用广播对象 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();
// 重设轮询
function resetInterval(timeout) {
clearInterval(timer);
// 获取 minimumTimeout 与 timeout 中大于1000的较小的一个
minimumTimeout = [minimumTimeout, timeout].filter(x => x >= 1000).sort()[0];
// 如果 connectionPorts 中已经没有连接端口了,则不再执行 request 函数
timer = setInterval(() => connectionPorts.size && request(command).then(broadcast), minimumTimeout || 60000);
}
3.3.6. 完整work环境代码
(function (scope) {
const isWorker = !(scope.constructor && scope.constructor.name === "Window");
/**
* 发送请求
* @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 环境代码 ...
})(this);
4. 完整work.js代码
5. 页面代码变化
封装之后,页面代码较原始版本几乎没有增长
而且可以快速复用于其他API接口的轮询操作
6. Demo
我发布的代码,没有任何版权,遵守WTFPL协议(如有引用,请遵守被引用代码的协议)
qq群:5946699 希望各位喜爱C#的朋友可以在这里交流学习,分享编程的心得和快乐