基于Electron27+Vite4+React18搭建桌面端项目|electron多开窗口实践
前段时间有分享一篇electron25+vite4搭建跨桌面端vue3应用实践。今天带来最新捣鼓的electron27+react18创建跨端程序、electron多开窗体(模拟QQ登录窗口切换主窗口)、自定义无边框窗口拖拽导航栏的一些知识分享。

electron团队更新迭代比较快,目前稳定版本已经升级到了electron27。而且运行及构建速度有了大幅度的提升!

版本信息
"vite": "^4.4.5"
"react": "^18.2.0"
"electron": "^27.0.1"
"electron-builder": "^24.6.4"
"vite-plugin-electron": "^0.14.1"
搭建react18项目
使用vite4构建工具创建react项目模板。
yarn create vite electron-vite4-react18
# 选择创建react模板
cd electron-vite4-react18
yarn install
yarn dev

至此,一个基础的react模板项目就创建好了。接下来就是安装electron相关的依赖配置。
安装electron关联依赖包
注意:如果安装出现卡顿情况,建议设置淘宝镜像源。
// 安装electron
yarn add -D electron
// 安装electron-builder 用于构建打包可安装exe程序
yarn add -D electron-builder
// 安装electron-devtools-installer 用于开发调试electron项目
yarn add -D electron-devtools-installer
另外还需要安装一个electron和vite的桥接插件vite-plugin-electron。
yarn add -D vite-plugin-electron
vite-plugin-electron:一款快速集成整合Vite和Electron,方便在渲染进程中使用Node API或Electron API功能。
到这里,所依赖的electron插件已经安装完毕。接下来就是创建主进程,启动项目了。
创建主进程配置

const { app, BrowserWindow } = require('electron')
const MultiWindow = require('./src/windows')
const createWindow = () => {
let win = new MultiWindow()
win.createWin({ isMainWin: true })
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
配置vite.config.js
在vite.config.js中引入vite-plugin-electron配置主进程入口electron-main.js文件。
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import electron from 'vite-plugin-electron'
import { resolve } from 'path'
import { parseEnv } from './src/utils/env'
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
const viteEnv = loadEnv(mode, process.cwd())
const env = parseEnv(viteEnv)
return {
plugins: [
react(),
electron({
entry: 'electron-main.js',
})
],
esbuild: {
// 打包去除 console.log 和 debugger
drop: env.VITE_DROP_CONSOLE && command === 'build' ? ["console", "debugger"] : []
},
/* 开发服务器配置 */
server: {
// 端口
port: env.VITE_PORT,
// 代理配置
proxy: {
// ...
}
},
resolve: {
// 设置别名
alias: {
'@': resolve(__dirname, 'src'),
'@assets': resolve(__dirname, 'src/assets'),
'@components': resolve(__dirname, 'src/components'),
'@views': resolve(__dirname, 'src/views')
}
}
}
})
配置package.json
在package.json文件中加入 "main": "electron-main.js" 入口配置,并且需要去掉 "type": "module" 。
注意:官方提示electron28之后可以支持"type": "module"
接下来就运行yarn electron:serve桌面端项目就能运行了。

至于一些electron打包配置,这里就不详细介绍了,之前有相关分享的文章。
https://www.cnblogs.com/xiaoyan2017/p/17436076.html

electron自定义无边框拖拽导航栏

创建窗口的时候设置 frame: false 即可创建一个无系统边框的窗体。

