Web Worker线程解决方案electron踩坑记录

目录
  • 初始化项目
  • 编写入口文件和 electron 插件
  • websocket
    • websocket 服务
    • 连接 websocket 服务
    • 发送心跳
    • 取消心跳
    • 重新连接
    • 其它优化
  • Worker

初始化项目

electron 开发时会遇到一对多的情况,在进行 websocket 通信时,如果接收到服务端多个指令时,而这个指令刚好需要占用线程,这个时候整个界面就会失去响应,那么我们就可以使用线程来解决这个问题.

npm create vite@latest electron-worker

执行完后修改 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 插件

创建 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 同级, 结构如下所示

└─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>

现在执行 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 接收服务端数据
    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:8181")
function register() {
  socket.init()
}
</script>
<template>
  <div>
    <button @click="register">注册</button>
  </div>
</template>
<style scoped>
</style>

点击注册后显示如下:

发送心跳

一般为了确保服务一直连接,需要客户端定时给服务发送心跳

export default class Socket {
  // ...
  heartbeatCount // 心跳次数
  heartbeatTimer // 心跳定时器
  heartbeatInterval = 1000 * 20 // 心跳发送频率(2秒一次)
  // ...
  sendHeartBeat() {
    this.heartbeatCount = 0
    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 = 3 // 最大重新次数
  reconnectTimer // 重连定时器
  reconnectCurrent = 0  // 重连次数
  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("超过重连次数,重连失败")
          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()
  }
}

我们每三秒一次进行尝试重新连接,如果重连三次还未连接,那我们认为无法重新连接

