告别Web限制:用Vue2+Electron 13.x手把手打造一个串口调试桌面工具(附完整源码)
基于Vue2与Electron构建跨平台串口调试工具实战指南
在物联网与嵌入式开发领域,串口通信作为最基础的数据交互方式之一,其调试过程却常常受限于浏览器沙箱环境。传统解决方案要么依赖专用硬件调试器,要么需要编写繁琐的本地程序,这对于前端开发者而言存在较高的技术门槛。本文将展示如何利用Vue2与Electron 13.x的组合优势,快速构建一个兼具美观界面与强大功能的跨平台串口调试工具,彻底摆脱Web环境的技术桎梏。
1. 开发环境配置与技术选型
1.1 基础工具链搭建
构建Electron+Vue2混合应用需要以下核心组件:
- Node.js 14+:确保支持ES6语法和Electron 13.x的依赖要求
- Vue CLI 4.x:提供标准化的Vue项目脚手架
- electron-builder:用于最终的应用打包与分发
推荐使用以下命令初始化项目环境:
# 安装Vue CLI(如已安装可跳过) npm install -g @vue/cli # 创建Vue2项目 vue create serial-port-tool --preset default # 添加Electron支持 cd serial-port-tool vue add electron-builder1.2 关键依赖选择
串口通信功能需要引入以下核心npm包:
| 包名称 | 版本 | 作用描述 |
|---|---|---|
| serialport | ^10.5.0 | 提供底层串口通信能力 |
| @serialport/stream | ^10.5.0 | 串口数据流处理基础库 |
| xterm | ^4.19.0 | 终端模拟器,用于数据展示 |
安装命令:
npm install serialport @serialport/stream xterm注意:serialport是Node.js原生模块,在不同平台可能需要重新编译。如遇安装问题,可尝试:
npm install --global windows-build-tools # Windows环境
2. 核心功能模块实现
2.1 串口管理服务封装
创建src/services/serialService.js实现串口生命周期管理:
import { SerialPort } from 'serialport' import { ReadlineParser } from '@serialport/parser-readline' class SerialService { constructor() { this.port = null this.parser = null } async listPorts() { return await SerialPort.list() } async openPort(options) { this.port = new SerialPort({ path: options.path, baudRate: options.baudRate || 9600, dataBits: 8, parity: 'none', stopBits: 1 }) this.parser = this.port.pipe(new ReadlineParser({ delimiter: '\n' })) return new Promise((resolve, reject) => { this.port.on('open', () => resolve()) this.port.on('error', err => reject(err)) }) } sendData(data) { if (!this.port?.isOpen) { throw new Error('Port not opened') } this.port.write(data + '\n') } closePort() { return new Promise(resolve => { if (!this.port?.isOpen) { return resolve() } this.port.close(err => { if (err) console.error(err) resolve() }) }) } } export default new SerialService()2.2 主进程与渲染进程通信
在src/background.js中配置进程间通信:
import { ipcMain } from 'electron' ipcMain.handle('serial:list', async () => { return serialService.listPorts() }) ipcMain.handle('serial:open', (_, options) => { return serialService.openPort(options) }) ipcMain.handle('serial:send', (_, data) => { return serialService.sendData(data) }) ipcMain.handle('serial:close', () => { return serialService.closePort() })3. 用户界面设计与交互实现
3.1 串口控制面板组件
创建src/components/SerialControl.vue:
<template> <div class="serial-control"> <el-select v-model="selectedPort" placeholder="选择串口"> <el-option v-for="port in portList" :key="port.path" :label="`${port.path} (${port.manufacturer || '未知设备'})`" :value="port.path"> </el-option> </el-select> <el-select v-model="baudRate" placeholder="波特率"> <el-option v-for="rate in [9600, 19200, 38400, 57600, 115200]" :key="rate" :label="rate" :value="rate"> </el-option> </el-select> <el-button type="primary" @click="handleOpen" :disabled="!selectedPort || isConnected"> 打开串口 </el-button> <el-button type="danger" @click="handleClose" :disabled="!isConnected"> 关闭串口 </el-button> </div> </template> <script> export default { data() { return { portList: [], selectedPort: '', baudRate: 9600, isConnected: false } }, async created() { await this.refreshPorts() }, methods: { async refreshPorts() { this.portList = await window.electron.ipcRenderer.invoke('serial:list') }, async handleOpen() { try { await window.electron.ipcRenderer.invoke('serial:open', { path: this.selectedPort, baudRate: this.baudRate }) this.isConnected = true this.$emit('connected') } catch (err) { this.$message.error(`打开失败: ${err.message}`) } }, async handleClose() { await window.electron.ipcRenderer.invoke('serial:close') this.isConnected = false this.$emit('disconnected') } } } </script>3.2 终端数据显示组件
创建src/components/TerminalDisplay.vue:
<template> <div id="terminal-container"></div> </template> <script> import { Terminal } from 'xterm' import 'xterm/css/xterm.css' export default { props: { content: String }, data() { return { term: null } }, mounted() { this.initTerminal() }, methods: { initTerminal() { this.term = new Terminal({ fontSize: 14, cursorBlink: true, theme: { background: '#1E1E1E', foreground: '#CCCCCC' } }) this.term.open(document.getElementById('terminal-container')) // 接收数据回调 this.$on('data', data => { this.term.write(data) }) } } } </script>4. 应用打包与分发优化
4.1 多平台打包配置
修改vue.config.js实现定制化打包:
module.exports = { pluginOptions: { electronBuilder: { nodeIntegration: true, builderOptions: { appId: 'com.example.serialtool', productName: '串口调试助手', win: { icon: 'build/icons/icon.ico', target: ['nsis', 'portable'] }, mac: { icon: 'build/icons/icon.icns', target: ['dmg', 'zip'] }, linux: { icon: 'build/icons/icon.png', target: ['AppImage', 'deb'] }, nsis: { oneClick: false, allowToChangeInstallationDirectory: true } } } } }4.2 解决serialport原生模块问题
创建scripts/rebuild.js处理跨平台编译:
const { rebuild } = require('electron-rebuild') async function main() { await rebuild({ buildPath: __dirname, electronVersion: require('electron/package.json').version, extraModules: ['serialport'] }) } main().catch(err => { console.error(err) process.exit(1) })在package.json中添加脚本命令:
{ "scripts": { "rebuild": "node scripts/rebuild.js", "postinstall": "npm run rebuild" } }5. 进阶功能扩展思路
5.1 数据协议解析器
实现常见硬件协议解析(如Modbus):
class ModbusParser { static parseRTU(buffer) { // 实现Modbus RTU协议解析逻辑 return { address: buffer[0], functionCode: buffer[1], data: buffer.slice(2, -2), crc: buffer.readUInt16LE(buffer.length - 2) } } static formatRTU(command) { // 实现Modbus RTU命令格式化 const buffer = Buffer.alloc(6) // ...填充数据 return buffer } }5.2 数据持久化与回放
集成SQLite实现历史数据存储:
const sqlite3 = require('sqlite3').verbose() const db = new sqlite3.Database('serial_data.db') db.serialize(() => { db.run(`CREATE TABLE IF NOT EXISTS sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, start_time DATETIME DEFAULT CURRENT_TIMESTAMP, port_path TEXT, baud_rate INTEGER )`) db.run(`CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER, direction TEXT CHECK(direction IN ('in', 'out')), content TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(session_id) REFERENCES sessions(id) )`) })5.3 自动化测试脚本
使用Puppeteer实现界面自动化测试:
const puppeteer = require('puppeteer-core') const { _electron: electron } = require('playwright') test('串口连接测试', async () => { const electronApp = await electron.launch({ args: ['.'] }) const window = await electronApp.firstWindow() // 模拟选择串口 await window.selectOption('.el-select', 'COM3') await window.click('button:has-text("打开串口")') // 验证连接状态 const button = await window.$('button:has-text("关闭串口")') expect(button).not.toBeDisabled() await electronApp.close() })