通过css设置 -webkit-app-region: drag 来实现拖拽区域。设置 -webkit-app-region: no-drag 取消拖拽响应。
自定义最大化/最小化/关闭
import { useState, useContext } from 'react'
import { Modal } from '@arco-design/web-react'
import { setWin } from '@/windows/action'
function WinBtn(props) {
const {
color = '#fff',
minimizable = true,
maximizable = true,
closable = true,
zIndex = 2023,
children
} = props
const [hasMaximized, setHasMaximized] = useState(false)
window.electronAPI.invoke('win__isMaximized').then(res => {
setHasMaximized(res)
})
window.electronAPI.receive('win__hasMaximized', (e, res) => {
setHasMaximized(res)
})
// 最小化
const handleWinMin = () => {
window.electronAPI.send("win__minimize")
}
// 最大化/还原
const handleWinMax2Min = () => {
window.electronAPI.invoke("win__max2min").then(res => {
console.log(res)
setHasMaximized(res)
})
}
// 关闭
const handleWinClose = () => {
if(window.config.isMainWin) {
Modal.confirm({
title: '提示',
content: <div style={{ textAlign: 'center' }}>是否最小化至托盘,不退出程序?</div>,
okButtonProps: {status: 'warning'},
style: {width: 360},
cancelText: '最小化至托盘',
okText: '残忍退出',
onOk: () => {
setWin('close')
},
onCancel: () => {
setWin('hide', window.config.id)
}
})
}else {
setWin('close', window.config.id)
}
}
return (
<>
<div className="vui__macbtn flexbox flex-alignc" style={{zIndex: zIndex}}>
<div className="vui__macbtn-groups flexbox flex-alignc" style={{color: color}}>
{ JSON.parse(minimizable) && <a className="mbtn min" title="最小化" onClick={handleWinMin}><svg x="0" y="0" width="10" height="10" viewBox="0 0 10 10"><path fill="#995700" d="M8.048,4.001c0.163,0.012 0.318,0.054 0.459,0.137c0.325,0.191 0.518,0.559 0.49,0.934c-0.007,0.097 -0.028,0.192 -0.062,0.283c-0.04,0.105 -0.098,0.204 -0.171,0.29c-0.083,0.098 -0.185,0.181 -0.299,0.24c-0.131,0.069 -0.271,0.103 -0.417,0.114c-2.031,0.049 -4.065,0.049 -6.096,0c-0.163,-0.012 -0.318,-0.054 -0.459,-0.137c-0.325,-0.191 -0.518,-0.559 -0.49,-0.934c0.007,-0.097 0.028,-0.192 0.062,-0.283c0.04,-0.105 0.098,-0.204 0.171,-0.29c0.083,-0.098 0.185,-0.181 0.299,-0.24c0.131,-0.069 0.271,-0.103 0.417,-0.114c2.031,-0.049 4.065,-0.049 6.096,0Z"></path></svg></a> }
{ JSON.parse(maximizable) &&
<a className="mbtn max" title={hasMaximized ? '向下还原' : '最大化'} onClick={handleWinMax2Min}>
{
hasMaximized ?
<svg x="0" y="0" width="10" height="10" viewBox="0 0 10 10"><path fill="#4d0000" d="M5,10c0,0 0,-2.744 0,-4.167c0,-0.221 -0.088,-0.433 -0.244,-0.589c-0.156,-0.156 -0.368,-0.244 -0.589,-0.244c-1.423,0 -4.167,0 -4.167,0l5,5Z"></path><path fill="#006400" d="M5,0c0,0 0,2.744 0,4.167c0,0.221 0.088,0.433 0.244,0.589c0.156,0.156 0.368,0.244 0.589,0.244c1.423,0 4.167,0 4.167,0l-5,-5Z"></path></svg>
:
<svg x="0" y="0" width="10" height="10" viewBox="0 0 10 10"><path fill="#4d0000" d="M2,3c0,0 0,2.744 0,4.167c0,0.221 0.088,0.433 0.244,0.589c0.156,0.156 0.368,0.244 0.589,0.244c1.423,0 4.167,0 4.167,0l-5,-5Z"></path><path fill="#006400" d="M8,7c0,0 0,-2.744 0,-4.167c0,-0.221 -0.088,-0.433 -0.244,-0.589c-0.156,-0.156 -0.368,-0.244 -0.589,-0.244c-1.423,0 -4.167,0 -4.167,0l5,5Z"></path></svg>
}
</a>
}
{ JSON.parse(closable) && <a className="mbtn close" title="关闭" onClick={handleWinClose}><svg x="0" y="0" width="10" height="10" viewBox="0 0 10 10"><path fill="#4d0000" d="M5,3.552c0.438,-0.432 0.878,-0.861 1.322,-1.287c0.049,-0.044 0.101,-0.085 0.158,-0.119c0.149,-0.091 0.316,-0.137 0.49,-0.146c0.04,0 0.04,0 0.08,0.001c0.16,0.011 0.314,0.054 0.453,0.135c0.08,0.046 0.154,0.104 0.218,0.171c0.252,0.262 0.342,0.65 0.232,0.996c-0.045,0.141 -0.121,0.265 -0.218,0.375c-0.426,0.444 -0.855,0.884 -1.287,1.322c0.432,0.438 0.861,0.878 1.287,1.322c0.097,0.11 0.173,0.234 0.218,0.375c0.04,0.126 0.055,0.26 0.043,0.392c-0.011,0.119 -0.043,0.236 -0.094,0.344c-0.158,0.327 -0.49,0.548 -0.852,0.566c-0.106,0.005 -0.213,-0.007 -0.315,-0.035c-0.156,-0.043 -0.293,-0.123 -0.413,-0.229c-0.444,-0.426 -0.884,-0.855 -1.322,-1.287c-0.438,0.432 -0.878,0.861 -1.322,1.287c-0.11,0.097 -0.234,0.173 -0.375,0.218c-0.126,0.04 -0.26,0.055 -0.392,0.043c-0.119,-0.011 -0.236,-0.043 -0.344,-0.094c-0.327,-0.158 -0.548,-0.49 -0.566,-0.852c-0.005,-0.106 0.007,-0.213 0.035,-0.315c0.043,-0.156 0.123,-0.293 0.229,-0.413c0.426,-0.444 0.855,-0.884 1.287,-1.322c-0.432,-0.438 -0.861,-0.878 -1.287,-1.322c-0.106,-0.12 -0.186,-0.257 -0.229,-0.413c-0.025,-0.089 -0.037,-0.182 -0.036,-0.275c0.004,-0.363 0.211,-0.704 0.532,-0.874c0.13,-0.069 0.272,-0.105 0.418,-0.115c0.04,-0.001 0.04,-0.001 0.08,-0.001c0.174,0.009 0.341,0.055 0.49,0.146c0.057,0.034 0.109,0.075 0.158,0.119c0.444,0.426 0.884,0.855 1.322,1.287Z"></path></svg></a> }
<i className="mr-10"></i>
{ children }
</div>
<div className="vui__mactitle">{window.config.title || '首页'}</div>
</div>
</>
)
}
export default WinBtn
electron自定义托盘图标/托盘闪烁

