Electron:
2026年7月30日インストール:electron-vite
pnpm create @quick-start/electrontailwind:
Shadcn:マニュアル
クライアントからサーバーサイドに通信する基本形:
main/index.ts
ipcMain.on("get-log", () => {
console.log("log");
});preload/index.ts
contextBridge.exposeInMainWorld("api", {
getLog: () => ipcRenderer.send("get-log")
});renderer/app.tsx
<button className="cursor-pointer" onClick={() => window.api.getLog()}>Log</button>インストール後にできる3つのフォルダについて
main
Nextjsにおける
/api/route.tsのような役割サーバーサイドのロジックを実行できる。
厳密にはサーバーサイドというよりメインプロセス側
実行環境はNodejs
ビルド時にNodejsの実行環境が同梱されるため、User側のPCにNodejsは必要無い。
よく使うと思われる機能:アプリのウインドウを管理する
new BrowserWindow()と、rendererからの通信を受け取るipcMain()
ipcMain:
ipcMain.on("get-log", () => {
console.log("log");
});ipcMain.handle("get-log", () => {
return `${new Date().toLocaleDateString()}`;
});ipcMain.onは何もreturnしない片方向通信、ipcMain.handleはreturnできる双方向通信に使う。
// preload
contextBridge.exposeInMainWorld("api", {
getLog: () => ipcRenderer.invoke("get-log")
});
---
// renderer
const handleClick = async () => {
const result = await window.api.getLog();
console.log(result);
}resourcesフォルダ:
import icon2 from "../../resources/icon2.png?asset";
const myTray = new Tray(icon2);メインプロセス側で使うアセット(例:トレイや通知で使う画像)はルートの
/resourcesに入れる。クライアント側で使うアセットは/renderer/src/assetsimportパスの末尾に
?assetをつけないとエラーになる。
preload
NextjsにおけるserverActionのようなクライアント(renderer)からAPIを呼び出す中間
contextBridge:
contextBridge.exposeInMainWorld("api", {
getLog: () => ipcRenderer.invoke("get-log")
});よく使うと思われる機能:
exposeInMainWorld()ipcRendererはsend(一方向)、invoke(双方向)メソッドを持ち、ipcMainのonとhandleに対応第一引数はrenderer側で使いやすい任意の値にする。
型定義
'window.api''は 'unknown' 型です
preloadに追加したAPIを型に追加しないと、rendererで呼び出した時に上記のようなエラーが出る。
デフォルトだとブラウザ標準のwindowオブジェクトしか認識されていないため。
preload/index.d.ts:
import { ElectronAPI } from "@electron-toolkit/preload";
declare global {
interface Window {
electron: ElectronAPI;
api: unknown;
}
}上記がデフォルトの状態
import { ElectronAPI } from "@electron-toolkit/preload";
interface CustomAPI {
getCount: (target: string) => Promise<number>;
incrementCount: (target: string) => Promise<number>;
quitApp: () => void;
}
declare global {
interface Window {
electron: ElectronAPI;
api: CustomAPI;
}
}preloadに書いたAPIを追加していく。
renderer
Nextjsにおける"use client"をつけたファイルのような役割。
preloadで定義したAPIを呼び出せる。
const handleDeleteHistory = async () => {
try {
await window.api.deleteHistory();
} catch (error) {
console.error("Failed to delete history:", error);
}
};assetsフォルダ:
import icon from "./assets/icon.png";
<img className="ring size-40 ring-orange-500" src={icon} alt="" />renderer側で使うアセットは
renderer/src/assetsに入れる。srcにパスを書くのではなくimportしないと表示されない。
APIに引数を渡す
renderer:
useEffect(() => {
const showAlert = async () => {
const message = await window.api.alertOnce("hello");
alert(message);
};
showAlert();
}, []);preload:
contextBridge.exposeInMainWorld("api", {
alertOnce: async (message: string) => {
return await ipcRenderer.invoke("alert-once", message);
}
});main:
ipcMain.handle("alert-once", async (_e, message) => {
console.log(message);
return message;
});第一引数はeventで固定
ipcMain.handle("set-limit", async (_e, target: string, limit: number) => {
});ローカルデータ
const dataPath = join(app.getPath("userData"), "log.json");app.getPath("userData")でOS毎の保存先を取得できる。ファイルの保存場所は
/app.getPath("userData")/package.jsonのname/Ubuntuの場合
/home/ユーザー/.config/package.jsonのname/
JSONファイル
// READ
const raw = fs.readFileSync(dataPath, "utf-8");
// WRITE
fs.writeFileSync(dataPath, JSON.stringify(data, null, 2), "utf-8");fs.writeFileSyncはファイルが存在しない場合に指定したパスへファイルを作成する。
SQLite
npm install better-sqlite3Ubuntuでエラーが出る場合
sudo apt install -y build-essential python3-dev libsqlite3-dev pkg-config他
alert/confirm()後にfocusが機能しない
問題:
UI側で
alert()、confirm()が表示された後にInputタグにfocusが当たらなくなる。原因:Electronの仕様
対策:
dialog.showMessageBox()を使用
alert:
ipcMain.handle("show-message-box", async (_e, message: string) => {
const win = BrowserWindow.getFocusedWindow();
await dialog.showMessageBox(win!, {
type: "info",
buttons: ["OK"],
message: message,
detail: ""
});
});アイコンのタイプ(info, error, question, warning)
confirm:
ipcMain.handle("show-confirm-box", async (_e, message: string) => {
const win = BrowserWindow.getFocusedWindow();
const { response } = await dialog.showMessageBox(win!, {
type: "question",
buttons: ["Yes", "No"],
defaultId: 1,
cancelId: 1,
message: message,
detail: ""
});
return response === 0;
});
---
// UI
const confirmed: boolean = await window.api.showConfirmBox(`削除?`);buttonsは押されると、押された値のインデックスを返す。defaultId、cancelIdを1にして誤爆を防ぐ。
日本語フォント
問題:
dialog.showMessageBox()はUbuntu x Snapだとデプロイ後に日本語フォントがすべて□になる。
対策:
dialog.showMessageBox()のDialogではなく、UI側でDialogを作成しそこからAPIを呼び出す。
version
バージョンの基本:
PATCH(一番右:
1.0.x): 既存機能のバグ修正。挙動を変えない軽微な修正CSSの調整程度では上げない。
MINOR(真ん中:
1.x.0): 新機能の追加(互換性を保ったまま機能が増えたとき)MINORが9になってもMAJORを繰り上げず、10,11,12...とMINORを上げていく。
MAJOR(一番左:
x.0.0): 互換性のない大変更(動かなくなるレベルの大きな仕様変更やリライト)
アプリのインストール
ビルド:
"build:win": "npm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux"linux用のコマンドを実行するとルートにdistフォルダが作成され、その中に
.debや.snapが作成される。
インストール:ビルドしたものをローカルでインストール
sudo snap install --dangerous dist/xxx.snap
sudo apt install dist/xxx.debsnapはsnap-storeへ登録していないので--dangerousフラグが必要
WindowsやMacも登録・認証しないとインストール時に警告が出る。
配布する場合、GithubリポジトリでDLできるようにさせ、インストールコマンドはローカルでやってもらうように促す。
snap-store
*保留:
アカウント作成:https://snapcraft.io/
snapcraft CLI をインストール&ログイン
sudo snap install snapcraft --classic
snapcraft loginSnap名を登録
snapcraft register test-counter-appSnap-storeにupload
snapcraft upload dist/test-counter-app-*.snap --release=stableここまで行うとアプリストアを検索するとアプリが見つかる。
アプリには複数の段階がある。
edge = 開発段階 、stable = 安定版
更新:
pnpm run build:linux
snapcraft upload dist/test-counter-app-*.snap --release=stable