Web Worker線程解決方案electron踩坑記錄
初始化項目
electron 開發(fā)時會遇到一對多的情況,在進行 websocket 通信時,如果接收到服務端多個指令時,而這個指令剛好需要占用線程,這個時候整個界面就會失去響應,那么我們就可以使用線程來解決這個問題.
npm create vite@latest electron-worker
執(zhí)行完后修改 package.json 如下:
{
"name": "electron-worker",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {},
"devDependencies": {
"@vitejs/plugin-vue": "^3.2.0",
"vite": "^3.2.0",
"vue": "^3.2.41",
"electron": "19.1.4",
"electron-builder": "^23.3.3"
}
}
編寫入口文件和 electron 插件
創(chuàng)建 mainEntry.js 作為 electron 的入口文件,啟動一個窗口
// src/main/mainEntry.js
import { app, BrowserWindow } from "electron";
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
let mainWindow;
app.whenReady().then(() => {
let config = {
webPreferences: {
nodeIntegration: true,
webSecurity: false,
allowRunningInsecureContent: true,
contextIsolation: false,
webviewTag: true,
spellcheck: false,
disableHtmlFullscreenWindowResize: true,
},
};
mainWindow = new BrowserWindow(config);
mainWindow.webContents.openDevTools({ mode: "undocked" });
mainWindow.loadURL(process.argv[2]);
});
編寫 vite 插件,在服務器啟動后加載 electron 入口文件
// plugins/devPlugin.js
export const devPlugin = () => {
return {
name: "dev-plugin",
configureServer(server) {
require("esbuild").buildSync({
entryPoints: ["./src/main/mainEntry.js"],
bundle: true,
platform: "node",
outfile: "./dist/mainEntry.js",
external: ["electron"],
});
server.httpServer.once("listening", () => {
let { spawn } = require("child_process");
let electronProcess = spawn(require("electron").toString(), ["./dist/mainEntry.js", `http://127.0.0.1:${server.config.server.port}/`], {
cwd: process.cwd(),
stdio: "inherit",
});
electronProcess.on("close", () => {
server.close();
process.exit();
});
});
},
};
};
使用插件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { devPlugin } from "./plugins/devPlugin";
export default defineConfig({
plugins: [devPlugin(), vue()],
})
將 vue 項目文件放入和 main 同級, 結(jié)構(gòu)如下所示
└─src
├─main
│ mainEntry.js
└─renderer
│ App.vue
│ main.js
├─assets
└─components
修改 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" rel="external nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/renderer/main.js"></script>
</body>
</html>
現(xiàn)在執(zhí)行 npm run dev 就可以運行項目了
websocket
websocket 服務
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({port: 8181});
wss.on('connection', function (ws) {
console.log('有客戶端連接');
ws.send("連接成功")
ws.on('message', function (jsonStr) {
console.log(jsonStr.toString());
});
});
連接 websocket 服務
準備 Socket 對象
export default class Socket {
websocket
wsUrl
constructor(wsUrl) {
this.wsUrl = wsUrl
}
init() {
if (this.websocket) return this.websocket
const socket = this.websocket = new WebSocket(this.wsUrl)
// WebSocket 接收服務端數(shù)據(jù)
socket.onmessage = (e) => {
console.log("接收服務端消息:", e.data)
}
// WebSocket 斷開連接后觸發(fā)
socket.onclose = (e) => {}
// WebSocket 連接成功
socket.onopen = () => {
console.log("連接成功")
}
// WebSocket 連接異常
socket.onerror = (e) => {}
}
}
連接 Socket
<script setup>
import Socket from './socket'
const socket = new Socket("ws://localhost:8181")
function register() {
socket.init()
}
</script>
<template>
<div>
<button @click="register">注冊</button>
</div>
</template>
<style scoped>
</style>
點擊注冊后顯示如下:

發(fā)送心跳
一般為了確保服務一直連接,需要客戶端定時給服務發(fā)送心跳
export default class Socket {
// ...
heartbeatCount // 心跳次數(shù)
heartbeatTimer // 心跳定時器
heartbeatInterval = 1000 * 20 // 心跳發(fā)送頻率(2秒一次)
// ...
sendHeartBeat() {
this.heartbeatCount = 0
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)
this.heartbeatTimer = setInterval(() => {
this.websocket.send("發(fā)送心跳")
}, this.heartbeatInterval)
}
}
App.vue
function sendHeartBeat() {
socket.sendHeartBeat()
}
<button @click="sendHeartBeat">發(fā)送心跳</button>

