SharedWorker實現多標簽頁聯動計時器


web workers對於每個前端開發者並不陌生,在mdn中的定義:Web Worker為Web內容在后台線程中運行腳本提供了一種簡單的方法。線程可以執行任務而不干擾用戶界面。此外,他們可以使用XMLHttpRequest執行 I/O  (盡管responseXMLchannel屬性總是為空)。一旦創建, 一個worker 可以將消息發送到創建它的JavaScript代碼, 通過將消息發布到該代碼指定的事件處理程序(反之亦然)。

我的理解:web workers可以為js帶來多線程環境,由js主線程創建並獨立於js主線程處理一些任務,同時也不會阻塞js主線程代碼的執行。

在我們日常開發中,主要使用這三類worker:DedicatedWorker,ServiceWorker,SharedWorker。其中,DedicatedWorker主要是在瀏覽器中單開一個私有線程,可以緩解js單線程中對一些復雜業務邏輯的處理壓力。ServiceWorker主要實現頁面資源的緩存,也是PWA應用中的重要組成部分,提升用戶離線體驗。SharedWorker常用於跨標簽頁通訊(必須是同源的瀏覽器上下文),共享信息,本文將使用SharedWorker實現多標簽頁聯動的計時器的demo,完整代碼也將全部貼出。

demo效果:

由於web worker有同源限制,需要啟用本地服務。

1.npm init -y

2.npm i vite -S

package.json:

{
  "name": "sharedworker_timer",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "vite"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "vite": "^2.7.1"
  }
}
npm run dev

 

項目目錄結構:

 

頁面視圖index.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>timer by sharedWorker</title>
</head>

<body>
    <div class=timer>
        <h1 class="timerNum">00:00:00</h1>
        <div class="timerButtonGroup">
            <button class="startBtn">開始</button>
            <button class="pauseBtn">暫停</button>
            <button class="resetBtn">重置</button>
        </div>
    </div>
    <script type="module" src="./src/app.js"></script>
</body>

</html>

 

app.js:

import './CreateWorker'; // 引入sharedworker,注冊message事件

; (function (doc) {

    let worker = new SharedWorker('./src/worker.js').port; // 使用sharedworker發送message事件

    const oStartBtn = doc.querySelector('.startBtn');
    const oPauseBtn = doc.querySelector('.pauseBtn');
    const oResetBtn = doc.querySelector('.resetBtn');


    const init = () => { // 初始化綁定事件
        bindEvent();
    }

    function bindEvent() {
        oStartBtn.addEventListener('click', handdleStartBtnClick, false); // 開始按鈕點擊事件
        oPauseBtn.addEventListener('click', handdlePauseBtnClick, false); // 暫停按鈕點擊事件
        oResetBtn.addEventListener('click', handdleResetBtnClick, false); // 重置按鈕點擊事件
    }

    function handdleStartBtnClick() {
        worker.postMessage({ // 給worker發送消息
            data: {
                event: 'start',
            }
        })
    }

    function handdlePauseBtnClick() {
        worker.postMessage({
            data: {
                event: 'pause',
            }
        })
    }

    function handdleResetBtnClick() {
        worker.postMessage({
            data: {
                event: 'reset',
            }
        })
    }

    init();

})(document)

 

worker.js:

let ports = new Map(),
    connectList = [],
    textlist = [],
    nolist = [];
self.addEventListener('connect', function (e) {
    var port = e.ports[0]
    port.start();
    port.addEventListener('message', function (e) {
        var worker = e.currentTarget,
            res = e.data;
        if (connectList.indexOf(worker) === -1) { // 收集所有連接的標簽頁
            connectList.push(worker)
        }

        switch (res.status) {
            case 0: // 標簽頁第一次連接
                inform(function (item) {
                    if (item != worker) { // 如果當前連接的標簽頁不是自己,發送該信息
                        item.postMessage('有新頁面加入 歷史標簽頁總數' + connectList.length);
                    } else { // 當前標簽頁是自己,打印自己標簽號碼
                        item.postMessage('我是新頁面編號' + connectList.length);
                    }
                });
                break;
            case 1:
                nolist.push(res.no);
                inform(function (item) {
                    item == worker && item.postMessage('自己的號碼牌為' + res.no + JSON.stringify(nolist));
                });
                break;
            case 'heartbeat': // 心跳
                ports.set(res, +new Date())
                break;
            default:
                textlist.push(res.data);
                inform(textlist);
                break;
        }
    })
    setInterval(() => {
        let now = +new Date()
        for (var [key, value] of ports) {
            console.log(now - value + " = " + value);
            if (now - value > 3100) {
                // 3秒以上沒發心跳,標簽頁已關閉,關閉相應的worker線程
                ports.delete(key)

            }
        }
    }, 1000)
});

