Electron-Vite (一)快速构建桌面应用

在前面的章节中,我们讲到了快速构建一个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中,主进程和渲染进程之间的通信,后续我会给大家带来更多实用的小技巧

相关推荐
中微子2 小时前
React 执行阶段与渲染机制详解(基于 React 18+ 官方文档)
前端
唐某人丶2 小时前
教你如何用 JS 实现 Agent 系统(2)—— 开发 ReAct 版本的“深度搜索”
前端·人工智能·aigc
中微子2 小时前
深入剖析 useState产生的 setState的完整执行流程
前端
遂心_2 小时前
JavaScript 函数参数传递机制:一道经典面试题解析
前端·javascript
小徐_23332 小时前
uni-app vue3 也能使用 Echarts?Wot Starter 是这样做的!
前端·uni-app·echarts
RoyLin2 小时前
TypeScript设计模式:适配器模式
前端·后端·node.js
遂心_3 小时前
深入理解 React Hook:useEffect 完全指南
前端·javascript·react.js
Moonbit3 小时前
MoonBit 正式加入 WebAssembly Component Model 官方文档 !
前端·后端·编程语言
龙在天3 小时前
ts中的函数重载
前端