这篇教程从安装环境开始,带着做一个简单的桌面待办应用。做完以后,可以在 Windows 上打开一个真正的桌面窗口,新增、完成和删除任务,关闭应用后任务也会保存在本地。

教程使用原生 HTML、CSS 和 JavaScript,不引入 React、Vue 或 TypeScript。这样做不是因为这些工具不好,而是第一次学 Electron 时,先把 Electron 自己的运行方式弄明白更重要。

默认环境是 Windows + PowerShell + VS Code。macOS 和 Linux 的代码基本相同,主要差别在安装命令、打包格式和系统路径。

1. Electron 到底是什么

Electron 可以理解成一个“装在桌面窗口里的网页应用”。它把 Chromium 和 Node.js 放到应用里,所以我们可以用熟悉的 HTML、CSS、JavaScript 写桌面软件,同时调用文件系统、窗口、菜单、通知等桌面能力。

一个 Electron 应用最重要的部分有三个:

部分 作用
主进程(Main Process) 管理应用生命周期、创建窗口、调用系统能力
渲染进程(Renderer Process) 显示页面,负责 HTML、CSS 和页面交互
预加载脚本(Preload Script) 在页面加载前准备一组安全的桥接 API

可以先记住这条关系:

Text
页面按钮
  ↓
渲染进程
  ↓ 通过 IPC 通信
预加载脚本
  ↓
主进程
  ↓
文件系统、系统窗口、菜单等能力

不要把所有代码都塞进一个 JavaScript 文件里。页面代码和操作电脑的代码分开,后面排错和维护都会轻松很多。

2. 安装开发环境

2.1 安装 Node.js

打开 Node.js 官方下载页面:

Node.js 官方下载页面

选择当前的 LTS 版本,不要选择奇数版本或标记为 Current 的版本作为第一次开发环境。

安装完成后打开 PowerShell,执行:

PowerShell
node -v
npm -v

能看到版本号就可以了。

Electron 会使用项目依赖中安装的 Electron 运行时,普通用户运行打包好的应用时不需要另外安装 Node.js。Node.js 主要是给开发和构建阶段使用的。

2.2 安装 Git

下载地址:

Git 官方下载页面

安装时使用默认选项即可。检查安装结果:

PowerShell
git --version

Git 不是运行 Electron 的硬性条件,但安装 Electron 依赖和使用很多模板时会用到,建议一起装好。

2.3 安装 VS Code

下载地址:

VS Code 官方下载页面

建议安装的扩展:

  • JavaScript and TypeScript Language Features(VS Code 自带)
  • ESLint(后面需要代码检查时再安装)
  • Prettier - Code formatter(需要统一格式时再安装)

刚开始不需要安装一堆 Electron 专用插件,VS Code 自带的 JavaScript 支持已经够写这个项目了。

3. 创建项目

3.1 创建项目目录

打开 PowerShell,执行:

PowerShell
mkdir D:\electron-projects
cd D:\electron-projects
mkdir electron-todo
cd electron-todo

路径尽量使用英文、数字和短横线,不要一开始就放到带空格、中文或层级很深的目录中。Windows 下路径太复杂时,npm 和打包工具的报错会变得不太直观。

3.2 初始化 npm 项目

PowerShell
npm init -y

执行后会生成 package.json。它是项目的配置文件,记录项目名称、入口文件、命令和依赖。

3.3 安装 Electron

Electron 只在开发和构建阶段使用,所以作为开发依赖安装:

PowerShell
npm install --save-dev electron

安装完成后,项目里会多出 node_modulespackage-lock.json

如果安装很慢,先确认网络可以访问 npm。Electron 安装过程中还会下载对应平台的 Electron 二进制文件,第一次安装比普通 npm 包慢是正常的。

3.4 配置启动命令

打开项目根目录的 package.json,把项目基本信息、mainscripts 改成下面这样。devDependencies 保留 npm 自动生成的内容:

JSON
{
  "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 应用启动”。

4. 先跑出一个空窗口

在项目根目录创建下面三个文件:

Text
electron-todo/
├─ main.js
├─ index.html
├─ package.json
├─ package-lock.json
└─ node_modules/

4.1 编写主进程 main.js

JavaScript
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()
  }
})

