接上一篇: Electron 初体验
代码完善
上一篇中,简单的让AI写了一些demo内容,菜鸟又在其上加上了不少功能。但感觉从vue到初次开发electron真正需要注意的,还是那些读取文件、连接数据库之类的东西。
而上一篇中就是主要介绍的就是这些,其实看懂一个,后面的都是以此类推即可!
这里之所以要写,是因为菜鸟感觉读取文件的方案还是不好,改成了连接数据库,让后端直接在服务器上安装了mySQL,直接用electron去连接即可。
菜鸟也对之前写的代码进行了抽离,目录结构变成了这样:
代码如下
main.js
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const path = require("path");
const { join } = require("path");
const fs = require("fs");
const { spawn } = require("child_process");
const express = require("express");
const mysql = require("mysql2/promise");
// ======================= 工具模块 =======================
const { selectPath } = require("./modules/selectPath.js");
const { runScript, runScriptNoWait } = require("./modules/runScript.js");
const { readDir } = require("./modules/readDir.js");
const { readConfig } = require("./modules/readConfig.js");
const { writeConfig } = require("./modules/writeConfig.js");
const { saveImageToUserData } = require("./modules/saveImageToUserData.js");
const { loadImageAsBase64 } = require("./modules/loadImageAsBase64.js");
const { getSeqChipPath } = require("./modules/getSeqChipPath.js");
const { readFa, readBFile, readCFile, readSpecifyFile } = require("./modules/readFa.js");
const { writeJsonToPath } = require("./modules/writeJsonToPath.js");
// ======================= API =======================
const { loginApi } = require("./api/login.js");
const { appraisalRecordAddApi } = require("./api/appraisalRecordAdd.js");
const { appraisalRecordListApi } = require("./api/appraisalRecordList.js");
// ======================= Electron 配置 =======================
// 屏蔽安全警告
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
// ======================= Window =======================
function createWindow() {
const win = new BrowserWindow({
width: 1920,
height: 1080,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false
}
});
// 启动时最大化(保留窗口按钮)
win.maximize();
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
// 开启调试台
win.webContents.openDevTools();
// 添加快捷键 F12 打开控制台
win.webContents.on("before-input-event", (event, input) => {
if (input.key === "F12") {
win.webContents.toggleDevTools();
event.preventDefault();
}
});
} else {
win.loadFile(join(__dirname, "../dist/index.html"));
}
}
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
// ======================= 目录初始化 —— 把配置文件复制到用户目录 =======================
const userDataPath = app.getPath("userData");
const scriptDir = path.join(userDataPath, "scripts");
if (!fs.existsSync(scriptDir)) {
fs.mkdirSync(scriptDir, { recursive: true });
}
// ======================= Config =======================
function ensureConfig() {
const configPath = path.join(userDataPath, "scripts", "config.json");
const defaultConfigPath = path.join(__dirname, "scripts", "config.json");
if (!fs.existsSync(configPath)) {
fs.copyFileSync(defaultConfigPath, configPath);
}
return configPath;
}
function loadConfig(configPath) {
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
}
// ======================= IPC =======================
// 选择路径
selectPath(ipcMain, dialog);
// 运行脚本 -- 等待结果
runScript(ipcMain, app, path, fs, spawn);
// 运行脚本 -- 不等待结果
runScriptNoWait(ipcMain, app, path, fs, spawn);
// 读取某个文件夹下是否有文件
readDir(ipcMain, fs);
// 读取配置文件
readConfig(ipcMain, app, path, fs);
// 写入配置文件
writeConfig(ipcMain, app, path, fs);
// 保存用户上传的图片到用户目录
saveImageToUserData(ipcMain, app, path, fs);
// 加载图片作为 base64 编码
loadImageAsBase64(ipcMain, app, path, fs);
// 获取配置文件下的测序路径,并将下面的文件返回成select可以使用的选项
getSeqChipPath(ipcMain, app, path, fs);
// 读取 FA 文件
readFa(ipcMain, fs, path);
// 读取 txt 文件
readBFile(ipcMain, fs, path);
// 读取 .blast.txt 文件
readCFile(ipcMain, fs, path);
// 读取指定文件
readSpecifyFile(ipcMain, fs, path);
// 写入 JSON 文件到指定路径
writeJsonToPath(ipcMain, path, fs);
// ======================= Local API =======================
let apiServer;
let apiPort;
let mysqlPool;
async function startLocalApi(config) {
const { MySQLHost, MySQLPort, MySQLUser, MySQLPassword, MySQLDatabase } = config;
mysqlPool = mysql.createPool({
host: MySQLHost,
port: MySQLPort,
user: MySQLUser,
password: MySQLPassword,
database: MySQLDatabase,
connectionLimit: 5
});
const api = express();
api.use(express.json());
api.use((req, res, next) => {
// 解决跨域问题
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
next();
});
// 登录接口
loginApi(api, mysqlPool);
// 鉴定记录添加接口
appraisalRecordAddApi(api, mysqlPool);
// 鉴定记录列表接口
appraisalRecordListApi(api, mysqlPool);
// ⭐ 随机端口,只监听本机
apiServer = api.listen(0, "127.0.0.1", () => {
apiPort = apiServer.address().port;
console.log("Local API started on port:", apiPort);
});
}
// 获取本地 API 端口
ipcMain.handle("getApiPort", () => apiPort);
// ======================= 启动顺序(关键) =======================
app.whenReady().then(async () => {
const configPath = ensureConfig();
const config = loadConfig(configPath);
await startLocalApi(config);
createWindow();
});
注意
这里跨域,GPT后面建议我更加完善的写法是下面这样的
第一种
替换api.use((req, res, next) => {})这一块的内容(GPT推荐的企业级标准做法,上面、下面的都不行的是时候,可以试试这个)
// 多下载一个cors库
const cors = require("cors");
……
api.use(cors({
origin: "http://localhost:5173",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true
}));
// 一定要有
app.options("*", cors());
……
第二种
更加完善的写法,如果按照菜鸟的写法,还有跨域问题,可以改成这个!
api.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
// 排除预检请求可能出现的问题
if (req.method === "OPTIONS") {
return res.sendStatus(204);
}
next()
});
如何使用接口?
菜鸟直接将API的端口存在了pinia中,避免每次调用还要去和electron通信
port.js
import { ref } from "vue";
import { defineStore } from "pinia";
export const usePortStore = defineStore("port", () => {
let port = ref(null);
// 异步 action
async function fetchPort() {
port.value = await window.electronAPI.getApiPort();
}
function getPort() {
return port.value;
}
return { port, fetchPort, getPort };
});
简单列举一个登录的例子
login.vue
<script setup>
import router from "@/router";
import { NForm, NFormItemRow, NInput, NButton, useMessage } from "naive-ui";
import { reactive, ref } from "vue";
import logo from "@/assets/img/logo.png";
import { usePortStore } from "@/stores/port";
import { useUserStore } from "@/stores/user";
const portStore = usePortStore();
const userStore = useUserStore();
const message = useMessage();
const formData = reactive({
username: "xxx",
password: "xxxxx"
});
const rules = reactive({
username: [{ required: true, message: "请输入用户名", trigger: ["blur"] }],
password: [{ required: true, message: "请输入密码", trigger: ["blur"] }]
});
const formRef = ref(null);
const login = () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
const res = await fetch(`http://127.0.0.1:${portStore.getPort()}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: formData.username, password: formData.password })
});
const result = await res.json();
console.log("登录响应:", result);
if (result.ok) {
// 可以存到 Pinia 或 localStorage
userStore.setUser(result.user);
router.replace("/home");
} else {
message.error("登录失败:" + result.message);
}
} catch (err) {
console.error("请求错误:", err);
}
} else {
console.log(errors);
}
});
};
const model = ref({
leftLogo: [
{
url: logo
}
],
rightLogo: [
{
url: logo
}
]
});
// 读取配置文件
const readConfig = async () => {
try {
const config = await window.electronAPI.readConfig();
console.log("读取到的配置:", config);
if (config.leftLogo[0]?.name) {
model.value.leftLogo = [
{
url: await window.electronAPI.loadImageAsBase64(config.leftLogo[0].name)
}
];
}
if (config.rightLogo[0]?.name) {
model.value.rightLogo = [
{
url: await window.electronAPI.loadImageAsBase64(config.rightLogo[0].name)
}
];
}
// 文本类配置(直接覆盖没问题)
if (config?.supportText) {
model.value.supportText = config.supportText;
}
console.log("配置后的模型:", model.value);
} catch (err) {
console.error("读取配置文件失败:", err);
}
};
readConfig();
</script>
<template>
<div class="flex h-full flex-col items-center justify-center">
<header class="w-full">
<div class="mb-4 flex items-center justify-evenly">
<img class="w-[200px]" :src="model.leftLogo[0]?.url" alt="logo" />
<img class="w-[200px]" :src="model.rightLogo[0]?.url" alt="logo" />
</div>
</header>
<main class="loginBox relative h-[70%] w-full">
<div
class="loginForm absolute top-[50%] left-[80%] w-[400px] translate-x-[-50%] translate-y-[-50%] rounded-[10px] bg-white p-[30px]"
>
<n-form ref="formRef" :model="formData" :rules="rules">
<n-form-item-row label="用户名" path="username">
<n-input placeholder="请输入用户名" v-model:value="formData.username" />
</n-form-item-row>
<n-form-item-row label="密码" path="password">
<n-input
type="password"
show-password-on="mousedown"
placeholder="请输入密码"
v-model:value="formData.password"
/>
</n-form-item-row>
</n-form>
<n-button type="primary" block secondary strong @click="login"> 登录 </n-button>
</div>
</main>
<footer>
<p class="text-center whitespace-pre-line">
{{ model.supportText }}
</p>
</footer>
</div>
</template>
<style scoped>
.loginBox {
background-image: url("@/assets/img/bg.jpg");
background-size: 100% 100%;
}
.loginForm {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
</style>
菜鸟这个主要就是读取配置文件、连接数据库读写数据,没有很复杂的操作,所以基本上使用到的electron就是这些!
在乌班图上打包
上一篇有说,windows上打包有缺陷,所以这里菜鸟用的方式是:让公司给开一个乌班图的服务器(得有图形服务),在服务器上配置一下node环境,将本地代码搞成压缩包传到服务器,在服务器上解压后执行npm i,并打包!
配置服务器上的node环境
这个菜鸟其实也是一窍不通,但是幸好有AI,问一下怎么在服务器上配置的node环境,就可以出现
第一步:
在乌班图的浏览器上,访问:nodejs.org/zh-cn/downl… ,并选择对应的版本下载即可!
第二步:
进入下载目录解压,解压后将其转移到 /usr/local/node 目录
cd yourDwonloadPath
tar -xvf node-vxx.xx.xx-linux-x64.tar.xz
移动目录
sudo mv node-vxx.xx.xx-linux-x64 /usr/local/node
第三步:
设置环境变量
vim ~/.bashrc
并添加
export NODE_HOME=/usr/local/node
export PATH=$NODE_HOME/bin:$PATH
让其生效
source ~/.bashrc
然后就可以 node -v 和 npm -v 看看是否真的生效了!
运行
将本地代码,打包并上传到乌班图服务器
这里也是直接问AI,服务器上可以支持解压7z吗?怎么操作,就可以得到!
解压之前需要下载解压软件
sudo apt update
sudo apt install -y p7zip-full
解压
cd 你上传的文件路径
解压到当前目录
7z x file.7z
解压到指定目录
7z x file.7z -o/home/p/output
然后执行npm i、npm run dev 看能不能运行
如果运行白屏
如果运行时出现白屏,可以在最上面多加上这几行试试(大概率不加也能运行)。
……
// 打包其实这几个配置无效 —— 如果需要在linux系统上npm run dev运行,要加上 —— 后面有说明(见下方deb包)
// 禁 GPU(消掉噪音) -- 新增
app.disableHardwareAcceleration();
// 关 sandbox(关键)-- 新增
app.commandLine.appendSwitch("disable-gpu");
app.commandLine.appendSwitch("no-sandbox");
app.commandLine.appendSwitch("disable-setuid-sandbox");
// 屏蔽安全警告
……
如果还运行不起来,报错X11等问题,则需要问AI:怎么在无头环境上运行!(这个菜鸟没有研究,但是确实可能会出现,如果跑的虚拟机差这差那!)
连接不上数据库
如果可以运行,但是连不上数据库,这时候是可以看见报错的,直接将报错给GPT,就会告诉我们要修改 main.js 中连接数据库的地方,不能再用ip地址了,要用本地ip地址
……
// 连接数据库
let apiServer;
let apiPort;
let mysqlPool;
async function startLocalApi() {
const api = express();
api.use(express.json());
// ⚠️ MySQL 连接池(只初始化一次)
mysqlPool = mysql.createPool({
host: "localhost", // 修改成localhost或者127.0.0.1
port: 13306,
user: "barcode_for_medicinal", // 数据库账号
password: "xxxx", // 数据库密码
database: "barcode_for_medicinal", // 数据库名称
connectionLimit: 5 // 连接池最大连接数
});
api.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
next();
});
// 登录接口
loginApi(api, mysqlPool);
// 鉴定记录添加接口
appraisalRecordAddApi(api, mysqlPool);
// 鉴定记录列表接口
appraisalRecordListApi(api, mysqlPool);
// ⭐ 随机端口,只监听本机
apiServer = api.listen(0, "127.0.0.1", () => {
apiPort = apiServer.address().port;
console.log("Local API started on port:", apiPort);
});
}
整个功能可以正常使用后,就到打包这一步了。
打包
需要修改package.json(这里已经是菜鸟试错多次,并在GPT的帮助下修改好了,所以就不一 一列举是怎么踩坑的了)
{
"name": "barcode-medicinal",
"productName": "Barcode Medicinal",
"version": "1.0.0",
"private": true,
"homepage": "http://xxx.xxx.xxx.xx:5050/other/barcodeformedicinal.git", // 一定要有,不然会报错(随便放个git地址即可)
"main": "ElectronSrc/main.js",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"dev:electron": "electron .",
"dev:full": "npm run dev & npm run dev:electron",
"build:vue": "vite build",
"build:electron": "electron-builder",
"build": "npm run build:vue && npm run build:electron -- --linux",
"preview": "vite preview",
"lint": "eslint . --fix --cache",
"format": "prettier --write src/"
},
"build": {
"appId": "com.example.barcodeformedicinal",
"productName": "barcode-medicinal", // 一定要用英文,windows上可以中文
"asar": true,
"directories": {
"output": "dist_electron"
},
"files": [
"dist/**",
"ElectronSrc/**",
"package.json"
],
// 这样写会打包4个出来
"linux": {
"target": [
// 打包deb
{
"target": "deb",
"arch": [
"x64",
"arm64"
]
},
// 打包AppImage
"AppImage",
],
"category": "Utility",
"description": "Barcode Medicinal Electron",
"maintainer": "Your Name <youremail@example.com>" // 必须写,假的也行,不然报错
}
},
"dependencies": {
……
},
"devDependencies": {
……
}
}
执行 npm run build,你会得到三个文件,打包后的文件,在package.json中的这里
"directories": {
"output": "dist_electron"
},
里面应该有几个乌班图可以运行的文件
运行AppImage
这里需要问AI,如何在服务器上,用命令行运行AppImage文件。
执行后会发现运行不起来,因为AppImage文件运行需要下载libfuse2
sudo apt update
sudo apt install libfuse2
cd dist_electron
chmod +x xxxx.AppImage
./xxxx.AppImage
GPT给了第二种方式,但是菜鸟浅试了不行(没有深究,感兴趣的读者可以试试!)
运行deb
这里也是直接问GPT,如何在服务器上运行deb文件
sudo dpkg -i dist_electron/barcode-medicinal_1.0.0_amd64.deb
就会自动帮你安装,如果看到 依赖错误,则继续执行下面的代码(一般不会)
sudo apt -f install
卸载
安装后想删除?
这里还挺复杂,不同的安装方式,删除不一样,这里菜鸟只给出用sudo dpkg安装的卸载方法!
一般执行这一个就行了
sudo apt purge 你的应用名(就是package.json中build中的productName)
清理垃圾 —— 可不执行
sudo apt autoremove
sudo apt autoclean
确保卸载
dpkg -l | grep barcode(没有输出就是正确的)
槽点
这里有一个槽点,你就算执行了清理垃圾这步操作,其实用户数据还是会存在,需要手动删除
cd ~/.config
ll
cd 你的软件名
rm -rf 你想删除的文件
至于别的几种卸载方式,菜鸟没有用到,就没有搞了,这里给出GPT的截图
打包白屏
这里不管是哪一种,打包后运行出来都是白屏!
菜鸟实在是没有办法,GPT没办法读取整个项目,所以用Trae查看我的整个项目。
下面就是Trae在查看完我的项目后,结合我说打包后运行出来是白屏给出的代码(完全不知道原因,感觉是服务器缺少依赖导致 —— 见下方 《问GPT为什么?》)
sudo apt install -y libnss3 libatk-bridge2.0-0 libxss1 libgbm-dev libx11-xcb1 libxcomposite1 libxcursor1 libxdamage1 libxi6 libxtst6 libxrandr2 libpangocairo-1.0-0 libgtk-3-0 libdrm2 libxkbcommon0 libwayland-client0 libwayland-server0
./barcode-medicinal-1.0.0.AppImage --disable-dev-shm-usage --no-sandbox --disable-gpu
结果真成功运行出来了,但是用户肯定不会这样运行啊!
所以还是要看deb,怎么搞?
deb包
一开始菜鸟,按照GPT说的加上了
却没有任何效果,后面才发现在main.js上加,在打包过程中会直接被忽略,应该写在package.json中的linux配置里,打包好后再按照上面的deb运行方式就可以运行不白屏了!
"linux": {
"target": [
{
"target": "deb",
"arch": [
"x64",
"arm64"
]
},
"AppImage",
],
"executableArgs": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu"
],
"category": "Utility",
"description": "Barcode Medicinal Electron",
"maintainer": "Your Name <youremail@example.com>" // 必须写,假的也行,不然报错
}
AppImage包 —— 骚操作
一开始菜鸟是试的deb不行,突发奇想,直接自己创建一个桌面快捷键,然后运行面的命令不就行了?
所以就问了GPT,然后GPT给出的一步一步的流程比较复杂,且vim也不是很会用,所以直接让GPT给了一个一次性输入就行的命令(需要带上自己的包名、路径,不然GPT给的还是不能直接copy)
# 创建 .desktop 文件
cat > ~/.local/share/applications/barcode-medicinal.desktop <<EOL
[Desktop Entry]
Name=Barcode Medicinal
Comment=Barcode Medicinal App
Exec=/home/bnzycjd/xxx/dist_electron/barcode-medicinal-1.0.0.AppImage --disable-dev-shm-usage --no-sandbox --disable-gpu
Icon=/home/bnzycjd/Pictures/logo.png
Terminal=false
Type=Application
Categories=Utility;
StartupWMClass=barcode-medicinal
EOL
# 赋予执行权限
chmod +x ~/.local/share/applications/barcode-medicinal.desktop
# 标记为可信任
gio set ~/.local/share/applications/barcode-medicinal.desktop metadata::trusted true
# 刷新应用菜单
update-desktop-database ~/.local/share/applications
这个icon如果一开始没写,后面写了也可能半天不刷新(这里有一个骚操作,就是在Show Apps里右键先将其Pin to Dash,然后再弄回来就好了)!
问GPT为什么?
反正菜鸟感觉是服务器里面东西不全,所以导致需要这些操作,感觉和代码没啥关系!
2026/1/7 其实这里有一个大坑
见另一篇文章:Electron 脚本调用大坑!害惨我了
总结
到此,整个项目算是彻底结束,全程我只负责给AI打下手,主要的Electron逻辑全部由AI生成。这种开发方式,更像是一种「人与 AI 的协作模式」:我不断向 AI 解释业务背景、拆解需求并校正实现方向,由它完成 Electron 层面的具体实现;而前端部分,由于我本身比较熟悉,且界面逻辑相对简单,仍然主要由我自己负责。
回头看这一年的变化,只能说 AI 的成长速度既让人感到压力,又很难不为之兴奋。
在实际实践中,AI 在代码编写、代码提示、Bug 排查以及问题定位等方面,确实为开发者带来了极大的便利。无论是快速生成可运行的示例代码,还是在遇到复杂报错时给出清晰的排查思路,都显著缩短了从“问题出现”到“问题解决”的时间成本。
从我的感受来说,AI 并不是在取代开发者,而是在持续介入我们的思考过程 —— 甚至在某些时候,让人产生一种“自己越来越不用动脑子”的错觉。
但冷静下来会发现,AI 真正取代的,往往只是那些简单、重复、缺乏上下文理解的逻辑书写者;它并不能淘汰业务梳理者、需求沟通者和系统架构的搭建者。
从我让AI写electron的读取文件代码、写接口代码,基本都是一次就过可以看出,后端的CRUD仔更容易被替换,这些固定逻辑,其实AI可以很好的写出来了,前端感觉只要界面复杂,那么不可替代性还是比较高的!
从使用 Trae 生成复杂项目的过程也能看出来:无论Trae公共方法写得多完善、GPT写的文件读取逻辑多么标准,AI 都无法直接交付一个真正可运行、符合个性化需求的完整系统,更无法独立完成复杂业务和界面层面的设计,但是也确实让技术越来越不值钱了。
可以看我的这两篇文章
都是Trae无法生成整体复杂项目,但是Trae也有很大的优点,就是可以全局读取整个项目,整体分析,是copilot、GPT无法完成的,cursor或许更强,但是收费就挡住了很多人!