其它优化

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 = 0 // 心跳次数
  heartbeatInterval: number = 1000 * 20 // 心跳发送频率(2秒一次)
  isReconnect: boolean = true // 是否可以重新连接
  reconnectCountMax: number = 3 // 最大重新次数
  reconnectCurrent: number = 0 // 已发起重连次数
  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 接收服务端数据
    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 = 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()
  }
  /**
   * 给服务端发送消息
   * @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后重新调用
        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)
  }
  /**
   * 发送心跳
   * @param data 心跳数据
   */
  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 关闭监听可以监听到,所以无需去额外处理
   */
  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: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("发送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踩坑记录的详细内容,更多关于Web Worker线程electron的资料请关注我们其它相关文章!

(0)

相关推荐

  • 如何使用JS中的webWorker

    目录 一.webWorker之初体验 二.webWorker之常用API 1.postMessage(data) 2.terminate() 3.message 4.error 三.worker上下文 四.关于worker 一.webWorker之初体验 所以,JavaScript是单线程也是有背景的. 如下: <!DOCTYPE html> <head> <title>singleThread</title> <meta http-equiv=&qu

  • vue与electron实现进程间的通信详情

    目录 一.配置内容 1.进程间的通信 第一种方式引入ipcRenderer 第二种方式引入ipcRenderer 2.渲染进程常用配置 3.将ipcMain封装到一个js中统一处理 三.总结 前言: 本文主要介绍electron渲染进程和主进程间的通信,以及在渲染进程和主进程中常用的配置项. 一.配置内容 1.进程间的通信 渲染进程和主进程间的通信主要通过ipcRenderer和ipcMain这两个模块实现的,其中ipcRenderer是在渲染进程中使用,ipcMain在主进程中使用. 其中,渲

  • Javascript Web Worker使用过程解析

    Web Worker 概述 JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事.前面的任务没做完,后面的任务只能等着.随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力. Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行.在主线程运行的同时,Worker 线程在后台运行,两者互不干扰.等到 Worker 线

  • JavaScript中的Web worker多线程API研究

    HTML5支持了Web Worker这样的API,允许网页在安全的情况下执行多线程代码.不过Web Worker实际上受到很多限制,因为它无法真正意义上共享内存数据,只能通过消息来做状态通知,所以甚至不能称之为真正意义上的"多线程". Web Worker的接口使用起来很不方便,它基本上自带一个sandbox,在沙箱中跑一个独立的js文件,通过 postMessage和 onMessge来和主线程通信: 复制代码 代码如下: var worker = new Worker("

  • WebWorker 封装 JavaScript 沙箱详情

    目录 1.场景 2.实现 IJavaScriptShadowbox 2.1 主线程的实现 2.2 web worker 线程的实现 3.使用 WebWorkerShadowbox/WebWorkerEventEmitter 4.限制 web worker 全局 api 5.web worker 沙箱的主要优势 1.场景 在前文  quickjs 封装 JavaScript 沙箱详情 已经基于 quickjs 实现了一个沙箱,这里再基于 web worker实现备用方案.如果你不知道 web wo

  • vue+electron实现创建多窗口及窗口间的通信(实施方案)

    目录 一.前言 二.实施方案 1.创建多窗口 2.多窗口间的通信 三.后记 一.前言 对于一个桌面应用来说,有时候单独一个窗口用户使用起来会不太方便,比方说写日报或者查看文件等,若是在同一窗口内,我只能做一件事,不能边预览文件,边去查看聊天消息内容等.又或者是多个应用间相互关联的需要同步查看的事件,这都是极其不方便的.因此我们可以将某些集成到electron软件中的应用或者某些界面用单独的窗口打开(以下称为独立窗口). 二.实施方案 1.创建多窗口 首先我们从electron官网中找到创建窗口的

  • Web Worker线程解决方案electron踩坑记录

    目录 初始化项目 编写入口文件和 electron 插件 websocket websocket 服务 连接 websocket 服务 发送心跳 取消心跳 重新连接 其它优化 Worker 初始化项目 electron 开发时会遇到一对多的情况,在进行 websocket 通信时,如果接收到服务端多个指令时,而这个指令刚好需要占用线程,这个时候整个界面就会失去响应,那么我们就可以使用线程来解决这个问题. npm create vite@latest electron-worker 执行完后修改

  • SpringMVC中文乱码踩坑记录

    目录 问题 问题根源 解决方案 方案一 方案二 问题 使用SpringMVC在返回一个字符串时发生了中文乱码问题.produces属性无效 @RequestMapping(value = "/nihao", produces = "text/plain;charset=UTF-8") @ResponseBody public String hello(HttpServletResponse response) throws UnsupportedEncodingEx

  • 关于python scrapy中添加cookie踩坑记录

    问题发现: 前段时间项目中,为了防止被封号(提供的可用账号太少),对于能不登录就可以抓取的内容采用不带cookie的策略,只有必要的内容才带上cookie去访问. 本来想着很简单:在每个抛出来的Request的meta中带上一个标志位,通过在CookieMiddleware中查看这个标志位,决定是否是给这个Request是否装上Cookie. 实现的代码大致如下: class CookieMiddleware(object): """ 每次请求都随机从账号池中选择一个账号去访

  • 微信小程序开发篇之踩坑记录

    最近参与开发了公司的第一款小程序,开发体验基本类似于基于webview的混合式开发,可以调用官方强大的api,但也有一些坑或者说不习惯的地方.这篇文章从实用性出发,记录了开发过程中的一些问题: 1. 样式优先级混乱 在使用button组件时,发现在class中设置width不生效,下面贴上代码: .my-button{ width: 140rpx; height: 60rpx; line-height: 60rpx; padding: 0; } 经过微信调试工具排查后,发现user agent的

  • JavaScript深拷贝的一些踩坑记录

    前言 之前去一家公司面试的时候,面试官问了我一个问题,说:"如何才能深拷贝一个对象".当时我心里有些窃喜,这么简单的问题还用想吗?于是脱口而出:"平时常用的有两种办法,第一种用JSON.parse(JSON.stringify(obj)),第二种可以使用for...in加递归完成".面试官听了以后点了点头觉得挺满意的. 当时我也并没有太过在乎这个问题,直到前段时间又想起这个问题,发现上面说的两种方法都是有Bug的. 提出问题 那么上面所说的Bug是什么呢? 特殊对象

  • Java常见踩坑记录之异常处理

    目录 一.Java异常类层次结构 二.Throwable类常用方法 三.try-catch-finally 四.使用 try-with-resources 来代替try-catch-finally 五.自定义异常 总结 一.Java异常类层次结构 Java中,所有的异常都来源于java.lang包中的Throwable类,它有两个重要的子类,Exception(异常)和Error(错误). Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获.Exception 又可以

  • JavaWeb踩坑记录之项目访问不到html文件

    踩坑问题和原因 踩坑问题 由于博主的JavaWeb是速成所以对一些知识点掌握的不是很熟,所以也就出现了今天这个问题——Tomcat访问不到html文件. 在运行是总是会出现404.每次出现这个就很烦,让人摸不着头脑.虽然这个问题其实对一些项目没有很大的影响,但是他会让我的项目目录会很杂乱.总的来说该问题就是不解决它,就会出现一堆静态资源都在一个文件夹.这可能会导致后期找一些项目的文件就得找半天. 踩坑原因 由于粗心的我把一些html文件都放在WEB-INF下面.因为WEB-INF下的资源不能直接

  • 详解vue-socket.io使用教程与踩坑记录

    目录 前言 我遇到的问题 使用教程 安装 引入(main.js) 使用(Page.vue) 解决方案 结合connect事件+store+路由守卫实现拦截 请先允许我狠狠吐个槽:vue-socket.io相关中文博客实在太少太少,来来去去就那么几篇,教程也比较零散,版本也比较老,就算我有暴风式搜索还是找不到解决问题的方案,然后我怒了,开始看源码.写测试demo.几乎把相关的issues都看了一遍,折腾1天后终于...搞定了,下面总结一下~ 考虑到很多小伙伴看完文章还是一头雾水或者无法复现方案,附

  • .net core 3.1在iis上发布的踩坑记录

    前言 写这篇文章的目的是希望像我一样喜欢.net 的人在发布 core到 iis上时少走点弯路 网上找了些资料,其实实际操作比较简单,就是有几个坑很恶心 踩坑记录 首先是你的服务器需要有core 的运行环境,安装前先关闭iis dotnet-hosting-3.1.4-win.exe 可以去微软的官网找最新的版本(去微软的官网找你要的版本就好了) 安装成功后,第一个坑出现了,启动iis,发现原来在iis上的网站都报503错误了. 直接玩大了,最后发现就是这个东西搞的鬼,你卸载它iis之前的网站就

  • Linux/Docker 中使用 System.Drawing.Common 踩坑记录分享

    前言 在项目迁移到 .net core 上面后,我们可以使用 System.Drawing.Common 组件来操作 Image,Bitmap 类型,实现生成验证码.二维码,图片操作等功能.System.Drawing.Common 组件它是依赖于 GDI+ 的,然后在 Linux 上并没有 GDI+,面向谷歌编程之后发现,Mono 团队使用 C语言 实现了GDI+ 接口,提供对非Windows系统的 GDI+ 接口访问能力,这个应该就是libgdiplus.所以想让代码在 linux 上稳定运

随机推荐