这段代码做了几件事:

  1. 引入 appBrowserWindow
  2. 创建一个宽 900、高 650 的窗口。
  3. 让窗口加载根目录的 index.html
  4. 在应用准备好以后再创建窗口。
  5. Windows 和 Linux 关闭所有窗口后退出应用;macOS 通常保留应用进程。

contextIsolationnodeIntegrationsandbox 先保留这个配置。它们和安全有关,不要为了让某段代码方便运行,就直接打开渲染进程的 Node 权限。

4.2 编写页面 index.html

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>

4.3 启动应用

在项目根目录执行:

PowerShell
npm start

如果弹出了一个桌面窗口,说明最基本的 Electron 环境已经成功。

关闭窗口后,PowerShell 中的进程也会结束。终端里如果出现开发日志,不要把它当成页面内容,那些是主进程输出。

5. 项目目录和三个进程

5.1 最终会用到的文件

Text
electron-todo/
├─ main.js       主进程:创建窗口、读写文件
├─ preload.js    预加载脚本:暴露安全 API
├─ index.html    页面结构
├─ renderer.js   渲染进程:页面交互
├─ style.css     页面样式
├─ package.json  npm 和 Electron 配置
└─ package-lock.json

5.2 主进程

主进程只有一个,负责整个应用的生命周期。它可以使用 Node.js API,也可以调用 Electron 的窗口、菜单、对话框、文件路径等能力。

典型代码写在 main.js

JavaScript
const { app, BrowserWindow } = require('electron/main')

5.3 渲染进程

每个 BrowserWindow 都对应一个渲染进程。渲染进程就是页面本身,主要写:

  • HTML
  • CSS
  • 页面 JavaScript

渲染进程应该按照浏览器页面的方式开发。不要在 renderer.js 中直接写 require('fs'),也不要直接读取电脑文件。

5.4 预加载脚本

preload.js 会在页面加载前执行。它可以接触一部分 Electron 能力,再通过 contextBridge 暴露少量、明确的函数给页面。

不要直接把完整的 ipcRenderer 暴露出去,例如下面这种写法不要用:

JavaScript
contextBridge.exposeInMainWorld('electron', {
  ipcRenderer,
})

应该只暴露当前页面真的需要的函数。下面的待办应用会暴露 loadTodossaveTodosgetAppVersion 三个函数。

6. 做一个带本地保存的待办应用

现在把空窗口改成真正能用的待办应用。这个应用的任务会保存到 Electron 的 userData 目录,关闭后再次打开仍然存在。

6.1 修改 main.js

main.js 全部替换成下面的代码:

JavaScript
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')
}

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(() => {
  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()
    }
  })
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

这里的 todos.json 不放在项目目录,而是放到 app.getPath('userData')。这是桌面应用保存用户数据的常用位置,开发环境和打包后都会由 Electron 提供正确路径。

6.2 编写 preload.js

在项目根目录新建 preload.js

JavaScript
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'),
})

这里没有把 ipcRenderer 整个暴露给网页,而是封装成三个明确的函数:

  • loadTodos():让主进程读取任务。
  • saveTodos(todos):让主进程保存任务。
  • getAppVersion():让主进程返回应用版本。

6.3 编写页面 index.html

index.html 替换成:

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>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <main class="app">
      <header class="header">
        <div>
          <p class="eyebrow">ELECTRON TODO</p>
          <h1>我的待办</h1>
          <p class="subtitle">把要做的事情记下来,完成一项就划掉一项。</p>
        </div>
        <span id="appVersion" class="version">加载中...</span>
      </header>

      <form id="todoForm" class="todo-form">
        <input
          id="todoInput"
          type="text"
          placeholder="例如:学习 Electron"
          autocomplete="off"
        />
        <button type="submit">添加</button>
      </form>

      <section class="toolbar">
        <span id="todoCount">共 0 项</span>
        <button id="clearCompleted" class="text-button" type="button">
          清除已完成
        </button>
      </section>

      <ul id="todoList" class="todo-list"></ul>
      <p id="emptyText" class="empty">还没有待办事项</p>
      <p id="statusText" class="status" aria-live="polite"></p>
    </main>

    <script src="renderer.js"></script>
  </body>
