Windows下Electron桌面待办应用开发实践:从环境配置到IPC
Electron 在 Windows 桌面开发中一直有稳定的使用场景:它把 Chromium 渲染引擎和 Node.js 运行时打包进应用,让开发者能用 HTML、CSS 和 JavaScript 写桌面软件,同时通过主进程调用文件系统、窗口、菜单、通知等系统能力。对于第一次接触 Electron 的开发者,容易陷入一个误区:一上来就引入 React、Vue 或 TypeScript,结果连主进程、渲染进程、预加载脚本之间的关系都没搞清楚。下面这篇实践,用一种更朴素的方式——原生三件套加 Electron 官方 API,在 Windows 上从零完成一个带本地持久化能力的桌面待办应用。整个开发过程基于 Windows + PowerShell + VS Code,macOS 和 Linux 的代码基本相同,差别主要在于安装命令、打包格式和系统路径。
开发环境准备
Electron 开发要求本机先有 Node.js 和 npm。打开 Node.js 官方下载页面,选择当前的 LTS 版本,不要选奇数版本或标注为 Current 的版本。安装完成后在 PowerShell 中执行以下命令验证:
node -v
npm -v
能看到版本号即可。注意,Electron 应用运行时并不依赖系统安装的 Node.js,它只服务于开发和构建阶段,普通用户运行打包后的应用时不会用到这个环境。
Git 建议同时安装。它虽不是运行 Electron 的硬性要求,但安装依赖、使用模板和版本管理都会用到。安装后同样用 git --version 验证。
编辑器推荐 VS Code,自带的 JavaScript 和 TypeScript 语言特性已经足够支撑当前项目。ESLint 和 Prettier 后续需要代码检查或统一格式时再装,第一次入门不需要挂一堆 Electron 专用插件。
项目初始化和 Electron 安装
在 PowerShell 中创建项目目录,路径尽量使用英文、数字和短横线,避免空格、中文或过深的层级。Windows 下路径太复杂时,npm 和打包工具给出的报错会很不直观。
mkdir D:\electron-projects
cd D:\electron-projects
mkdir electron-todo
cd electron-todo
npm init -y
npm init -y 会生成 package.json,这是项目的配置文件,记录入口文件、命令和依赖。Electron 只在开发和构建阶段需要,因此作为开发依赖安装:
npm install --save-dev electron
安装过程中除了下载 npm 包,还会下载对应平台的 Electron 二进制文件,第一次安装比普通 npm 包慢是正常现象。如果网络较慢,先确认 PowerShell 能否正常访问 npm registry。
安装完成后修改 package.json,将 main 字段指向 main.js,并添加 start 脚本:
{
"name": "electron-todo",
"version": "1.0.0",
"description": "一个简单的 Electron 桌面待办应用",
"author": "你的名字或团队名称",
"license": "MIT",
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
main 表示 Electron 启动后首先执行 main.js;npm start 实际运行的是 electron .,点号表示把当前目录作为 Electron 应用启动。
先跑出一个最小窗口
在项目根目录创建 main.js 和 index.html。主进程代码负责创建窗口:
const { app, BrowserWindow } = require('electron/main')
const path = require('node:path')
function createWindow() {
const mainWindow = new BrowserWindow({
width: 900,
height: 650,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
这段代码完成了四件事:引入 app 和 BrowserWindow;创建 900x650 的窗口;加载根目录的 index.html;在应用 ready 后创建窗口。Windows 和 Linux 下关闭所有窗口后进程退出,macOS 通常保留应用进程。contextIsolation、nodeIntegration 和 sandbox 这几个安全相关配置先按默认值保留,不要为了某个功能直接放开渲染进程的 Node 权限。
index.html 先做最简单的页面:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>桌面待办</title>
</head>
<body>
<h1>桌面待办</h1>
<p>Electron 窗口已经启动。</p>
</body>
</html>
在项目根目录执行 npm start,如果弹出桌面窗口,说明 Electron 环境已经跑通。关闭窗口后,PowerShell 中的应用进程也会结束。终端里出现的主进程日志不要当成页面内容,它们属于开发者输出。
三进程模型与代码职责
一个完整的 Electron 待办项目,文件划分应该明确:main.js 负责主进程,preload.js 负责预加载脚本,index.html 和 renderer.js 属于渲染进程,style.css 负责样式。
主进程是整个应用的入口,只有一个实例,管理应用生命周期、创建窗口、调用系统能力。它可以使用 Node.js API 和 Electron 的窗口、菜单、对话框、文件路径等能力。
每个 BrowserWindow 对应一个渲染进程,本质就是页面本身。渲染进程应严格按照浏览器页面方式开发,不要在 renderer.js 中直接 require('fs'),也不要试图直接读取电脑文件。
预加载脚本在页面加载前执行,它可以接触部分 Electron 能力,再通过 contextBridge 向页面暴露明确、安全的接口。不要直接把完整的 ipcRenderer 暴露给页面,例如下面的写法是反模式:
// 错误示例,不要使用
contextBridge.exposeInMainWorld('electron', {
ipcRenderer,
})
正确做法是只暴露当前页面真正需要的函数。本文的待办应用会暴露 loadTodos、saveTodos 和 getAppVersion 三个函数。
带本地持久化的待办应用
接下来把空窗口升级为能真正使用的应用,任务数据保存到 Electron 的 userData 目录,重启后依然存在。
先修改 main.js,引入 ipcMain 和文件系统模块,实现读写 todos.json 的逻辑:
const { app, BrowserWindow, ipcMain } = require('electron/main')
const fs = require('node:fs/promises')
const path = require('node:path')
function getTodoFilePath() {
return path.join(app.getPath('userData'), 'todos.json')
}
async function loadTodos() {
try {
const content = await fs.readFile(getTodoFilePath(), 'utf8')
const todos = JSON.parse(content)
return Array.isArray(todos) ? todos : []
} catch (error) {
if (error.code === 'ENOENT') {
return []
}
throw error
}
}
async function saveTodos(todos) {
if (!Array.isArray(todos)) {
throw new TypeError('待办数据必须是数组')
}
const filePath = getTodoFilePath()
await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, JSON.stringify(todos, null, 2), 'utf8')
}
todos.json 不放在项目目录,而是存放在 app.getPath('userData') 返回的路径中。这是桌面应用保存用户数据的标准做法,开发环境和打包后的应用都能获得正确的路径,不会受安装目录只读权限影响。
在 app.whenReady 回调中注册 IPC 处理器:
app.whenReady().then(() => {
ipcMain.handle('todos:load', () => loadTodos())
ipcMain.handle('todos:save', (_event, todos) => saveTodos(todos))
ipcMain.handle('app:get-version', () => app.getVersion())
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
接着创建 preload.js,通过 contextBridge 封装 IPC 调用:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
loadTodos: () => ipcRenderer.invoke('todos:load'),
saveTodos: (todos) => ipcRenderer.invoke('todos:save', todos),
getAppVersion: () => ipcRenderer.invoke('app:get-version'),
})
页面渲染部分沿用普通 DOM 操作思路。index.html 包含表单、任务列表、统计信息等元素,renderer.js 负责处理新增、完成、删除任务,每次变更后调用 window.electronAPI.saveTodos(todos) 持久化数据。核心交互逻辑如下:
const todoForm = document.querySelector('#todoForm')
const todoInput = document.querySelector('#todoInput')
const todoList = document.querySelector('#todoList')
const todoCount = document.querySelector('#todoCount')
const emptyText = document.querySelector('#emptyText')
const statusText = document.querySelector('#statusText')
const clearCompletedButton = document.querySelector('#clearCompleted')
const appVersion = document.querySelector('#appVersion')
let todos = []
async function persistTodos() {
try {
await window.electronAPI.saveTodos(todos)
showStatus('已保存')
} catch (error) {
console.error(error)
showStatus('保存失败,请查看控制台')
}
}
function renderTodos() {
todoList.replaceChildren()
todos.forEach((todo) => {
const item = document.createElement('li')
item.className = 'todo-item'
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = todo.done
checkbox.addEventListener('change', async () => {
todo.done = checkbox.checked
renderTodos()
await persistTodos()
})
const title = document.createElement('span')
title.className = `todo-title${todo.done ? ' done' : ''}`
title.textContent = todo.title
const deleteButton = document.createElement('button')
deleteButton.className = 'delete-button'
deleteButton.type = 'button'
deleteButton.textContent = '删除'
deleteButton.addEventListener('click', async () => {
todos = todos.filter((currentTodo) => currentTodo.id !== todo.id)
renderTodos()
await persistTodos()
})
item.append(checkbox, title, deleteButton)
todoList.append(item)
})
const unfinishedCount = todos.filter((todo) => !todo.done).length
todoCount.textContent = `共 ${todos.length} 项,未完成 ${unfinishedCount} 项`
emptyText.hidden = todos.length > 0
}
todoForm.addEventListener('submit', async (event) => {
event.preventDefault()
const title = todoInput.value.trim()
if (!title) {
showStatus('请输入待办事项')
todoInput.focus()
return
}
todos.unshift({ id: Date.now().toString(), title, done: false })
todoInput.value = ''
renderTodos()
await persistTodos()
todoInput.focus()
})
页面加载时调用 window.electronAPI.loadTodos() 读取本地数据,再通过 getAppVersion() 显示版本号,这两个异步调用放在初始化函数中即可。
关键实践总结
从这次工作流可以提炼出几条适用于 Windows 桌面开发的经验:
1. 安全默认值不要动。contextIsolation: true、nodeIntegration: false、sandbox: true 是 Electron 官方推荐的安全基线。渲染进程被设计为不可信页面,如果所有页面代码都能直接访问 Node.js,任何一处 XSS 都可能升级为系统命令执行。
2. 数据持久化路径使用 app.getPath('userData')。这个路径由系统统一管理,在 Windows 上类似 %APPDATA%\electron-todo,开发环境和打包后都能正确解析,且不会污染项目目录。
3. IPC 接口要收敛。主进程只注册应用真正需要的 handler,预加载脚本只暴露必要的函数,不把 ipcRenderer 整体暴露出去,这是隔离边界的基本要求。
4. 渲染进程按浏览器页面方式开发,不要设想它能直接 require 模块。保持这种心智模型,后续迁移到 Web 端或对接 Electron 更高级能力时才不会混乱。
5. 调试时区分进程。主进程的日志出现在启动它的终端中,渲染进程的日志可以在开发者工具中查看。通过 console.error 和状态提示结合的方式,能快速定位保存失败等问题。
这个例子虽然简单,却完整覆盖了 Electron 应用的核心链路:环境搭建、进程分工、安全配置、IPC 通信和本地数据持久化。掌握这些基础后,再按需引入 React、Vue 或 TypeScript,理解起来会顺畅得多。
Re: Windows下Electron桌面待办应用开发实践:从环境配置到IPC
看了楼主的分享,正好最近也在折腾 Electron,这篇从零开始讲得很清楚。特别是强调“先搞清楚主进程、渲染进程、预加载脚本之间的关系”这点太对了,很多新手教程上来就套框架,反而把核心概念绕晕了。 有几个细节想跟楼主确认一下:`sandbox: true` 的情况下,preload 脚本里如果用 `require` 引入 `contextBridge` 和 `ipcRenderer`,是不是需要额外配置?之前试过在 sandbox 开启时 preload 里不能直接用 Node 的 `path` 模块,得用 `require('electron')` 提供的 API。另外,楼主后续会讲到 IPC 通信的具体写法吗?比如渲染进程怎么安全地调用主进程的持久化读写?期待后面的实践部分。Re: Windows下Electron桌面待办应用开发实践:从环境配置到IPC
感谢楼主分享,这个从零开始的思路很实用。我刚开始接触 Electron 的时候也总想着直接上框架,结果主进程和渲染进程的关系绕得云里雾里的。用原生三件套先跑通流程,对理解 Electron 的架构确实帮助很大。 另外楼主提到 Windows 下路径避免中文和空格这点深有体会,之前项目目录里带了个空格,打包时各种奇怪报错,排查了很久才找到原因。期待后半部分关于 IPC 和持久化的内容,这也是我一直想搞明白的地方。Re: Windows下Electron桌面待办应用开发实践:从环境配置到IPC
楼主的教程写得很清晰,正好最近也在折腾Electron,跟着走了一遍,环境配置和最小窗口的部分确实没遇到什么坑。尤其是“不要一上来就上框架”这个提醒,很认同,官方API先把流程跑通比什么都重要。 有一点想追问:标题里提到了IPC,但正文好像到加载index.html就截断了,后面主进程和渲染进程通信、还有本地持久化(是用electron-store还是直接写文件?)的部分会继续更新吗?期待后续内容。
页:
[1]