微软专家 发表于 前天 14:00

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,理解起来会顺畅得多。

热心网友2 发表于 前天 19:00

Re: Windows下Electron桌面待办应用开发实践:从环境配置到IPC

看了楼主的分享,正好最近也在折腾 Electron,这篇从零开始讲得很清楚。特别是强调“先搞清楚主进程、渲染进程、预加载脚本之间的关系”这点太对了,很多新手教程上来就套框架,反而把核心概念绕晕了。 有几个细节想跟楼主确认一下:`sandbox: true` 的情况下,preload 脚本里如果用 `require` 引入 `contextBridge` 和 `ipcRenderer`,是不是需要额外配置?之前试过在 sandbox 开启时 preload 里不能直接用 Node 的 `path` 模块,得用 `require('electron')` 提供的 API。另外,楼主后续会讲到 IPC 通信的具体写法吗?比如渲染进程怎么安全地调用主进程的持久化读写?期待后面的实践部分。

热心网友7 发表于 前天 19:10

Re: Windows下Electron桌面待办应用开发实践:从环境配置到IPC

感谢楼主分享,这个从零开始的思路很实用。我刚开始接触 Electron 的时候也总想着直接上框架,结果主进程和渲染进程的关系绕得云里雾里的。用原生三件套先跑通流程,对理解 Electron 的架构确实帮助很大。 另外楼主提到 Windows 下路径避免中文和空格这点深有体会,之前项目目录里带了个空格,打包时各种奇怪报错,排查了很久才找到原因。期待后半部分关于 IPC 和持久化的内容,这也是我一直想搞明白的地方。

热心网友3 发表于 前天 19:15

Re: Windows下Electron桌面待办应用开发实践:从环境配置到IPC

楼主的教程写得很清晰,正好最近也在折腾Electron,跟着走了一遍,环境配置和最小窗口的部分确实没遇到什么坑。尤其是“不要一上来就上框架”这个提醒,很认同,官方API先把流程跑通比什么都重要。 有一点想追问:标题里提到了IPC,但正文好像到加载index.html就截断了,后面主进程和渲染进程通信、还有本地持久化(是用electron-store还是直接写文件?)的部分会继续更新吗?期待后续内容。
页: [1]
查看完整版本: Windows下Electron桌面待办应用开发实践:从环境配置到IPC