【JS 】SharedWorker 优化前端轮询请求(续)

1. 书接上回

【JS 】SharedWorker 优化前端轮询请求

经过一顿改造,性能是上去了,但是代码却还是不够简洁,所以继续封装

2. 思路

目标:使用一个js文件完成所有轮询请求,封装调用方法,简化代码

  1. 一个js文件判断是web环境还是work环境

  2. web环境返回封装的函数:判断兼容性,创建worker,传入参数,订阅广播

  3. work环境访问指定的webapi并发送广播

3. 代码

3.1. 一个js文件判断是web环境还是work环境

判断当前环境的thiswindow还是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. 封装方法 setPostIntervalsetGetInterval

/**
 * 发送请求
 * @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代码

work.js

5. 页面代码变化

封装之后,页面代码较原始版本几乎没有增长
而且可以快速复用于其他API接口的轮询操作

6. Demo

SharedWorker 封装演示 - JSRUN

posted @ 2023-02-19 00:55  冰麟轻武  阅读(265)  评论(0编辑  收藏  举报