Electron:
2026年6月24日インストール:electron-vite
pnpm create @quick-start/electrontailwind:
Shadcn:マニュアルで行う
UIのボタンを押すとサーバーサイドでlogを出す例:
main/index.ts
ipcMain.on("get-log", () => {
console.log("log");
});preload/index.ts
contextBridge.exposeInMainWorld("api", {
getLog: () => ipcRenderer.invoke("get-log")
});renderer/app.tsx
<button className="cursor-pointer" onClick={() => window.api.getLog()}>Log</button>インストール後にできる3つのフォルダ
main
Nextjsにおける /api/route.tsのような役割
サーバーサイドのロジックを実行できる。
一番使うと思われる機能は2つ
アプリのウインドウを管理する
new BrowserWindow()と、rendererからの通信を受け取るipcMain()
ipcMain:
ipcMain.on("get-log", () => {
console.log("log");
});ipcMain.on("get-log", () => {
return `${new Date().toLocaleDateString()}`;
});ipcMain.onは何もreturnしない・できないが、ipcMain.handleはreturnできる。handleで返すものは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に入れる。
パスはimportし、末尾に?assetをつけないとエラーになる。
preload
NextjsにおけるserverActionのようなクライアント(renderer)からAPIを呼び出す中間
contextBridge:
contextBridge.exposeInMainWorld("api", {
getLog: () => ipcRenderer.invoke("get-log")
});exposeInMainWorld()をメインで使う。
ipcRendererは sendとinvokeメソッドを持ち、ipcMainのonとhandleに対応。sendは一方向でonに対応、invokeが双方向でhandleに対応。
(method) Electron.ContextBridge.exposeInMainWorld(apiKey: string, api: any): void第一引数は"api"という名前にするのが一般的
electronAPI:
const ipcHandle = (): void => window.electron.ipcRenderer.send("ping");electron-viteのモックには上記のようなpreloadフォルダのファイルを経由せずに直接mainとやり取りをしているコードがある。
preloadに設定されている"electron"というcontextBridgeを経由することでUI側のファイルから直接mainのicpMainにアクセスできる。
// preload
import { electronAPI } from "@electron-toolkit/preload";
contextBridge.exposeInMainWorld("electron", electronAPI);基本的には
window.api.getData()のようにpreload側で定義・経由した方がいい。
renderer
Nextjsにおける"use client"をつけたファイルのような役割。
window.apiの型
window.api.quitApp()'window.api''は 'unknown' 型です
上記のようなエラーが赤線で出るのは、デフォルトだとブラウザ標準のwindowしか認識されていないため
preload/index.d.ts:
import { ElectronAPI } from "@electron-toolkit/preload";
declare global {
interface Window {
electron: ElectronAPI;
api: unknown;
}
}デフォルトで作成されたものにwindow.apiの型を追加していく。
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;
}
}assets:
import icon from "./assets/icon.png";
<img className="ring size-40 ring-orange-500" src={icon} alt="" />デフォルトで作成されるrenderer/src/assetsに画像を入れる。srcにパスを書くのではなくimportしないと表示されない。
引数を渡す
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")/package.jsonのname/Ubuntuの場合
/home/ユーザー/.config/
OS毎にパスを設定する必要は無い。
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: ["はい", "いいえ"],
defaultId: 1,
cancelId: 1,
message: message
detail: ""
});
return response === 0;
});
---
// UI
const confirmed: boolean = await window.api.showConfirmBox(`削除?`);日本語フォント
問題:
dialog.showMessageBox()とshowConfirmBox()は「Linux x Snap」だとデプロイ後に日本語フォントがすべて□になる。他の組み合わせだと起きない模様。UI側でDialogを作成したり、toastを利用してUI側にメッセージを出す。
アプリのインストール
ビルド:
"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が作成される。
package.jsonのversionを上げた場合はdistを削除してからコマンドを実行する。
インストール:
sudo snap install --dangerous dist/xxx.snapSnap-storeへ登録していないので--dangerousフラグが必要
WindowsやMacも登録・認証しないとインストール時に警告が出る。
snapの公開
electron-builder.ymlのモック値を変える必要がある箇所はAI等を参照
アカウント作成: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ここまで行うとアプリストアを検索するとアプリが見つかる。
stableとあるが、アプリには複数の段階がある。
edge = 開発段階 、stable = 安定版
edgeでuploadしたらhttps://snapcraft.io/ or ターミナルからstableに昇格させる。
更新:
pnpm run build:linux
snapcraft upload dist/test-counter-app-*.snap --release=stablepackage.jsonのversionを上げるのを忘れない事