React + Electron 实战项目
本章节我们将使用 React + Electron 制作一个笔记应用,这个实战项目名为 ReactElectronNotes,功能涵盖多窗口、托盘、文件操作和 IPC 通信,是初学者练手非常合适的项目。
如果你还不了解 React,可以先参考:React 教程。
ReactElectronNotes 项目将教你从零开始构建一个 跨平台桌面笔记应用,包含以下特性:
- React 前端渲染
- Electron 主进程管理
- IPC 通信(主进程 ↔ 渲染进程)
- 笔记数据本地持久化(
electron-store
) - 系统托盘与菜单集成
项目结构说明
ReactElectronNotes/ │ ├── src/ # React 源码目录 ├── build/ # 打包后的 React 静态文件 ├── electron-main.js # Electron 主进程 ├── preload.js # 渲染与主进程通信桥 ├── note.html # 子窗口页面 ├── note.js # 子窗口脚本 ├── package.json └── assets/ └── icon.png # 托盘图标
一、项目功能概述
功能模块 | 说明 |
---|---|
主窗口 | 显示所有笔记列表 |
子窗口 | 新建笔记输入 |
本地存储 | 使用 electron-store 保存数据 |
托盘 | 最小化到托盘,可点击唤起主界面 |
IPC 通信 | 渲染进程与主进程交互,读写数据 |
二、项目初始化
1. 创建 React 项目
npx create-react-app ReactElectronNotes cd ReactElectronNotes
2. 安装 Electron 及依赖
npm install --save-dev electron electron-builder concurrently wait-on npm install electron-store
3. 修改 package.json
确保在 scripts
中添加 Electron 启动命令:
"main": "electron-main.js", "scripts": { "react-start": "react-scripts start", "react-build": "react-scripts build", "electron-start": "concurrently \"npm run react-start\" \"wait-on http://localhost:3000 && electron .\"", "electron-build": "react-scripts build && electron-builder" }
三、创建 Electron 主进程
新建文件 electron-main.js
:
实例
const { app, BrowserWindow, Tray, Menu, ipcMain } = require('electron')
const path = require('path')
const Store = require('electron-store')
let mainWindow, noteWindow, tray
const store = new Store()
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:3000')
} else {
mainWindow.loadFile(path.join(__dirname, 'build/index.html'))
}
mainWindow.on('minimize', (e) => {
e.preventDefault()
mainWindow.hide()
})
createTray()
}
function createNoteWindow() {
noteWindow = new BrowserWindow({
width: 400,
height: 300,
parent: mainWindow,
modal: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
noteWindow.loadFile(path.join(__dirname, 'note.html'))
}
function createTray() {
tray = new Tray(path.join(__dirname, 'assets/icon.png'))
const menu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ label: '退出', click: () => app.quit() }
])
tray.setToolTip('ReactElectronNotes')
tray.setContextMenu(menu)
}
app.whenReady().then(createMainWindow)
ipcMain.handle('get-notes', () => {
return store.get('notes', [])
})
ipcMain.on('save-note', (event, note) => {
const notes = store.get('notes', [])
notes.push(note)
store.set('notes', notes)
mainWindow.webContents.send('update-notes', notes)
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
const path = require('path')
const Store = require('electron-store')
let mainWindow, noteWindow, tray
const store = new Store()
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:3000')
} else {
mainWindow.loadFile(path.join(__dirname, 'build/index.html'))
}
mainWindow.on('minimize', (e) => {
e.preventDefault()
mainWindow.hide()
})
createTray()
}
function createNoteWindow() {
noteWindow = new BrowserWindow({
width: 400,
height: 300,
parent: mainWindow,
modal: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
noteWindow.loadFile(path.join(__dirname, 'note.html'))
}
function createTray() {
tray = new Tray(path.join(__dirname, 'assets/icon.png'))
const menu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ label: '退出', click: () => app.quit() }
])
tray.setToolTip('ReactElectronNotes')
tray.setContextMenu(menu)
}
app.whenReady().then(createMainWindow)
ipcMain.handle('get-notes', () => {
return store.get('notes', [])
})
ipcMain.on('save-note', (event, note) => {
const notes = store.get('notes', [])
notes.push(note)
store.set('notes', notes)
mainWindow.webContents.send('update-notes', notes)
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
四、创建 Preload 脚本
新建 preload.js
:
实例
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
getNotes: () => ipcRenderer.invoke('get-notes'),
saveNote: (note) => ipcRenderer.send('save-note', note),
onUpdateNotes: (callback) => ipcRenderer.on('update-notes', callback)
})
contextBridge.exposeInMainWorld('electronAPI', {
getNotes: () => ipcRenderer.invoke('get-notes'),
saveNote: (note) => ipcRenderer.send('save-note', note),
onUpdateNotes: (callback) => ipcRenderer.on('update-notes', callback)
})
说明:
通过 contextBridge
安全地暴露 API 给渲染进程。
五、React 前端界面
src/App.js
实例
import React, { useState, useEffect } from 'react'
function App() {
const [notes, setNotes] = useState([])
useEffect(() => {
window.electronAPI.getNotes().then(setNotes)
window.electronAPI.onUpdateNotes((_event, newNotes) => setNotes(newNotes))
}, [])
const openNoteWindow = () => {
window.open('note.html', '新建笔记', 'width=400,height=300')
}
return (
<div style={{ padding: 20 }}>
<h1>我的笔记</h1>
<button onClick={openNoteWindow}>新建笔记</button>
<ul>
{notes.map((n, i) => (
<li key={i}>{n}</li>
))}
</ul>
</div>
)
}
export default App
function App() {
const [notes, setNotes] = useState([])
useEffect(() => {
window.electronAPI.getNotes().then(setNotes)
window.electronAPI.onUpdateNotes((_event, newNotes) => setNotes(newNotes))
}, [])
const openNoteWindow = () => {
window.open('note.html', '新建笔记', 'width=400,height=300')
}
return (
<div style={{ padding: 20 }}>
<h1>我的笔记</h1>
<button onClick={openNoteWindow}>新建笔记</button>
<ul>
{notes.map((n, i) => (
<li key={i}>{n}</li>
))}
</ul>
</div>
)
}
export default App
新建 note.html
与 note.js
note.html
实例
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>新建笔记</title>
</head>
<body>
<h2>新建笔记</h2>
<textarea id="note"></textarea><br>
<button id="saveBtn">保存</button>
<script src="note.js"></script>
</body>
</html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>新建笔记</title>
</head>
<body>
<h2>新建笔记</h2>
<textarea id="note"></textarea><br>
<button id="saveBtn">保存</button>
<script src="note.js"></script>
</body>
</html>
note.js
实例
const saveBtn = document.getElementById('saveBtn')
saveBtn.addEventListener('click', () => {
const note = document.getElementById('note').value
if (note.trim()) {
window.electronAPI.saveNote(note)
window.close()
}
})
saveBtn.addEventListener('click', () => {
const note = document.getElementById('note').value
if (note.trim()) {
window.electronAPI.saveNote(note)
window.close()
}
})
六、运行与打包
启动开发环境
npm run electron-start
打开后会出现 React 主窗口,可点击"新建笔记"弹出输入窗口。
打包发布
npm run electron-build
会生成跨平台的可执行文件(.exe
、.app
、.AppImage
等)。
strong>七、扩展方向
功能 | 思路 |
---|---|
Markdown 支持 | 使用 react-markdown 渲染内容 |
数据搜索 | React 端通过 filter() 实现本地搜索 |
自动保存 | 监听输入框 onChange 自动保存 |
云同步 | 结合 Firebase 或自建 REST API |
快捷键 | 使用 globalShortcut 注册 Ctrl+N 新建笔记 |
点我分享笔记