这篇教程从安装环境开始,带着做一个简单的桌面待办应用。做完以后,可以在 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 |
可以先记住这条关系:
页面按钮
↓
渲染进程
↓ 通过 IPC 通信
预加载脚本
↓
主进程
↓
文件系统、系统窗口、菜单等能力
不要把所有代码都塞进一个 JavaScript 文件里。页面代码和操作电脑的代码分开,后面排错和维护都会轻松很多。
2. 安装开发环境
2.1 安装 Node.js
打开 Node.js 官方下载页面:
选择当前的 LTS 版本,不要选择奇数版本或标记为 Current 的版本作为第一次开发环境。
安装完成后打开 PowerShell,执行:
node -v
npm -v
能看到版本号就可以了。
Electron 会使用项目依赖中安装的 Electron 运行时,普通用户运行打包好的应用时不需要另外安装 Node.js。Node.js 主要是给开发和构建阶段使用的。
2.2 安装 Git
下载地址:
安装时使用默认选项即可。检查安装结果:
git --version
Git 不是运行 Electron 的硬性条件,但安装 Electron 依赖和使用很多模板时会用到,建议一起装好。
2.3 安装 VS Code
下载地址:
建议安装的扩展:
- JavaScript and TypeScript Language Features(VS Code 自带)
- ESLint(后面需要代码检查时再安装)
- Prettier - Code formatter(需要统一格式时再安装)
刚开始不需要安装一堆 Electron 专用插件,VS Code 自带的 JavaScript 支持已经够写这个项目了。
3. 创建项目
3.1 创建项目目录
打开 PowerShell,执行:
mkdir D:\electron-projects
cd D:\electron-projects
mkdir electron-todo
cd electron-todo
路径尽量使用英文、数字和短横线,不要一开始就放到带空格、中文或层级很深的目录中。Windows 下路径太复杂时,npm 和打包工具的报错会变得不太直观。
3.2 初始化 npm 项目
npm init -y
执行后会生成 package.json。它是项目的配置文件,记录项目名称、入口文件、命令和依赖。
3.3 安装 Electron
Electron 只在开发和构建阶段使用,所以作为开发依赖安装:
npm install --save-dev electron
安装完成后,项目里会多出 node_modules 和 package-lock.json。
如果安装很慢,先确认网络可以访问 npm。Electron 安装过程中还会下载对应平台的 Electron 二进制文件,第一次安装比普通 npm 包慢是正常的。
3.4 配置启动命令
打开项目根目录的 package.json,把项目基本信息、main 和 scripts 改成下面这样。devDependencies 保留 npm 自动生成的内容:
{
"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. 先跑出一个空窗口
在项目根目录创建下面三个文件:
electron-todo/
├─ main.js
├─ index.html
├─ package.json
├─ package-lock.json
└─ node_modules/
4.1 编写主进程 main.js
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。 - 创建一个宽 900、高 650 的窗口。
- 让窗口加载根目录的
index.html。 - 在应用准备好以后再创建窗口。
- Windows 和 Linux 关闭所有窗口后退出应用;macOS 通常保留应用进程。
contextIsolation、nodeIntegration 和 sandbox 先保留这个配置。它们和安全有关,不要为了让某段代码方便运行,就直接打开渲染进程的 Node 权限。
4.2 编写页面 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>
4.3 启动应用
在项目根目录执行:
npm start
如果弹出了一个桌面窗口,说明最基本的 Electron 环境已经成功。
关闭窗口后,PowerShell 中的进程也会结束。终端里如果出现开发日志,不要把它当成页面内容,那些是主进程输出。
5. 项目目录和三个进程
5.1 最终会用到的文件
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:
const { app, BrowserWindow } = require('electron/main')
5.3 渲染进程
每个 BrowserWindow 都对应一个渲染进程。渲染进程就是页面本身,主要写:
- HTML
- CSS
- 页面 JavaScript
渲染进程应该按照浏览器页面的方式开发。不要在 renderer.js 中直接写 require('fs'),也不要直接读取电脑文件。
5.4 预加载脚本
preload.js 会在页面加载前执行。它可以接触一部分 Electron 能力,再通过 contextBridge 暴露少量、明确的函数给页面。
不要直接把完整的 ipcRenderer 暴露出去,例如下面这种写法不要用:
contextBridge.exposeInMainWorld('electron', {
ipcRenderer,
})
应该只暴露当前页面真的需要的函数。下面的待办应用会暴露 loadTodos、saveTodos 和 getAppVersion 三个函数。
6. 做一个带本地保存的待办应用
现在把空窗口改成真正能用的待办应用。这个应用的任务会保存到 Electron 的 userData 目录,关闭后再次打开仍然存在。
6.1 修改 main.js
把 main.js 全部替换成下面的代码:
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:
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 替换成:
<!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
: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:
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 停止,再执行:
npm start
按下面顺序测试:
- 添加一条待办。
- 勾选它,确认显示删除线。
- 点击删除,确认任务消失。
- 添加几条任务,关闭应用。
- 再次执行
npm start,确认任务还在。 - 在输入框中输入带有
<和>的文字,确认页面只显示文字,没有被当成 HTML 执行。
7. 把这段通信串起来看
新增任务时,数据经过的路径是:
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
读取任务时,方向反过来:
main.js 读取 todos.json
↑
ipcMain.handle('todos:load')
↑
preload.js 返回 Promise
↑
renderer.js 调用 window.electronAPI.loadTodos()
7.1 为什么不让页面直接读文件
渲染进程里的页面可能加载第三方脚本、远程页面或存在 XSS 漏洞。如果直接给页面 Node.js 文件权限,页面一旦被恶意代码控制,就可能读写用户电脑上的文件。
所以当前 Electron 项目通常保持:
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})
页面只拿到需要的函数,主进程负责真正的文件操作。
7.2 ipcMain.handle 和 ipcRenderer.invoke
这两个 API 是一对:
// 主进程
ipcMain.handle('todos:load', () => loadTodos())
// 预加载脚本
ipcRenderer.invoke('todos:load')
invoke 会返回 Promise,很适合“页面发起请求,主进程处理后返回结果”的场景。
不要在页面里随意创建任意 IPC 通道。通道名称和参数都应该由你自己定义,并且只开放实际需要的操作。
8. 调试 Electron 应用
8.1 查看渲染进程日志
在 Electron 窗口中按:
Ctrl + Shift + I
可以打开 Chromium DevTools。renderer.js 中的 console.log 会出现在这里。
如果快捷键没有反应,可以在 main.js 的 BrowserWindow 创建后临时加上:
mainWindow.webContents.openDevTools()
调试完成后删掉这行,避免每次启动都自动打开 DevTools。
8.2 查看主进程日志
main.js 中的 console.log 会出现在启动 Electron 的 PowerShell 窗口里:
console.log('数据文件:', getTodoFilePath())
页面控制台和 PowerShell 是两个地方。遇到问题时先判断报错来自哪一边,不要只盯着其中一个控制台。
8.3 常见问题
| 问题 | 处理方法 |
|---|---|
electron 不是命令 |
确认已经执行 npm install --save-dev electron,并用 npm start 启动 |
找不到 main.js |
检查 package.json 的 main 是否写成 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 应用,在项目根目录执行:
npm install --save-dev @electron-forge/cli
npx electron-forge import
electron-forge import 会把现有 Electron 项目接入 Forge,通常会:
- 增加
forge.config.js。 - 在
package.json中增加start、package和make等脚本。 - 增加生成不同平台产物所需的 Maker 依赖。
如果提示确认配置,按默认选项继续即可。执行结束后打开 package.json,确认脚本中存在类似下面的内容:
{
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make"
}
}
不要为了“看起来干净”手动删除 Forge 添加的依赖,它们是打包流程需要的。
9.2 先生成打包目录
npm run package
这个命令会把应用和 Electron 运行时放到 out 目录附近,适合先检查打包后的应用能不能启动。
9.3 生成安装包
npm run make
Forge 会先执行打包,再根据 forge.config.js 中的 Maker 生成平台安装包。一般可以在下面的位置找到结果:
out/make/
Windows 下通常会生成安装程序或压缩包,具体文件名和格式取决于当前 Forge 配置以及目标架构。不要把文件名写死到脚本里,直接打开 out/make 查看即可。
9.4 打包后启动失败怎么办
先运行:
npm run package
确认打包目录中的应用能不能启动。如果源码能运行、打包后不能运行,重点检查:
main入口是否正确。preload.js是否位于项目根目录并被正确引用。- 是否把运行时需要的文件写到了未被打包的位置。
- 是否依赖了只在开发环境存在的绝对路径。
forge.config.js是否被手动改坏。
10. 文件、图标和正式发布前要知道的事
10.1 用户数据不要写进项目目录
开发阶段可以在项目旁边放测试文件,但正式应用应该使用:
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. 从这个项目继续扩展
待办应用跑通以后,可以按这个顺序继续:
- 增加菜单和系统托盘。
- 增加快捷键和窗口置顶。
- 使用
dialog打开文件选择框。 - 增加多窗口和窗口间通信。
- 把页面替换成 Vue、React 或其他前端框架。
- 使用 TypeScript 给 IPC API 加类型。
- 配置图标、代码签名和安装包名称。
- 接入自动更新。
建议不要一开始就同时引入框架、数据库、自动更新和复杂打包配置。先确保主进程、渲染进程和 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 import、npm run package和npm run make。 - 打包结果通常位于
out目录,正式安装包通常位于out/make。 - 没有写死 Electron、Node.js 或 Forge 的具体版本号,避免版本升级后命令和内容过时。
private note
交流
文章暂不开放公开评论。如果你有想法、问题或建议,欢迎私下联系站长。
联系站长 →