/**
* Electron多窗口管理器
* @author Andy Q:282310962
*/ const { app, BrowserWindow, ipcMain, Menu, Tray, dialog, globalShortcut } = require('electron')
// const { loadEnv } = require('vite')
const { join } = require('path') // 根目录路径
process.env.ROOT = join(__dirname, '../../') const isDev = process.env.NODE_ENV === 'development'
// const winURL = isDev ? 'http://localhost:3000/' : join(__dirname, 'dist/index.html')
const winURL = isDev ? process.env.VITE_DEV_SERVER_URL : join(process.env.ROOT, 'dist/index.html') class MultiWindow {
constructor() {
// 主窗口对象
this.main = null
// 窗口组
this.group = {} // 托盘图标
this.tray = null
this.flashTimer = null
this.trayIco1 = join(process.env.ROOT, 'resource/tray.ico')
this.trayIco2 = join(process.env.ROOT, 'resource/tray-empty.ico') // 监听ipcMain事件
this.listenIpc() // 创建系统托盘
this.createTray()
} // 系统配置参数
winOptions() {
return {
// 窗口图标
icon: join(process.env.ROOT, 'resource/shortcut.ico'),
backgroundColor: '#fff',
autoHideMenuBar: true,
titleBarStyle: 'hidden',
width: 900,
height: 600,
resizable: true,
minimizable: true,
maximizable: true,
frame: false, // 设置为 false 时可以创建一个无边框窗口 默认值为 true
show: false, // 窗口是否在创建时显示
webPreferences: {
contextIsolation: true, // 启用上下文隔离(为了安全性)(默认true)
nodeIntegration: false, // 启用Node集成(默认false)
preload: join(process.env.ROOT, 'electron-preload.js')
}
}
} // 创建新窗口
createWin(options) {
// ...
} // ... // 主进程监听事件
listenIpc() {
// 创建新窗体
ipcMain.on('win-create', (event, args) => this.createWin(args)) // ... // 托盘图标闪烁
ipcMain.on('win__flashTray', (event, bool) => this.flashTray(bool)) // 屏幕截图
ipcMain.on('win__setCapture', () => {
// ...
})
} // 创建系统托盘图标
createTray() {
console.log(__dirname)
console.log(join(process.env.ROOT, 'resource/tray.ico')) const trayMenu = Menu.buildFromTemplate([
{
label: '打开主界面',
icon: join(process.env.ROOT, 'resource/home.png'),
click: () => {
try {
for(let i in this.group) {
let win = this.getWin(i)
if(!win) return
// 是否主窗口
if(this.group[i].isMainWin) {
if(win.isMinimized()) win.restore()
win.show()
}
}
} catch (error) {
console.log(error)
}
}
},
{
label: '设置中心',
icon: join(process.env.ROOT, 'resource/setting.png'),
click: () => {
for(let i in this.group) {
let win = this.getWin(i)
if(win) win.webContents.send('win__ipcData', { type: 'CREATE_WIN_SETTING', value: null })
}
},
},
{
label: '锁屏',
click: () => null,
},
{
label: '关闭托盘闪烁',
click: () => {
this.flashTray(false)
}
},
{type: 'separator'},
/* {
label: '重启',
click: () => {
// app.relaunch({ args: process.argv.slice(1).concat(['--relaunch']) })
// app.exit(0) app.relaunch()
app.quit()
}
}, */
{
label: '关于',
click: () => {
for(let i in this.group) {
let win = this.getWin(i)
if(win) win.webContents.send('win__ipcData', { type: 'CREATE_WIN_ABOUT', value: null })
}
}
},
{
label: '关闭应用并退出',
icon: join(process.env.ROOT, 'resource/logout.png'),
click: () => {
dialog.showMessageBox(this.main, {
title: '询问',
message: '确定要退出应用程序吗?',
buttons: ['取消', '最小化托盘', '退出应用'],
type: 'error',
noLink: false, // true传统按钮样式 false链接样式
cancelId: 0
}).then(res => {
console.log(res) const index = res.response
if(index == 0) {
console.log('取消')
}if(index == 1) {
console.log('最小化托盘')
for(let i in this.group) {
let win = this.getWin(i)
if(win) win.hide()
}
}else if(index == 2) {
console.log('退出应用') try {
for(let i in this.group) {
let win = this.getWin(i)
if(win) win.webContents.send('win__ipcData', { type: 'WIN_LOGOUT', value: null })
}
// app.quit 和 app.exit(0) 都可退出应用。
// 前者可以被打断并触发一些事件,而后者将强制应用程序退出而不触发任何事件或允许应用程序取消操作。
app.quit()
} catch (error) {
console.log(error)
}
}
})
}
}
])
this.tray = new Tray(this.trayIco1)
this.tray.setContextMenu(trayMenu)
this.tray.setToolTip(app.name)
this.tray.on('double-click', () => {
console.log('double clicked')
}) // 开启托盘闪烁
// this.flashTray(true)
} // 托盘图标闪烁
flashTray(flash) {
let hasIco = false if(flash) {
if(this.flashTimer) return
this.flashTimer = setInterval(() => {
this.tray.setImage(hasIco ? this.trayIco1 : this.trayIco2)
hasIco = !hasIco
}, 500)
}else {
if(this.flashTimer) {
clearInterval(this.flashTimer)
this.flashTimer = null
}
this.tray.setImage(this.trayIco1)
}
} // 销毁托盘图标
destoryTray() {
this.flashTray(false)
this.tray.destroy()
this.tray = null
}
} module.exports = MultiWindow
electron支持的一些环境变量。
process.env.NODE_ENV
process.env.VITE_DEV_SERVER_URL

