安装electron

安装cnpm

npm install -g cnpm --registry=https://registry.npm.taobao.org

使用cnpm安装electron,可以解决一些安装错误,比如连接超时等

cnpm install -g electron

项目构建

Electron-Vite脚手架构建

官网:https://cn.electron-vite.org/

构建命令:

pnpm create @quick-start/electron

Electron

在一个空的node项目中执行node初始化命令

npm init

在该项目中安装electron

cnpm install --save-dev electron

可以发现package.json文件的devDependencies多了electron,证明安装成功。
安装成功后,在scripts写上electron .,后续用于启动electron

{
"name": "electron_test",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^26.2.4"
}
}

其中main对应的main.js表示electron的入口文件

由于空项目中还没有需要我们创建,并且创建到根目录下

const { app, BrowserWindow } = require('electron')

function createWindow() {
// 创建浏览器窗口
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})

// 窗口最大化
win.maximize()

// 加载应用的index.html
win.loadFile('./src/index.html')

// 打开开发者工具
// win.webContents.openDevTools()
}

// 当Electron初始化完成并且准备创建浏览器窗口时调用该方法
app.whenReady().then(createWindow)

// 当所有窗口都关闭时退出应用
app.on('window-all-closed', () => {
// 在macOS上,除非用户用Cmd + Q显式退出,否则应用与菜单栏始终处于活动状态
if (process.platform !== 'darwin') {
app.quit()
}
})

app.on('activate', () => {
// 在macOS上,当单击dock图标并且没有其他窗口打开时,重新创建一个窗口
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})

最后在当前文件夹路径下,启动命令框,并执行命令启动electron

npm run start

Electron + Vue3 + TypeScript

创建vue3项目

npm create vue@latest	

测试vue3项目

npm i
npm run dev

安装electron依赖

cnpm install electron electron-builder electron-devtools-installer vite-plugin-electron vite-plugin-electron-renderer rimraf -D

配置electron,新建electron-main/index.ts

// electron-main/index.ts
import {app, BrowserWindow} from "electron"
import * as path from "path"

const createWindow = () => {
const win = new BrowserWindow({
webPreferences: {
contextIsolation: false, // 是否开启隔离上下文
nodeIntegration: true, // 渲染进程使用Node API
preload: path.join(__dirname, "./preload.js"), // 需要引用js文件
},
})

// 如果打包了,渲染index.html
if (process.env.NODE_ENV !== 'development') {
win.loadFile(path.join(__dirname, "./index.html"))
win.webContents.openDevTools()
} else {
const url = "http://localhost:5173" // 本地启动的vue项目路径。注意:vite版本3以上使用的端口5173;版本2用的是3000
win.loadURL(url)
win.webContents.openDevTools()
}
}

app.whenReady().then(() => {
createWindow() // 创建窗口
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

// 关闭窗口
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})

新建electron-preload/preload.ts

// electron-preload/preload.ts
import * as os from "os";

console.log("platform", os.platform());

修改tsconfig.json

添加下面的内容

"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","electron-main/**/*.ts","electron-preload/**/*.ts"],

修改vite.config.ts

添加下面的内容

...
import electron from "vite-plugin-electron"
import electronRenderer from "vite-plugin-electron-renderer"
import polyfillExports from "vite-plugin-electron-renderer"

export default defineConfig(({mode}) => ({
base: mode == 'development' ? '' : './',
plugins: [
...
electron([{
entry: "electron-main/index.ts", // 主进程文件
},
{
entry: 'electron-preload/preload.ts'
}
]),
electronRenderer(),
polyfillExports(),
],
...
build: {
emptyOutDir: false, // 默认情况下,若 outDir 在 root 目录下,则 Vite 会在构建时清空该目录
outDir: "dist-electron"
},
}))

修改package.json

  • 删除type
  • 添加"main": "dist-electron/index.js",
  • 修改"build": "rimraf dist-electron && vite build && electron-builder",

配置.gitignore

/dist-electron/

运行electron

执行命令

npm run dev

Electron+Next.js

使用next.js框架与Electron结合

创建项目

npx create-nextron-app appName

安装依赖项

yarn

打包程序

在package.json文件中添加一项

{
...
"scripts": {
...
"build": "nextron build",
"build:all": "nextron build --all",
"build:win32": "nextron build --win --ia32",
"build:win64": "nextron build --win --x64",
"build:mac": "nextron build --mac --x64",
"build:linux": "nextron build --linux"
}
...
}

执行命令

yarn build:win64

Electron+Vue2+Flask

在现有的Vue2项目上使用Electron打包成客户端

后端

添加接口

添加退出程序和判断当前服务是否打开的接口

关闭Electron的时候,只需要请求shutdown接口即可

@app.route('/shutdown')
def shutdown():
# 发送 SIGINT 信号(模拟 Ctrl+C)
os.kill(os.getpid(), signal.SIGINT)
return "Server shutting down..."


@app.route('/')
def hello():
return 'Server is running...'

打包

pyinstaller run_c.py

注意添加必要的文件和文件夹,logs、static

前端

安装依赖

npm install electron electron-builder vue-cli-plugin-electron-builder -D

添加后端

将后端打包好的run文件夹放到项目根目录下

添加Electron入口脚本

在项目根目录下新建background.js

import {app, protocol, BrowserWindow} from "electron";
import {createProtocol} from "vue-cli-plugin-electron-builder/lib";
import path from "path";
import {spawn} from "child_process";

const SERVER_PATH = "run/run.exe"; // 后端可执行文件路径
const PROTOCOL = "http://"; // 自定义协议
const SERVER_URL = "127.0.0.1"; // 后端服务URL
const SERVER_PORT = 8890; // 后端服务端口

const isDevelopment = process.env.NODE_ENV !== "production"; // 开发环境标志

/**
* 启动后端服务
*/
async function startBackendService() {
const backendPath = isDevelopment
? path.join(__dirname, "..", SERVER_PATH) // 开发环境路径
: path.join(process.resourcesPath, SERVER_PATH); // 生产环境路径

try {
const backend = spawn(backendPath); // 启动后端服务

// 监听后端服务的输出
backend.stdout.on("data", (data) => {
console.log(`服务 输出: ${data}`);
});

// 监听后端服务的错误输出
backend.stderr.on("data", (data) => {
console.error(`服务 错误: ${data}`);
});

// 监听后端服务的退出事件
backend.on("close", (code) => {
console.log(`服务 退出码: ${code}`);
});

await waitForBackend(); // 等待后端服务就绪

// 在应用程序退出时关闭后端服务
app.on("before-quit", async () => {
console.log("正在关闭服务...");
const res = await fetch(
`${PROTOCOL}${SERVER_URL}:${SERVER_PORT}/shutdown`
);
if (res.ok) {
console.log("成功关闭后端服务");
} else {
console.error("关闭后端服务失败");
}
});
} catch (error) {
console.error("关闭服务出现问题:", error);
}
}

/**
* 等待后端服务就绪
* @returns
*/
async function waitForBackend() {
const maxRetries = 30; // 最大重试次数
const retryInterval = 1000; // 重试间隔(毫秒)

for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(`${PROTOCOL}${SERVER_URL}:${SERVER_PORT}/`);
if (response.ok) {
console.log("服务端已就绪");
return;
}
} catch (error) {
console.log(`正在等待服务端启动... (${i + 1}/${maxRetries})`);
}
await new Promise((resolve) => setTimeout(resolve, retryInterval));
}
throw new Error("服务端启动失败");
}

// 注册 Electron自定义协议
// 允许使用 app:// 协议加载本地资源
protocol.registerSchemesAsPrivileged([
{scheme: "app", privileges: {secure: true, standard: true}},
]);

/**
* 创建浏览器窗口
*/
async function createWindow() {
// 创建浏览器窗口。
const win = new BrowserWindow({
width: 1200, // 调整默认窗口大小
height: 800, // 调整默认窗口大小
webPreferences: {
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, // 允许使用Node.js API
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION, // 禁用上下文隔离
webSecurity: false, // 允许加载本地资源
},
show: false, // 初始化时隐藏窗口
});

// 窗口准备好后显示,避免白屏
win.once("ready-to-show", () => {
win.show();
});

// 加载资源
if (process.env.WEBPACK_DEV_SERVER_URL) {
// 如果处于开发模式,则加载开发服务器的url
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
// 打开开发者工具
if (!process.env.IS_TEST) win.webContents.openDevTools();
} else {
createProtocol("app"); // 激活 Electron自定义协议
win.loadURL("app://./index.html");
}
}

// 关闭所有窗口后退出。
app.on("window-all-closed", () => {
app.quit();
});

// 当应用程序激活时,通常在 macOS 上重新创建一个窗口。
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

// 在 Electron 准备好时创建浏览器窗口。
app.on("ready", async () => {
await startBackendService(); // 启动后端服务
createWindow();
});

修改vue的配置

必须填写mainProcessFile、customFileProtocol

const { defineConfig } = require("@vue/cli-service");
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");

module.exports = defineConfig({
...
pluginOptions: {
electronBuilder: {
customFileProtocol: "./",
nodeIntegration: true,
mainProcessFile: "./background.js",
builderOptions: {
appId: "com.example.myapp",
productName: "MyElectronApp",
copyright: "Copyright © 2023",
directories: {
output: "dist_electron",
buildResources: "build",
},
extraResources: [
{
from: "public",
to: "public",
filter: ["**/*"],
},
{
from: "run", // 添加后端服务文件夹
to: "run", // 打包后端服务文件夹的路径
filter: ["**/*"]
}
],
// 添加主入口文件配置
extraMetadata: {
main: "background.js",
},
// 添加 NSIS 配置
win: {
target: [
{
target: "nsis",
arch: ["x64"],
},
],
icon: "./public/favicon256.ico", // 图标
},
nsis: {
oneClick: false, // 是否一键安装
allowElevation: true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
allowToChangeInstallationDirectory: true, // 允许修改安装目录
installerIcon: "./public/favicon.ico", // 安装图标
uninstallerIcon: "./public/favicon.ico", // 卸载图标
installerHeaderIcon: "./public/favicon.ico", // 安装时头部图标
createDesktopShortcut: true, // 创建桌面图标
createStartMenuShortcut: true, // 创建开始菜单图标
shortcutName: "POS-ERP", // 图标名称
perMachine: false, // 是否开启安装时权限限制(此电脑或当前用户)
deleteAppDataOnUninstall: true, // 卸载时是否清除用户数据
},
},
},
},
});

修改路由模式

...
const router = new VueRouter({
mode: 'hash', // 需要改为hash模式
scrollBehavior: () => ({ y: 0 }),
routes,
});
...

修改api请求

开发环境下使用代理,生产环境下使用本地服务地址

注意:Electron打包成的前端,与后端使用http方式进行交互的时候无法使用cookie、session!

let baseUrl = ""; // 后端接口地址
if (process.env.NODE_ENV === "development") {
// 开发环境
baseUrl = "/api";
} else {
// 生产环境
baseUrl = "http://localhost:8890";
}

添加npm命令

修改package.json

{
...
"scripts": {
...
"electron:serve": "vue-cli-service electron:serve",
"electron:build": "vue-cli-service electron:build"
},
...
}

打包

执行命令

npm run electron:build

Electron通信

Electron引入TypeORM

配置连接

Electron项目的数据库使用SQLite

import 'reflect-metadata'
import { DataSource } from 'typeorm'
import Config from './config'
import { Project } from './Entity/Project'

export const AppDataSource: DataSource = new DataSource({
type: 'sqlite',
database: Config.DB_PATH, // './database.sqlite'
synchronize: false,
logging: true,
entities: [Project],
migrations: [__dirname + '/migrations/*.ts'],
subscribers: []
})

设置模型

项目表

import { Entity, PrimaryColumn, Column } from 'typeorm'

@Entity('project')
export class Project {
@PrimaryColumn()
id: string

@Column()
name: string
}

TypeScript配置

由于Electron项目会有多个TypeScript的配置,因此迁移TypeORM定义的数据库时需要定义TypeORM专用的TypeScript配置

在根目录下新建tsconfig.typeorm.json文件

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"target": "ES2020",
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true
}
}

配置命令

配置TypeORM迁移命令

需要安装cross-env插件

pnpm add -D cross-env

添加package.json脚本配置

{
...
"scripts": {
...
"migration:generate": "cross-env TS_NODE_PROJECT=tsconfig.typeorm.json typeorm-ts-node-commonjs migration:generate src/main/migrations/auto -d src/main/data-source.ts",
"migration:run": "cross-env TS_NODE_PROJECT=tsconfig.typeorm.json typeorm-ts-node-commonjs migration:run -d src/main/data-source.ts",
"migration:revert": "cross-env TS_NODE_PROJECT=tsconfig.typeorm.json typeorm-ts-node-commonjs migration:revert -d src/main/data-source.ts"
},
...
}

执行命令

执行命令前需要检查migrations的路径,data-source.ts数据库连接文件的路径

如果migrations文件夹不存在需要创建

TS_NODE_PROJECT这个参数指向TypeORM专属的TypeScript配置

个性化

修改图标

需要修改electron右上角的图标可以在main.js中的BrowserWindow对象中修改icon属性

const { app, BrowserWindow } = require('electron')

function createWindow() {
// 创建浏览器窗口
const win = new BrowserWindow({
icon: "ico的路径",
})
}

打包

安装electron-build

npm install electron-builder -g

package.json文件中写入打包配置

{
"name": "electron_test",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder --win --x64" // 配置打包命令
},
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^26.2.4",
"electron-builder": "^24.6.4"
},
"build": {
"productName": "cropImage", // 软件名称
"appId": "com.zh.cropImage", // 软件id
"directories": {
"output": "dist" // 打包输出地址
},
"mac": {
"icon": "src\\static\\icon\\logo.ico" // mac系统下,图标的路径
},
"win": {
"icon": "src\\static\\icon\\logo.ic", // windows系统下,图标的路径
"target": [
"nsis"
]
},
"nsis": {
"oneClick": false, // 是否一键安装
"allowToChangeInstallationDirectory": true, // 是否允许修改安装目录
"perMachine": true // 是否以每台机器为单位进行安装
}
}
}

执行打包命令进行打包

npm run build

常见错误

打包依赖安装

electron-v.xxxx.zip

下载对应文件,无需解压放到AppData\Local\electron\Cache目录下

Linux系统一般放在~/.cash/electron的目录下

image-20231006215832415

winCodeSign-v.xxx.7z

下载对应文件,解压放到AppData\Local\electron-builder\Cache\winCodeSign目录下

image-20231006215640752

nsis-v.xxx.7z

下载对应文件,解压放到AppData\Local\electron-builder\Cache\nsis目录下

image-20231006215543666

nsis-resources-v.xxx.7z

下载对应文件,解压放到AppData\Local\electron-builder\Cache\nsis目录下

image-20231006215546769

不显示图片

设置相对路径

const { defineConfig } = require("@vue/cli-service");
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");

module.exports = defineConfig({
pluginOptions: {
electronBuilder: {
...
customFileProtocol: "./",
...
},
},
});

不显示界面

路由设置为hash

重定向路径

错误情况:使用location.replace重定向路由会导致Electron寻找路由对应的文件,而不是直接切换路径

因此要使用vue的路由工具router.push(path)

跨域问题

Electron升级后,请求后端无法携带cookie

目前只能使用Token的方式验证用户身份,不能使用session

验证码字体问题

报错信息:

服务 错误: [2025-04-27 14:12:07,495] ERROR in run_c: [127.0.0.1] [/user/verify_code] [GET] [500]
Traceback (most recent call last):
File "flask\app.py", line 880, in full_dispatch_request
File "flask\app.py", line 865, in dispatch_request
File "flask\views.py", line 110, in view
File "flask\views.py", line 191, in dispatch_request
File "apps\user\views.py", line 82, in get
File "apps\Controller\C_Sys_Controller.py", line 505, in get_verify_code
File "apps\utils\VerifyCodeUtils.py", line 75, in get_verify_code2
File "captcha\image.py", line 200, in generate
File "captcha\image.py", line 188, in generate_image
File "captcha\image.py", line 158, in create_captcha_image
File "captcha\image.py", line 105, in _draw_character
File "captcha\image.py", line 65, in truefonts
File "captcha\image.py", line 66, in <listcomp>
File "PIL\ImageFont.py", line 807, in truetype
File "PIL\ImageFont.py", line 804, in freetype
File "PIL\ImageFont.py", line 244, in __init__
OSError: cannot open resource

分析代码:

def get_verify_code2():
img_width = 200
img_height = 100
fontsize = (42, 50, 56)

image = ImageCaptcha(
width=img_width, height=img_height, font_sizes=fontsize)
captcha_text = VerifyCodeUtils.random_captcha_text()
captcha_image = Image.open(image.generate(captcha_text))
return captcha_image, captcha_text

image.generate(captcha_text),其中这一步使用调用了pillow的方法,pillow又调用了本地环境的字体

因此,只需要将字体文件夹添加到打包环境中即可,在生成验证码图片的时候指定字体文件即可

def get_verify_code2():
img_width = 200
img_height = 100
fontsize = (42, 50, 56)

# 配置字体路径
font_path = os.path.join(Config.FONT_PATH, 'arial.ttf')

image = ImageCaptcha(
width=img_width, height=img_height, font_sizes=fontsize, fonts=[font_path])
captcha_text = VerifyCodeUtils.random_captcha_text()
captcha_image = Image.open(image.generate(captcha_text))
return captcha_image, captcha_text

config.py

class Config:
...
# 文件路径
FILE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
# 字体文件路径
FONT_PATH = os.path.join(FILE_PATH, 'fonts')
...