</html>

6.4 编写 style.css

CSS
:root {
  color-scheme: light;
  font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
  color: #1f2937;
  background: #f3f4f6;
}

* {
  box-sizing: border-box;
}

body {
  min-width: 560px;
  margin: 0;
}

button,
input {
  font: inherit;
}

button {
  cursor: pointer;
}

.app {
  width: min(760px, calc(100vw - 64px));
  margin: 0 auto;
  padding: 56px 0;
}

.header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 32px;
}

.eyebrow {
  margin: 0 0 8px;
  color: #6366f1;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
}

h1 {
  margin: 0;
  font-size: 42px;
}

.subtitle {
  margin: 12px 0 0;
  color: #6b7280;
}

.version {
  flex-shrink: 0;
  color: #9ca3af;
  font-size: 12px;
}

.todo-form {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
}

.todo-form input {
  min-width: 0;
  flex: 1;
  height: 46px;
  padding: 0 16px;
  border: 1px solid #d1d5db;
  border-radius: 10px;
  outline: none;
  background: #ffffff;
}

.todo-form input:focus {
  border-color: #6366f1;
  box-shadow: 0 0 0 3px #e0e7ff;
}

.todo-form button {
  width: 80px;
  border: 0;
  border-radius: 10px;
  color: #ffffff;
  background: #4f46e5;
}

.todo-form button:hover {
  background: #4338ca;
}

.toolbar {
  display: flex;
  justify-content: space-between;
  margin-bottom: 12px;
  color: #6b7280;
  font-size: 14px;
}

.text-button {
  padding: 0;
  border: 0;
  color: #6366f1;
  background: transparent;
}

.text-button:hover {
  color: #4338ca;
}

.todo-list {
  display: grid;
  gap: 10px;
  padding: 0;
  margin: 0;
  list-style: none;
}

.todo-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  background: #ffffff;
}

.todo-item input {
  width: 18px;
  height: 18px;
  accent-color: #4f46e5;
}

.todo-title {
  min-width: 0;
  flex: 1;
  overflow-wrap: anywhere;
}

.todo-title.done {
  color: #9ca3af;
  text-decoration: line-through;
}

.delete-button {
  border: 0;
  color: #ef4444;
  background: transparent;
}

.empty {
  padding: 44px 0;
  color: #9ca3af;
  text-align: center;
}

.status {
  min-height: 22px;
  margin: 20px 0 0;
  color: #6b7280;
  font-size: 13px;
}

6.5 编写渲染进程 renderer.js

在项目根目录新建 renderer.js

JavaScript
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 = []
let statusTimer

function showStatus(message) {
  statusText.textContent = message
  clearTimeout(statusTimer)

  statusTimer = setTimeout(() => {
    statusText.textContent = ''
  }, 1800)
}

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()
})

clearCompletedButton.addEventListener('click', async () => {
  todos = todos.filter((todo) => !todo.done)
  renderTodos()
  await persistTodos()
})

async function init() {
  try {
    todos = await window.electronAPI.loadTodos()
    appVersion.textContent = `Electron ${await window.electronAPI.getAppVersion()}`
  } catch (error) {
    console.error(error)
    showStatus('读取本地数据失败')
  }

  renderTodos()
}

init()

这里特意使用 textContent 写入用户输入的任务名称,而不是拼接 innerHTML。用户输入永远不要直接当 HTML 插进去,这是一个很值得从第一个项目就养成的习惯。

6.6 重新运行并测试

如果应用还在运行,先回到 PowerShell 按 Ctrl + C 停止,再执行:

PowerShell
npm start

按下面顺序测试:

  1. 添加一条待办。
  2. 勾选它,确认显示删除线。
  3. 点击删除,确认任务消失。
  4. 添加几条任务,关闭应用。
  5. 再次执行 npm start,确认任务还在。
  6. 在输入框中输入带有 <> 的文字,确认页面只显示文字,没有被当成 HTML 执行。

7. 把这段通信串起来看

新增任务时,数据经过的路径是:

Text
renderer.js
  todos.unshift(...)
  ↓
window.electronAPI.saveTodos(todos)
  ↓
preload.js
  ipcRenderer.invoke('todos:save', todos)
  ↓
main.js
  ipcMain.handle('todos:save', ...)
  ↓
fs.writeFile(...)
  ↓
userData/todos.json

读取任务时,方向反过来:

Text
main.js 读取 todos.json
  ↑
ipcMain.handle('todos:load')
  ↑
preload.js 返回 Promise
  ↑
renderer.js 调用 window.electronAPI.loadTodos()

7.1 为什么不让页面直接读文件

渲染进程里的页面可能加载第三方脚本、远程页面或存在 XSS 漏洞。如果直接给页面 Node.js 文件权限,页面一旦被恶意代码控制,就可能读写用户电脑上的文件。

所以当前 Electron 项目通常保持:

JavaScript
const mainWindow = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
  },
})

页面只拿到需要的函数,主进程负责真正的文件操作。

7.2 ipcMain.handleipcRenderer.invoke

这两个 API 是一对:

JavaScript
// 主进程
ipcMain.handle('todos:load', () => loadTodos())
JavaScript
// 预加载脚本
ipcRenderer.invoke('todos:load')

invoke 会返回 Promise,很适合“页面发起请求,主进程处理后返回结果”的场景。

不要在页面里随意创建任意 IPC 通道。通道名称和参数都应该由你自己定义,并且只开放实际需要的操作。

8. 调试 Electron 应用

8.1 查看渲染进程日志

在 Electron 窗口中按:

Text
Ctrl + Shift + I

可以打开 Chromium DevTools。renderer.js 中的 console.log 会出现在这里。

如果快捷键没有反应,可以在 main.jsBrowserWindow 创建后临时加上:

JavaScript
mainWindow.webContents.openDevTools()

调试完成后删掉这行,避免每次启动都自动打开 DevTools。

8.2 查看主进程日志

main.js 中的 console.log 会出现在启动 Electron 的 PowerShell 窗口里:

JavaScript
console.log('数据文件:', getTodoFilePath())

页面控制台和 PowerShell 是两个地方。遇到问题时先判断报错来自哪一边,不要只盯着其中一个控制台。

8.3 常见问题

问题 处理方法
electron 不是命令 确认已经执行 npm install --save-dev electron,并用 npm start 启动
找不到 main.js 检查 package.jsonmain 是否写成 main.js
窗口一闪就没了 查看 PowerShell 中的主进程报错,常见是文件路径或 JavaScript 语法错误
页面是空白的 打开 DevTools,看 Console 和 Network;检查 index.html 引用的文件名
window.electronAPI 是 undefined 检查 preload 路径、文件名和 contextBridge.exposeInMainWorld 代码
数据没有保存 查看主进程报错,确认 ipcMain.handle 的通道名和 invoke 的通道名一致
修改代码不生效 停止后重新执行 npm start,原生 Electron 不会自动热更新
npm 安装失败 先看错误是网络超时还是权限问题,再重试;不要看到一行红字就马上改项目代码

9. 使用 Electron Forge 打包

开发时运行的是源码,别人不能直接拿你的项目目录当成正式软件。要交给别人使用,需要把 Electron、页面文件和依赖打包成安装包或可执行文件。

Electron 本身不负责完整的打包和分发流程,官方教程推荐使用 Electron Forge。

9.1 安装 Forge 并导入项目

先停止正在运行的 Electron 应用,在项目根目录执行:

PowerShell
npm install --save-dev @electron-forge/cli
npx electron-forge import

electron-forge import 会把现有 Electron 项目接入 Forge,通常会:

  • 增加 forge.config.js
  • package.json 中增加 startpackagemake 等脚本。
  • 增加生成不同平台产物所需的 Maker 依赖。

如果提示确认配置,按默认选项继续即可。执行结束后打开 package.json,确认脚本中存在类似下面的内容:

JSON
{
  "scripts": {
    "start": "electron-forge start",
    "package": "electron-forge package",
    "make": "electron-forge make"
  }
}

不要为了“看起来干净”手动删除 Forge 添加的依赖,它们是打包流程需要的。

9.2 先生成打包目录

PowerShell
npm run package

这个命令会把应用和 Electron 运行时放到 out 目录附近,适合先检查打包后的应用能不能启动。

9.3 生成安装包

PowerShell
npm run make

Forge 会先执行打包,再根据 forge.config.js 中的 Maker 生成平台安装包。一般可以在下面的位置找到结果:

Text
out/make/

Windows 下通常会生成安装程序或压缩包,具体文件名和格式取决于当前 Forge 配置以及目标架构。不要把文件名写死到脚本里,直接打开 out/make 查看即可。

9.4 打包后启动失败怎么办

先运行:

PowerShell
npm run package

确认打包目录中的应用能不能启动。如果源码能运行、打包后不能运行,重点检查:

  • main 入口是否正确。
  • preload.js 是否位于项目根目录并被正确引用。
  • 是否把运行时需要的文件写到了未被打包的位置。
  • 是否依赖了只在开发环境存在的绝对路径。
  • forge.config.js 是否被手动改坏。

10. 文件、图标和正式发布前要知道的事

10.1 用户数据不要写进项目目录

开发阶段可以在项目旁边放测试文件,但正式应用应该使用:

JavaScript
app.getPath('userData')

这个路径由 Electron 根据系统和应用名称提供。用户数据、配置文件、缓存等内容放这里,比写死 C:\Users\你的名字\Desktop 更可靠。

10.2 图标需要单独配置

安装包图标不是 index.html 里的图片。需要在 Forge 配置和对应 Maker 中设置图标文件。

Windows、macOS 和 Linux 对图标格式的要求不同。准备正式图标时,先查看 Electron Forge 的 Icon 文档,再按目标平台准备资源。

10.3 代码签名

自己电脑上生成的安装包,可以直接测试。但没有签名的桌面软件,在 Windows 或 macOS 上可能会触发安全提示。

真正发给别人使用时,应当了解目标平台的代码签名和证书要求。签名不是让程序“能不能运行”的功能,而是帮助系统确认程序来自谁、发布后有没有被篡改。

10.4 不要把密钥写进前端代码

下面这些内容不要直接写进 renderer.js 或 HTML:

  • 接口私钥。
  • 数据库密码。
  • 云服务 Secret。
  • 只能由服务端使用的 Token。

打包后的 Electron 应用最终会发到用户电脑上,前端代码和资源都可以被拿到。需要保密的内容应放在服务端。

11. 从这个项目继续扩展

待办应用跑通以后,可以按这个顺序继续:

  1. 增加菜单和系统托盘。
  2. 增加快捷键和窗口置顶。
  3. 使用 dialog 打开文件选择框。
  4. 增加多窗口和窗口间通信。
  5. 把页面替换成 Vue、React 或其他前端框架。
  6. 使用 TypeScript 给 IPC API 加类型。
  7. 配置图标、代码签名和安装包名称。
  8. 接入自动更新。

建议不要一开始就同时引入框架、数据库、自动更新和复杂打包配置。先确保主进程、渲染进程和 IPC 能稳定工作,再逐项增加功能。

12. 最后校对记录

本文于 2026 年 8 月 19 日按 Electron 和 Electron Forge 官方公开文档进行校对,重点检查了这些内容:

  • Electron 官方仍建议使用 Node.js 的 LTS 版本进行开发。
  • Electron 应用的主进程、渲染进程和预加载脚本职责保持分离。
  • 渲染进程不直接使用 Node.js 能力,页面通过预加载脚本和 IPC 调用主进程功能。
  • contextBridge 应该暴露有限的业务函数,不应该把完整的 ipcRenderer 直接暴露给页面。
  • 文件读写使用 app.getPath('userData'),而不是把用户数据写进项目目录。
  • Electron 本身不包含完整的打包分发工具,教程使用 Electron Forge。
  • Forge 的常见流程是 electron-forge importnpm run packagenpm run make
  • 打包结果通常位于 out 目录,正式安装包通常位于 out/make
  • 没有写死 Electron、Node.js 或 Forge 的具体版本号,避免版本升级后命令和内容过时。

13. 参考资料

阅读进度 0%