另外需要注意,__dirname变量指向当前文件。如:src/windows/index.js文件,则根目录需要 ../../ 返回。
Okay,以上就是electron27+react18+vite4搭建桌面端实践项目的一些分享知识,希望对大家有所帮助哈!

基于Electron27+Vite4+React18搭建桌面端项目|electron多开窗口实践的更多相关文章
- 基于 intellij IDEA 快速搭建Spring Boot项目
在<一步步搭建 Spring Boot maven 框架的工程>一文中,已经介绍了如何使用Eclipse快速搭建Spring Boot项目.由于最近将开发工具由Eclipse ...
- vue脚手架搭建移动端项目--flexible.js
通过命令行 node -v 查看是否安装node环境 在 nodejs 和 webpack已安装的前提下,随便一个文件夹下,输入命令行 npm install vue-cli -g 安装完成后,通过 ...
- 【angularjs】使用ionic+angular 搭建移动端项目,字体适配
解析: 首先,rem是以html为基准. 一般的,各大主流浏览器的font-size默认值为16px,此时1rem=16px.如果此时将rem与px进行换算很麻烦,比如0.75rem=12px. 为了 ...
- 基于Vue2.x的小米商城移动端项目
初学vue已经有一段时间,为了检验自己的学习成果,决定做一个项目作为一个阶段性总结,项目花了差不多半个月时间,目前实现了7个页面,商城的主要功能基本实现,代码已经放到github上面. 这个项目把大部 ...
- 【angularjs】使用angular搭建PC端项目,开关按钮
方法一(使用指令) 1.指令(angular-ui-switch.js) angular.module('uiSwitch', []) app.directive('switch', function ...
- 【web】使用ionic搭建移动端项目 icon-radio 标签在ios下全部选中的问题
这块css 导致的问题 .disable-pointer-events { pointer-events: none; }
- vue3系列:vue3.0自定义全局弹层V3Layer|vue3.x pc桌面端弹窗组件
基于Vue3.0开发PC桌面端自定义对话框组件V3Layer. 前两天有分享一个vue3.0移动端弹出层组件,今天分享的是最新开发的vue3.0版pc端弹窗组件. V3Layer 一款使用vue3.0 ...
- day97:MoFang:移动端APP开发准备&移动端项目搭建&APICloud前端框架
目录 1.移动端开发相关概念 1.APP类型 2.移动端屏幕介绍 3.移动端自适配方案 4.元信息(meta) 2.APP开发准备 1.注册APPCLoud账号 2.下载APP开发编辑器 3.下载AP ...
- 【vue3-element-admin 】基于 Vue3 + Vite4 + TypeScript + Element-Plus 从0到1搭建后台管理系统(前后端开源@有来开源组织)
vue3-element-admin 是基于 vue-element-admin 升级的 Vue3 + Element Plus 版本的后台管理前端解决方案,技术栈为 Vue3 + Vite4 + T ...
- 基于Vue2 搭建移动端 webapp 框架
Vue.js2.0作为国内热门并广为人知的前端框架,其与其他主流框架的优势在此不做过多赘述.搭建框架步骤如下: 转自:http://www.jianshu.com/p/beae26e57b0f 安装N ...
随机推荐
- Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics
在Delta Lake官网上提到的一篇新一代湖仓架构的论文. 这篇论文由Databricks团队2021年发表于CIDR会议. 这个会议是对sigmod和vldb会议的补充. 可以看到这篇论文和前一篇 ...
- Python 运行 shell 命令的一些方法
哈喽大家好,我是咸鱼 我们知道,python 在自动化领域中被广泛应用,可以很好地自动化处理一些任务 就比如编写 Python 脚本自动化执行重复性的任务,如文件处理.数据处理.系统管理等需要运行其他 ...
- PostgreSQL+GeoHash地图点位聚合
PG数据库安装扩展 需要用到pg数据库的空间扩展postgis,在进行操作之前需要在数据库中安装扩展. CREATE EXTENSION postgis; CREATE EXTENSION postg ...
- python:map函数
参考示例 def test(x): return x * 2 mylist = [1, 2, 3, 4, 5] result = list(map(test, mylist)) print(resul ...
- Django CSRF cookie not set.
错误原因 由于django框架的settings.py配置了中间件,为了防止跨站请求伪造,form表单POST方式会导致出现报错 解决办法: 将'django.middleware.csrf.Csrf ...
- 是时候丢掉BeanUtils了
前言 为了更好的进行开发和维护,我们都会对程序进行分层设计,例如常见的三层,四层,每层各司其职,相互配合.也随着分层,出现了VO,BO,PO,DTO,每层都会处理自己的数据对象,然后向上传递,这就避免 ...
- 重返照片的原始世界:我为.NET打造的RAW照片解析利器
重返照片的原始世界:我为.NET打造的RAW照片解析利器 如果你是我的老读者,你可能还记得,在2019年,我冒险进入了一片神秘的领域--用C#解析RAW格式的照片: 20191208 - 用.NET解 ...
- 青少年CTF平台-Web-Robots
题目信息 题目名称:Robots 题目描述:昨天十三年社团讲课,讲了Robots.txt的作用,小刚上课没有认真听课正在着急,你能不能帮帮忙? 题目难度:一颗星 解题过程 访问题目链接 在这里插入图片 ...
- 青少年CTF平台 Web签到
题目说明 Web一星简单题,Web签到. 直接启动环境,等待30秒左右访问题目链接. 做题过程 进入后,题目好像没有告诉我们什么有用的信息, F12,看遍了题目源码,也没有发现flag,正当我怀疑这个 ...
- [postgresql]逻辑备份与还原
前言 Postgres提供pg_dump和pg_dumpall用于数据库逻辑备份. pg_dumpall:将一个PostgreSQL数据库集群全部转储到一个脚本文件中 pg_dump:可以选择一个数据 ...