可以看到我們在服務端日志里看到有持續(xù)心跳日志
取消心跳
因為是定時器發(fā)送,當服務端掉線后定時器卻還在繼續(xù)發(fā)送,現(xiàn)在我們來優(yōu)化這個
// 斷開連接
onclose() {
console.log("已斷開連接")
this.websocket = null
// 清除心跳定時器
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)
}
在 socket 斷開后進行調(diào)用
// WebSocket 斷開連接后觸發(fā)
socket.onclose = (e) => {
this.onclose()
}
重新連接
websocket 斷開有可能是客戶端網(wǎng)絡問題,所以我們需要進行嘗試重連
export default class Socket {
// ...
socketOpen // 是否連接
isReconnect = true // 是否可以重新連接
reconnectCountMax = 3 // 最大重新次數(shù)
reconnectTimer // 重連定時器
reconnectCurrent = 0 // 重連次數(shù)
reconnectInterval // 1000 * 3 // 重連頻率(3秒一次)
// ...
// 斷開連接
onclose() {
console.log("已斷開連接")
this.websocket = null
// 清除心跳定時器
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)
// 需要重新連接
if (this.isReconnect) {
this.reconnectTimer = setTimeout(() => {
if (this.reconnectCurrent >= this.reconnectCountMax) {
console.log("超過重連次數(shù),重連失敗")
clearTimeout(this.reconnectTimer)
} else {
this.reconnectCurrent += 1
this.reconnect()
}
}, this.reconnectInterval)
}
}
// 重新連接
reconnect() {
console.log("重新連接", this.reconnectCurrent)
if (this.websocket && this.socketOpen) {
this.websocket.close()
}
this.init()
}
}
我們每三秒一次進行嘗試重新連接,如果重連三次還未連接,那我們認為無法重新連接
其它優(yōu)化
export enum PostMessageType {
ON_OPEN = 'open', // websocket開啟
ON_ERROR = 'error', // websocket異常
ON_CLOSE = 'close', // websocket關閉
ON_MESSAGE = 'message', // websocket接收消息
RECONNECT = 'reconnect', // websocket重新連接
HEARTBEAT = 'heartbeat', // websocket發(fā)送心跳
OFF = 'off', // websocket主動關閉
REGISTER = 'register', // websocket注冊成功
}
class Socket {
wsUrl: string // 服務地址
websocket: WebSocket | null = null // websocket對象
socketOpen: boolean = false // socket是否開啟
heartbeatTimer: any // 心跳定時器
heartbeatCount: number = 0 // 心跳次數(shù)
heartbeatInterval: number = 1000 * 20 // 心跳發(fā)送頻率(2秒一次)
isReconnect: boolean = true // 是否可以重新連接
reconnectCountMax: number = 3 // 最大重新次數(shù)
reconnectCurrent: number = 0 // 已發(fā)起重連次數(shù)
reconnectTimer: any // 重連timer
reconnectInterval: number = 1000 * 3 // 重連頻率(3秒一次)
constructor(url: string) {
this.wsUrl = url
}
// socket 初始化
init() {
if (this.websocket) return this.websocket
const socket = this.websocket = new WebSocket(this.wsUrl)
// WebSocket 接收服務端數(shù)據(jù)
socket.onmessage = (e) => {
this.receive(e.data)
}
// WebSocket 斷開連接后觸發(fā)
socket.onclose = (e) => {
this.postMessage(PostMessageType.ON_CLOSE, e)
this.onclose()
}
// WebSocket 連接成功
socket.onopen = () => {
this.onopen()
}
// WebSocket 連接異常
socket.onerror = (e) => {
this.postMessage(PostMessageType.ON_ERROR, e)
}
}
// 連接成功后的回調(diào)
onopen() {
this.socketOpen = true
this.isReconnect = true
this.reconnectCurrent = 1
this.heartbeatCount = 0
this.postMessage(PostMessageType.ON_OPEN)
}
/**
* 消息處理器
* @param type
* @param data
*/
postMessage(type: PostMessageType, data?: any) {}
/**
* 斷開連接
*/
onclose() {
this.websocket = null
this.socketOpen = false
// 清除心跳定時器
clearInterval(this.heartbeatTimer)
// 需要重新連接
if (this.isReconnect) {
this.reconnectTimer = setTimeout(() => {
if (this.reconnectCurrent >= this.reconnectCountMax) {
clearTimeout(this.reconnectTimer)
} else {
this.reconnectCurrent += 1
this.reconnect()
}
}, this.reconnectInterval)
}
}
/**
* 重新連接
*/
reconnect() {
this.postMessage(PostMessageType.RECONNECT, this.reconnectCurrent)
if (this.websocket && this.socketOpen) {
this.websocket.close()
}
this.init()
}
/**
* 給服務端發(fā)送消息
* @param data
* @param callback
*/
send(data: any, callback?: () => void) {
const ws = this.websocket
if (!ws) {
this.init()
setTimeout(() => {
this.send(data, callback)
}, 1000)
return
}
switch (ws.readyState) {
case ws.OPEN:
ws.send(data)
if (callback) {
callback()
}
break
case ws.CONNECTING:
// 未開啟,則等待1s后重新調(diào)用
setTimeout(() => {
this.send(data, callback)
}, 1000)
break
default:
this.init()
setTimeout(() => {
this.send(data, callback)
}, 1000)
}
}
receive(data: any) {
this.postMessage(PostMessageType.ON_MESSAGE, data)
}
/**
* 發(fā)送心跳
* @param data 心跳數(shù)據(jù)
*/
sendHeartBeat(data: any) {
this.heartbeatCount = 0
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)
this.heartbeatTimer = setInterval(() => {
this.send(data, () => {
this.heartbeatCount += 1
this.postMessage(PostMessageType.HEARTBEAT, { heartBeatData: data, heartbeatCount: this.heartbeatCount })
})
}, this.heartbeatInterval)
}
/**
* 主動關閉websocket連接
* 關閉后 websocket 關閉監(jiān)聽可以監(jiān)聽到,所以無需去額外處理
*/
close() {
this.isReconnect = false
this.postMessage(PostMessageType.OFF, "主動斷開websocket連接")
this.websocket && this.websocket.close()
}
}
上面是基礎的 websocket ,具體使用需要結(jié)合業(yè)務進行繼承使用
export default class SelfSocket extends Socket {
registerData: any // 注冊數(shù)據(jù)
heartBeatData: any // 心跳數(shù)據(jù)
constructor(url: string) {
super(url);
}
initSocket(registerData: any, heartBeatData: any) {
this.registerData = registerData
this.heartBeatData = heartBeatData
super.init()
}
onopen() {
this.register()
super.onopen();
}
/**
* websocket 注冊消息,注冊成功后進行心跳發(fā)送
*/
register() {
this.send(this.registerData, () => {
this.sendHeartBeat(this.heartBeatData)
this.postMessage(PostMessageType.REGISTER, this.registerData)
})
}
send(data: any, callback?: () => void) {
// 數(shù)據(jù)加密
const str = _encrypt(data)
super.send(str, callback);
}
receive(data: any) {
this.postMessage(PostMessageType.ON_MESSAGE, _decode(data))
}
postMessage(type: PostMessageType, e?: any) {}
}
我們公司 websocket 連接需要注冊后進行心跳發(fā)送,且在接收和發(fā)送數(shù)據(jù)時都進行了加密和解密,簡單的可以使用 base64 進行
Worker
Web Worker 使用可以參考阮一峰老師的文章,這里就不做過多介紹
創(chuàng)建一個 websocketWorker.js
const URL = "ws://localhost:8181"
import Socket from "./socket";
const ws = new Socket(URL)
self.addEventListener('message', (e) => {
const { type, data } = e.data
switch (type) {
case "init":
ws.init();
break
case "message":
ws.send(data)
break
case "close":
ws.close()
break
default:
console.error("發(fā)送websocket命令有誤")
break
}
})
<script setup>
import Worker from './websocketWorker?worker'
const worker = new Worker()
worker.onmessage = function (e) {
console.log(e.data)
}
function register() {
worker.postMessage({
type: 'init'
})
}
function close() {
worker.postMessage({
type: 'close'
})
}
</script>
<template>
<div>
<button @click="register">注冊</button>
<button @click="close">關閉服務</button>
</div>
</template>
vite 使用 worker 可以查看 worker選項
如果是 webpack 可以查看 worker-loader
module.exports = {
chainWebpack: config => {
config.module
.rule('worker')
.test(/.worker.js$/)
.use('worker-loader')
.loader('worker-loader')
.options({
inline: 'no-fallback',
})
.end()
config.module.rule('js').exclude.add(/.worker.js$/)
}
}
這里是我的配置
以上就是Web Worker線程解決方案electron踩坑記錄的詳細內(nèi)容,更多關于Web Worker線程electron的資料請關注腳本之家其它相關文章!
相關文章
微信小程序 中wx.chooseAddress(OBJECT)實例詳解
這篇文章主要介紹了微信小程序 中wx.chooseAddress(OBJECT)實例詳解的相關資料,需要的朋友可以參考下2017-03-03
JS實現(xiàn)將圖片URL轉(zhuǎn)base64示例詳解
這篇文章主要為大家介紹了JS實現(xiàn)將圖片URL轉(zhuǎn)base64示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
Intl對象DateTimeFormat?ListFormat?RelativeTimeFormat使用講解
這篇文章主要為大家介紹了Intl對象DateTimeFormat?ListFormat?RelativeTimeFormat使用講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06
ResizeObserver 監(jiān)視 DOM大小變化示例詳解
這篇文章主要為大家介紹了ResizeObserver 監(jiān)視 DOM大小變化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10

