在前面的章节中,我们讲到了快速构建一个Electron的应用,但实际使用起来,对开发者来说,可以说是隔靴搔痒
下面我来将一个新型的构建工具 Electron-Vite
废话不多说,直接开始构建Electron应用
一、安装
1.1 我们随便找一个文件夹,开始创建我们的项目
1.2 然后打开终端,输入指令
sql
npm create @quick-start/electron@latest
输入 y ,继续执行
这里是输入我们的项目名称,这里我就输入了 "study-electron"
这里是选择我们项目的响应式框架,我这里选择了vue框架
这里是选择是否加入TypeScript支持,我选择了是
后面的一些选项,直接选择是即可
1.3 最终会生成这样一个文件夹
进入文件夹,我们会发现文件结构是这个样子
二、运行项目
我们用VS Code打开这个项目
2.1 安装依赖
我们打开VS Code的终端,然后在终端里面输入
css
npm i
等待npm的安装,最后安装成功的界面如下
2.2 运行
我们打开项目里面package.json文件
可以看到electron-vite已经为我们配置了相当多的命令
然后我们在终端里面输入
arduino
npm run dev
我们的电脑桌面就会弹出来这样一个窗口,如下图所示
这个就是我们的Electron应用
相对于原生的Electron创建应用,我们使用的Electron-Vite构建工具要更加的方便,快捷
三、项目文件结构
下面我们来看一下我们的项目文件结构,并一一解释
四、主进程和渲染进程之间通信
我们先认识一下主进程和渲染进程是什么
在 Electron 中,主进程和渲染进程是两大核心概念,分工明确:
- 主进程 :
是应用的入口点(通常对应main.js
),负责管理整个应用的生命周期(启动、关闭等)、创建和控制窗口、处理系统级事件(如菜单、对话框),以及与操作系统交互。
它运行在 Node.js 环境中,拥有完整的系统权限,可访问 Electron 提供的底层 API(如窗口操作、全局快捷键等)。 - 渲染进程 :
每个窗口对应一个独立的渲染进程,负责渲染网页内容(HTML/CSS/JavaScript),处理页面交互(如 DOM 操作、用户输入)。
它运行在 Chromium 浏览器环境中,权限受限(类似普通网页),若需访问系统资源或主进程功能,需通过 IPC(进程间通信)与主进程交互。
打个比方:
在主进程中,我们可以随意的操作文件系统,比如说随意删除D盘的某个文件,获取系统信息,监听端口号等等
但是在渲染进程中,我们无法做到上面的事情,渲染进程就和网页端开发一模一样
如果我们需要在渲染进程里面去操作文件系统,就需要进行主进程渲染进程通信
在项目里面
main文件夹就相当于我们的主进程
renderer文件夹就相当于我们的渲染进程
4.1 清理我们Electron项目
我们先让我们的项目变得干净一些,找到我们的App.vue
删除多余内容,修改成如下内容
找到渲染进程下面的main.ts
删除多余代码,变成下面的样子
最后我们的项目页面,就会是这个样子,一片干净整洁
4.2 主进程向渲染进程发消息
第一步:修改我们主进程下面的index.ts文件
主要是将局部变量mainWindow提取出来,变成全局变量
ts
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
export let mainWindow: BrowserWindow
function createWindow(): void {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// IPC test
ipcMain.on('ping', () => console.log('pong'))
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
第二步:然后在我们的主进程文件夹下面创建一个文件 toRendererMsg.ts
文件内容如下
ts
import { mainWindow } from '.'
export const toMsg = (): void => {
setInterval(() => {
//获取当前的标准时间
const date = new Date()
mainWindow.webContents.send('toMsg', date.toLocaleTimeString())
}, 1000)
}
这个代码的意思是,每隔一秒钟,我就要向渲染进程发一次消息
第三步:然后我们将这个方法放在index.ts里面执行
第四步:回到我们的App.vue里面,编写下面的程序
html
<template>
<span>主进程发过来的消息</span>
<span>{{ msg }}</span>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const msg = ref('')
onMounted(() => {
window.electron.ipcRenderer.on('toMsg', (e, data) => {
msg.value = data
})
})
</script>
在上面的程序里面,我们定义了一个msg的变量,用来接收主进程发过来的消息内容
然后我们在钩子函数里面,定义了监听方法
在主进程里面,我们是通过 toMsg 窗口来发消息的
所以在渲染进程里面,我们就需要通过 监听 toMsg 窗口来获取到消息
接下来我们来运行项目
在终端里面执行
arduino
npm run dev
效果如下,可以看到时间在变化
4.3 渲染进程向主进程通信(单向)
第一步:还是回到我们的App.vue文件里面,将程序替换成下面的内容
html
<template>
<div @click="handleClick">向主进程发消息</div>
</template>
<script setup lang="ts">
const handleClick = (): void => {
window.electron.ipcRenderer.send('toMain', 'hello world')
}
</script>
我们定义了一个div,然后定义了一个点击事件
通过这个点击事件,我们可以直接向主进程发消息
第二步:然后我们回到主进程,在主进程文件夹下面创建一个新的文件 monitorEvent.ts
文件内容如下
ts
import { ipcMain } from 'electron'
export const monitorEvent = (): void => {
ipcMain.on('toMain', (e, data) => {
console.log(data)
})
}
然后将这个方法放入到index.ts里面
第三步:运行项目
我们点击这个文字
在我们的vs code的终端里面就会出现我们预期的结果
4.4 渲染进程向主进程通信(双向)
在4.3 中,我们渲染进程的消息发给了主进程,但是这消息是单程消息,发出去就没有后续了,如果我们想发出消息,并获得处理的结果,就需要双向通信了
第一步:还是修改我们的App.vue
html
<template>
<div @click="handleClick">获取D盘文件目录</div>
<template v-for="(item, index) in fileList" :key="index">
<p>{{ item }}</p>
</template>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const fileList = ref<string[]>([])
const handleClick = async (): Promise<void> => {
const res = await window.electron.ipcRenderer.invoke('getFileList', 'D')
fileList.value = res
}
</script>
第二步:完善一下我们的monitorEvent.ts
改成下面内容
ts
import { ipcMain } from 'electron'
import fs from 'fs/promises'
export const monitorEvent = (): void => {
ipcMain.on('toMain', (e, data) => {
console.log(data)
})
ipcMain.handle('getFileList', async (e, data) => {
if (data === 'D') {
//获取D盘文件列表
const rootPath = 'D:/' // D盘根目录路径
try {
// 读取目录,withFileTypes: true 可以获取文件类型信息
const entries = await fs.readdir(rootPath, { withFileTypes: true })
// 分离文件和目录
const directories = [] as string[]
const files = [] as string[]
for (const entry of entries) {
if (entry.isDirectory()) {
directories.push(entry.name)
} else if (entry.isFile()) {
files.push(entry.name)
}
}
console.log('D盘根目录下的目录:')
console.log(directories)
console.log('\nD盘根目录下的文件:')
console.log(files)
return [...directories, ...files]
} catch (err) {
console.error('读取目录失败:', err)
throw err // 可以根据需要处理错误
}
}
})
}
第三步:点击触发事件
当我们点击这个文字时
渲染进程就会向主进程发消息,然后主进程整理D盘下面所有文件及目录的名称,返回给渲染进程
4.5 渲染进程向主进程通信(双向-同步)
在 4.4 里面,渲染进程向主进程通信时,需要 async/await 处理,那假如说我现在不想要异步通信,想要同步通信,可以做到吗?
这是当然的,不过弊端就在于主进程在处理数据时,会阻碍渲染进程
第一步:这时我们就需要用到预加载(preload)里面的文件了
找到下面的文件
然后修改成下面的内容
ts
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
export const api = {
getFileList: (path: string): string[] => {
return ipcRenderer.sendSync('getFileList', path)
}
}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}
第二步:然后再找到与index.ts相邻的index.d.ts,修改内容如下
第三步:我们回到monitorEvent.ts文件里面
将原有的handle改为on
第四步:回到App.vue里面,修改内容如下
html
<template>
<div @click="handleClick">获取D盘文件目录</div>
<template v-for="(item, index) in fileList" :key="index">
<p>{{ item }}</p>
</template>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const fileList = ref<string[]>([])
const handleClick = (): void => {
const res = window.api.getFileList('D')
fileList.value = res
}
</script>
我们可以看到,handleClick方法原先的async/await 不见了,这表示我们可以进行同步通信
效果就不再展示,大家自行查验
4.6 主进程向渲染进程返回到数据暴露类型
如果大家自己做了一遍4.5的内容,就已经知道主进程向渲染进程返回数据时,怎么暴露数据类型了
如下图所示
我们在预加载ts里面,写了一个方法 getFileList,然后这个方法我们也暴露了返回值的类型,那之后我们在前端接收的时候,就会被告知返回类型是什么
如下图所示,我们可以看到我们用来接收数据的res变量,数据类型是一个string[]
如果我们不通过预加载TS,直接主进程和渲染进程通信,那么我们是无法获取返回值的类型,只能得到一个any类型的返回值
通过预加载TS,可以进行同步通信,也可以获取返回值的类型
五、总结
本章重点讲解electron-vite中,主进程和渲染进程之间的通信,后续我会给大家带来更多实用的小技巧