// 分發消息
function inform(obj) {
    var cb = (typeof obj === 'function') ? obj : function (item) {
        item.postMessage(obj);
    }
    connectList.forEach(cb);
}

 

CreateWorker.js

import TimerEventList from './timer'; // 引入計時器類

; (function (doc) {
    const timerDom = doc.querySelector('.timerNum');
    const timerEventList = TimerEventList.create(timerDom); // 創建計時器

    let no;
    let worker = new SharedWorker('./src/worker.js').port;
    worker.start()
    worker.addEventListener('message', (res) => {
        // console.log('來自worker的數據:', res.data)
        if (res.data && res.data.includes('我是新頁面編號')) {
            no = res.data.replace('我是新頁面編號', '')
            worker.postMessage({
                status: 1,
                no,
            });
            return
        }
        let e = res.data[res.data.length - 1];

        if (e.event == "start") { // 如果worker中event為start
            timerEventList.notify('start') // 發布start事件
        }
        if (e.event == "pause") {
            timerEventList.notify('pause')
        }
        if (e.event == "reset") {
            timerEventList.notify('reset')
        }
    }, false)
    worker.postMessage({ // 第一次加入worker
        status: 0,
    });
    function heartbeat() { 
        worker.postMessage({
            'status': 'heartbeat',
            'data': no
        })
    }
    heartbeat()
    setInterval(() => { // 心跳檢測
        heartbeat()
    }, 3000);
})(document)

 

下面是計時器模塊(./src/timer):

index.js

import ChangeDom from "./ChangeDom"; // 引入改變dom類
import ClickEvent from "./ClickEvent"; // 引入點擊事件類

const EVENT_TYPE = { // 事件類型
    start: 'start',
    pause: 'pause',
    reset: 'reset'
}

class TimerEventList {

    static instance ;
    TimerDom;
    clickEvent;
    changeDom;
    startHandlers = [];
    pauseHandlers = [];
    resetHandlers = [];

    constructor(TimerDom) {
        this.TimerDom = TimerDom;
        this.initTimer();
    }
  // 單例模式
    static create(timerDom) {
        if (!TimerEventList.instance) {
            TimerEventList.instance = new TimerEventList(timerDom);
        }
        return TimerEventList.instance;
    }

    initTimer() {
        this.clickEvent = ClickEvent.create();
        this.changeDom = ChangeDom.create(this.TimerDom);

        for (let k in EVENT_TYPE) {
            this.initHandlers(EVENT_TYPE[k]);
        }

    }

    initHandlers(type) { // 處理對應點擊事件,並將點擊的事件和對應操作dom推入對應的handlers數組 switch (type) {
            case EVENT_TYPE.start:
                this.startHandlers.push(this.clickEvent.startClick.bind(this.clickEvent));
                this.startHandlers.push(this.changeDom.startTimer.bind(this.changeDom));
                break;
            case EVENT_TYPE.pause:
                this.pauseHandlers.push(this.clickEvent.pauseClick.bind(this.clickEvent));
                this.pauseHandlers.push(this.changeDom.pauseTimer.bind(this.changeDom));
                break;
            case EVENT_TYPE.reset:
                this.resetHandlers.push(this.clickEvent.resetClick.bind(this.clickEvent));
                this.resetHandlers.push(this.changeDom.resetTimer.bind(this.changeDom));
                break;
            default:
                break;
        }
    }
  // 觀察者模式
    notify(type) { // 訂閱計時器相關點擊和dom操作事件
        let i=0,
            handlers = [],
            res;
        switch (type) {
            case EVENT_TYPE.start:
                handlers = this.startHandlers;
                break;
            case EVENT_TYPE.pause:
                handlers = this.pauseHandlers;
                break;
            case EVENT_TYPE.reset:
                handlers = this.resetHandlers;
                break;
            default:
                break;
        }
        res = handlers[i]();

        while (i < handlers.length - 1) {
            i++;
            res = res.then(param => {
                return handlers[i](param);
            })
        }
    }
}

