目录
- 初始化项目
- 编写入口文件和 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
<!DOCTYPE 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$/)
}
}
这里是我的配置