安装electron 安装cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
使用cnpm安装electron,可以解决一些安装错误,比如连接超时等
项目构建 Electron-Vite脚手架构建 官网:https://cn.electron-vite.org/
构建命令:
pnpm create @quick-start/electron
Electron 在一个空的node项目中执行node初始化命令
在该项目中安装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 () win.loadFile ('./src/index.html' ) } app.whenReady ().then (createWindow) app.on ('window-all-closed' , () => { if (process.platform !== 'darwin' ) { app.quit () } }) app.on ('activate' , () => { if (BrowserWindow .getAllWindows ().length === 0 ) { createWindow () } })
最后在当前文件夹路径下,启动命令框,并执行命令启动electron
Electron + Vue3 + TypeScript 创建vue3项目
测试vue3项目
安装electron依赖 cnpm install electron electron-builder electron-devtools-installer vite-plugin-electron vite-plugin-electron-renderer rimraf -D
配置electron,新建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 , preload : path.join (__dirname, "./preload.js" ), }, }) if (process.env .NODE_ENV !== 'development' ) { win.loadFile (path.join (__dirname, "./index.html" )) win.webContents .openDevTools () } else { const url = "http://localhost:5173" 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 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 : "dist-electron" }, }))
修改package.json
删除type;
添加"main": "dist-electron/index.js",
修改"build": "rimraf dist-electron && vite build && electron-builder",
配置.gitignore
运行electron 执行命令
Electron+Next.js 使用next.js框架与Electron结合
创建项目 npx create-nextron-app appName
安装依赖项
打包程序 在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" } ... }
执行命令
Electron+Vue2+Flask
在现有的Vue2项目上使用Electron打包成客户端
后端 添加接口 添加退出程序和判断当前服务是否打开的接口
关闭Electron的时候,只需要请求shutdown接口即可
@app.route('/shutdown' ) def shutdown (): os.kill(os.getpid(), signal.SIGINT) return "Server shutting down..." @app.route('/' ) def hello (): return 'Server is running...'
打包
注意添加必要的文件和文件夹,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" ; 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); } } 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 ("服务端启动失败" ); } 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 , contextIsolation : !process.env .ELECTRON_NODE_INTEGRATION , webSecurity : false , }, show : false , }); win.once ("ready-to-show" , () => { win.show (); }); if (process.env .WEBPACK_DEV_SERVER_URL ) { await win.loadURL (process.env .WEBPACK_DEV_SERVER_URL ); if (!process.env .IS_TEST ) win.webContents .openDevTools (); } else { createProtocol ("app" ); win.loadURL ("app://./index.html" ); } } app.on ("window-all-closed" , () => { app.quit (); }); app.on ("activate" , () => { if (BrowserWindow .getAllWindows ().length === 0 ) createWindow (); }); 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" , }, win : { target : [ { target : "nsis" , arch : ["x64" ], }, ], icon : "./public/favicon256.ico" , }, nsis : { oneClick : false , allowElevation : true , 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' , 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" } , ... }
打包 执行命令
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 , 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插件
添加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" , "directories" : { "output" : "dist" } , "mac" : { "icon" : "src\\static\\icon\\logo.ico" } , "win" : { "icon" : "src\\static\\icon\\logo.ic" , "target" : [ "nsis" ] } , "nsis" : { "oneClick" : false , "allowToChangeInstallationDirectory" : true , "perMachine" : true } } }
执行打包命令进行打包
常见错误 打包依赖安装 electron-v.xxxx.zip 下载对应文件,无需解压 放到AppData\Local\electron\Cache目录下
Linux系统一般放在~/.cash/electron的目录下
winCodeSign-v.xxx.7z 下载对应文件,解压 放到AppData\Local\electron-builder\Cache\winCodeSign目录下
nsis-v.xxx.7z 下载对应文件,解压 放到AppData\Local\electron-builder\Cache\nsis目录下
nsis-resources-v.xxx.7z 下载对应文件,解压 放到AppData\Local\electron-builder\Cache\nsis目录下
不显示图片 设置相对路径
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' ) ...