export default TimerEventList;

 

ClickEvent.js

class ClickEvent {

    static instance;
  // 單例
    static create() {
        if (!ClickEvent.instance) {
            ClickEvent.instance = new ClickEvent();
        }
        return ClickEvent.instance;
    }

    startClick() { // 返回Promise,方便做一些異步操作,本demo中沒有涉及相關異步操作,這里留作擴展 return new Promise((resolve, reject) => {
            resolve('start')
        })
    }

    pauseClick() {
        return new Promise((resolve, reject) => {
            resolve('pause')
        })
    }

    resetClick() {
        return new Promise((resolve, reject) => {
            resolve('reset')
        })
    }
}

export default ClickEvent;

 

ChangeDom.js

import Time from './Time';

class ChangeEvent {

    static instance;
    time;
    timerDom;

    constructor(timerDom) { // 構造器接收dom對象,這里dom對象為頁面時間的顯示節點 this.timerDom = timerDom;
        this.time = Time.create(this.timerView.bind(this)) // 將操作dom的方法傳入Time類中,作為回調使用,這里bind改變this指向
    }
  // 單例
    static create(timerDom) {
        if (!ChangeEvent.instance) {
            ChangeEvent.instance = new ChangeEvent(timerDom);
        }
        return ChangeEvent.instance;
    }

    startTimer() {
        console.log('開始計時!');
        this.time.timeStart();
    }

    pauseTimer() {
        console.log('暫停計時!');
        this.time.timePause();
    }

    resetTimer() {
        console.log('結束計時!');
        this.time.timeReset();
        this.timerDom.innerText = '00:00:00';
    }

    timerView(v) {
        console.log('=====>', v)
        this.timerDom.innerText = v;
    }
}

export default ChangeEvent;

 

Time.js

class Time {

    static instance;
    hour = 0;
    minute = 0;
    second = 0;
    str = '00:00:00';
    timer = null;
    cb = null;
    
    constructor (cb) {
        this.cb = cb;
    }
  // 單例
    static create(cb) { // 接收回調函數,用於后面更新dom if (!Time.instance) {
            Time.instance = new Time(cb);
        }
        return Time.instance;
    }

    timeStart() {
        clearInterval(this.timer) // 開啟新的計時器之前,先清除一遍計時器,避免雙重計時 this.timer = setInterval(this.timeCallback.bind(this), 1000)
    }

    timePause() {
        this.timer&&clearInterval(this.timer);
    }

    timeReset() {
        this.timer&&clearInterval(this.timer);
        this.hour = 0;
        this.minute = 0;
        this.second = 0;
        this.str = '00:00:00';
    }

    timeCallback() { // 計時器主要方法,用於改變小時、分鍾、秒,返回新的時間字符串,以及更新dom this.second = this.second + 1
        if (this.second >= 60) {
            this.second = 0
            this.minute = this.minute + 1
        }
        if (this.minute >= 60) {
            this.minute = 0
            this.hour = this.hour + 1
        }
        this.str = this.toDub(this.hour) + ':' + this.toDub(this.minute) + ':' + this.toDub(this.second);
        this.cb(this.str);
    }

    toDub(n) { // 補0 if (n < 10 ) {
            return '0' + n
        }else {
            return '' + n
        }
    }
}

export default Time;

 

以上就是使用SharedWorker實現計時器的完整過程和代碼,基本實現了多標簽頁之間計時器同步開始計時、暫停、重置等功能,掌握本demo,相信你也可以用SharedWorker解決絕大多數跨標簽頁通信的場景!如果有什么問題,歡迎留言討論~

注:本文用到的SharedWorker相關api,不熟悉的話,可以查看相關文檔,這里不多贅述。

 

腳踏實地行,海闊天空飛~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM