目录
- 初始化项目
- 编写入口文件和 electron 插件
- websocket
- websocket 服务
- 连接 websocket 服务
- 发送心跳
- 取消心跳
- 重新连接
- 其它优化
- Worker
初始化项目
electron 开发时会遇到一对多的情况,在进行 websocket 通信时,如果接收到服务端多个指令时,而这个指令刚好需要占用线程,这个时候整个界面就会失去响应,那么我们就可以使用线程来解决这个问题.
npm create vite@latest electron-worker
执行完后修改 package.json 如下:
{ | |
"name": "electron-worker", | |
"private": true, | |
"version": ".0.0", | |
"scripts": { | |
"dev": "vite", | |
"build": "vite build", | |
"preview": "vite preview" | |
}, | |
"dependencies": {}, | |
"devDependencies": { | |
"@vitejs/plugin-vue": "^.2.0", | |
"vite": "^.2.0", | |
"vue": "^.2.41", | |
"electron": ".1.4", | |
"electron-builder": "^.3.3" | |
} | |
} |
编写入口文件和 electron 插件
创建 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[]); | |
}); |
编写 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://.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 同级, 结构如下所示
└─src | |
├─main | |
│ mainEntry.js | |
└─renderer | |
│ App.vue | |
│ main.js | |
├─assets | |
└─components |
修改 index.html
<html lang="en"> | |
<head> | |
<meta charset="UTF-" /> | |
<link rel="icon" type="image/svg+xml" href="/vite.svg" rel="external nofollow" /> | |
<meta name="viewport" content="width=device-width, initial-scale=.0" /> | |
<title>Vite + Vue</title> | |
</head> | |
<body> | |
<div id="app"></div> | |
<script type="module" src="/src/renderer/main.js"></script> | |
</body> | |
</html> |
现在执行 npm run dev 就可以运行项目了
websocket
websocket 服务
var WebSocketServer = require('ws').Server; | |
var wss = new WebSocketServer({port:}); | |
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 接收服务端数据 | |
socket.onmessage = (e) => { | |
console.log("接收服务端消息:", e.data) | |
} | |
// WebSocket 断开连接后触发 | |
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:") | |
function register() { | |
socket.init() | |
} | |
</script> | |
<template> | |
<div> | |
<button @click="register">注册</button> | |
</div> | |
</template> | |
<style scoped> | |
</style> |
点击注册后显示如下:
发送心跳
一般为了确保服务一直连接,需要客户端定时给服务发送心跳
export default class Socket { | |
// ... | |
heartbeatCount // 心跳次数 | |
heartbeatTimer // 心跳定时器 | |
heartbeatInterval = * 20 // 心跳发送频率(2秒一次) | |
// ... | |
sendHeartBeat() { | |
this.heartbeatCount = | |
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer) | |
this.heartbeatTimer = setInterval(() => { | |
this.websocket.send("发送心跳") | |
}, this.heartbeatInterval) | |
} | |
} |
App.vue
function sendHeartBeat() { | |
socket.sendHeartBeat() | |
} | |
<button @click="sendHeartBeat">发送心跳</button> |
可以看到我们在服务端日志里看到有持续心跳日志
取消心跳
因为是定时器发送,当服务端掉线后定时器却还在继续发送,现在我们来优化这个
// 断开连接 | |
onclose() { | |
console.log("已断开连接") | |
this.websocket = null | |
// 清除心跳定时器 | |
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer) | |
} |
在 socket 断开后进行调用
// WebSocket 断开连接后触发 | |
socket.onclose = (e) => { | |
this.onclose() | |
} |
重新连接
websocket 断开有可能是客户端网络问题,所以我们需要进行尝试重连
export default class Socket { | |
// ... | |
socketOpen // 是否连接 | |
isReconnect = true // 是否可以重新连接 | |
reconnectCountMax = // 最大重新次数 | |
reconnectTimer // 重连定时器 | |
reconnectCurrent = // 重连次数 | |
reconnectInterval // * 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("超过重连次数,重连失败") | |
clearTimeout(this.reconnectTimer) | |
} else { | |
this.reconnectCurrent += | |
this.reconnect() | |
} | |
}, this.reconnectInterval) | |
} | |
} | |
// 重新连接 | |
reconnect() { | |
console.log("重新连接", this.reconnectCurrent) | |
if (this.websocket && this.socketOpen) { | |
this.websocket.close() | |
} | |
this.init() | |
} | |
} |
我们每三秒一次进行尝试重新连接,如果重连三次还未连接,那我们认为无法重新连接
其它优化
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发送心跳 | |
OFF = 'off', // websocket主动关闭 | |
REGISTER = 'register', // websocket注册成功 | |
} | |
class Socket { | |
wsUrl: string // 服务地址 | |
websocket: WebSocket | null = null // websocket对象 | |
socketOpen: boolean = false // socket是否开启 | |
heartbeatTimer: any // 心跳定时器 | |
heartbeatCount: number = // 心跳次数 | |
heartbeatInterval: number = * 20 // 心跳发送频率(2秒一次) | |
isReconnect: boolean = true // 是否可以重新连接 | |
reconnectCountMax: number = // 最大重新次数 | |
reconnectCurrent: number = // 已发起重连次数 | |
reconnectTimer: any // 重连timer | |
reconnectInterval: number = * 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 接收服务端数据 | |
socket.onmessage = (e) => { | |
this.receive(e.data) | |
} | |
// WebSocket 断开连接后触发 | |
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) | |
} | |
} | |
// 连接成功后的回调 | |
onopen() { | |
this.socketOpen = true | |
this.isReconnect = true | |
this.reconnectCurrent = | |
this.heartbeatCount = | |
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 += | |
this.reconnect() | |
} | |
}, this.reconnectInterval) | |
} | |
} | |
/** | |
* 重新连接 | |
*/ | |
reconnect() { | |
this.postMessage(PostMessageType.RECONNECT, this.reconnectCurrent) | |
if (this.websocket && this.socketOpen) { | |
this.websocket.close() | |
} | |
this.init() | |
} | |
/** | |
* 给服务端发送消息 | |
* @param data | |
* @param callback | |
*/ | |
send(data: any, callback?: () => void) { | |
const ws = this.websocket | |
if (!ws) { | |
this.init() | |
setTimeout(() => { | |
this.send(data, callback) | |
},) | |
return | |
} | |
switch (ws.readyState) { | |
case ws.OPEN: | |
ws.send(data) | |
if (callback) { | |
callback() | |
} | |
break | |
case ws.CONNECTING: | |
// 未开启,则等待s后重新调用 | |
setTimeout(() => { | |
this.send(data, callback) | |
},) | |
break | |
default: | |
this.init() | |
setTimeout(() => { | |
this.send(data, callback) | |
},) | |
} | |
} | |
receive(data: any) { | |
this.postMessage(PostMessageType.ON_MESSAGE, data) | |
} | |
/** | |
* 发送心跳 | |
* @param data 心跳数据 | |
*/ | |
sendHeartBeat(data: any) { | |
this.heartbeatCount = | |
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer) | |
this.heartbeatTimer = setInterval(() => { | |
this.send(data, () => { | |
this.heartbeatCount += | |
this.postMessage(PostMessageType.HEARTBEAT, { heartBeatData: data, heartbeatCount: this.heartbeatCount }) | |
}) | |
}, this.heartbeatInterval) | |
} | |
/** | |
* 主动关闭websocket连接 | |
* 关闭后 websocket 关闭监听可以监听到,所以无需去额外处理 | |
*/ | |
close() { | |
this.isReconnect = false | |
this.postMessage(PostMessageType.OFF, "主动断开websocket连接") | |
this.websocket && this.websocket.close() | |
} | |
} |
上面是基础的 websocket ,具体使用需要结合业务进行继承使用
export default class SelfSocket extends Socket { | |
registerData: any // 注册数据 | |
heartBeatData: any // 心跳数据 | |
constructor(url: string) { | |
super(url); | |
} | |
initSocket(registerData: any, heartBeatData: any) { | |
this.registerData = registerData | |
this.heartBeatData = heartBeatData | |
super.init() | |
} | |
onopen() { | |
this.register() | |
super.onopen(); | |
} | |
/** | |
* websocket 注册消息,注册成功后进行心跳发送 | |
*/ | |
register() { | |
this.send(this.registerData, () => { | |
this.sendHeartBeat(this.heartBeatData) | |
this.postMessage(PostMessageType.REGISTER, this.registerData) | |
}) | |
} | |
send(data: any, callback?: () => void) { | |
// 数据加密 | |
const str = _encrypt(data) | |
super.send(str, callback); | |
} | |
receive(data: any) { | |
this.postMessage(PostMessageType.ON_MESSAGE, _decode(data)) | |
} | |
postMessage(type: PostMessageType, e?: any) {} | |
} |
我们公司 websocket 连接需要注册后进行心跳发送,且在接收和发送数据时都进行了加密和解密,简单的可以使用 base64 进行
Worker
Web Worker 使用可以参考阮一峰老师的文章,这里就不做过多介绍
创建一个 websocketWorker.js
const URL = "ws://localhost:" | |
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("发送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$/) | |
} | |
} |
这里是我的配置