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など)。
七、拡張の方向性
| 機能 | アイデア |
|---|---|
| Markdown 対応 | を使用してreact-markdownコンテンツをレンダリング |
| データ検索 | React 側はfilter()を使用してローカル検索を実装 |
| 自動保存 | 入力ボックスを監視onChange自動保存 |
| クラウド同期 | Firebase や自前の REST API と組み合わせる |
| ショートカットキー | を使用してglobalShortcutCtrl+N で新規ノートを作成するよう登録 |