超分かりやすいElectronの環境構築記事を見てブラウザもどき構築
TL;DR
@sprout2000さんの素晴らしい環境構築記事を参考に、Electron + React + TypeScriptでシンプルなブラウザアプリを構築しました。
実際に動くものを作りながら、Electronの基本概念が理解できました。
// たった23行でElectronアプリが起動
app.whenReady().then(() => {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
webviewTag: true,
},
});
mainWindow.loadFile("dist/index.html");
});
きっかけ:Electronって触ったことなかったな
普段はWeb開発をメインにしていますが、デスクトップアプリって作ったことがありませんでした。
「ElectronってVSCodeやSlackで使われてるあれでしょ?どうやって作るんだろう...」
興味はあったものの、なかなか手が出せずにいました。
環境構築の壁
Electronを始めようと公式ドキュメントを見たものの:
- 概念が多い: メインプロセス、レンダラープロセス、プリロードスクリプト...
- 設定ファイルが多い: webpack, TypeScript, package.json...
- 情報が散らばっている: セキュリティ対策、ビルド設定、プロセス間通信...
「環境構築だけで1日終わりそう...」
そんな時に出会ったのが、@sprout2000さんの記事でした。
Electronの基本概念:3つのプロセス
実装の前に、Electronの仕組みを理解しておきましょう。
1. メインプロセス(Main Process)
// src/main.ts
import { BrowserWindow, app } from "electron";
app.whenReady().then(() => {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
});
mainWindow.loadFile("dist/index.html");
});
- 役割: アプリ全体を管理するNode.js環境
- できること: ウィンドウの作成、システムAPIへのアクセス
- 1つのアプリに1つだけ存在
2. レンダラープロセス(Renderer Process)
// src/web/App.tsx
import { useState } from "react";
export const App = () => {
const [url, setUrl] = useState("https://www.google.com");
return <div>ブラウザUI</div>;
};
- 役割: UIを表示するChromiumのWebページ環境
- できること: HTML/CSS/JSでUIを構築(Reactも使える!)
- 複数のウィンドウ = 複数のレンダラープロセス
3. プリロードスクリプト(Preload Script)
// src/preload.ts
console.log("preloaded!");
- 役割: メインとレンダラーの橋渡し
- 重要: セキュリティを保ちながらNode.js機能を公開
プロジェクト構成
実際に作ったブラウザアプリの構成です:
simple-electron-browser/
├── src/
│ ├── main.ts # メインプロセス(23行)
│ ├── preload.ts # プリロードスクリプト(2行)
│ ├── global.d.ts # TypeScript型定義
│ └── web/ # レンダラープロセス
│ ├── index.html # HTMLテンプレート
│ ├── index.tsx # Reactエントリーポイント
│ ├── App.tsx # メインコンポーネント(94行)
│ └── App.css # スタイル
├── webpack.config.ts # Webpack設定(115行)
├── tsconfig.json # TypeScript設定
└── package.json # 依存関係
シンプルな構成。 これだけで動作するブラウザが完成します。
実装:各ファイルの役割を理解しながら構築
1. package.json - 依存関係の定義
{
"name": "simple-electron-browser",
"main": "dist/main.js",
"scripts": {
"dev": "rimraf dist && run-p dev:webpack dev:electron",
"dev:webpack": "cross-env NODE_ENV=\"development\" webpack --progress",
"dev:electron": "wait-on ./dist/index.html ./dist/main.js && electronmon .",
"build": "cross-env NODE_ENV=\"production\" webpack --progress"
},
"devDependencies": {
"electron": "^38.3.0",
"react": "^19.2.0",
"typescript": "^5.9.3",
"webpack": "^5.102.1"
// その他の依存関係...
}
}
スクリプトの流れ
npm run dev
↓
1. rimraf dist # 古いビルドを削除
2. run-p # 以下を並列実行
├─ webpack --watch # TSX→JSにコンパイル & 監視
└─ wait-on → electronmon # ビルド完了を待ってElectron起動
2. webpack.config.ts - 3つのビルドターゲット
Electronアプリは3つの異なる環境で動くコードを必要とします:
// メインプロセス向け設定
const main: Configuration = {
target: "electron-main",
entry: { main: "./src/main.ts" },
};
// プリロードスクリプト向け設定
const preload: Configuration = {
target: "electron-preload",
entry: { preload: "./src/preload.ts" },
};
// レンダラープロセス向け設定
const renderer: Configuration = {
target: "web", // セキュリティのため "electron-renderer" は使わない
entry: { app: "./src/web/index.tsx" },
plugins: [
new MiniCssExtractPlugin(), // CSSを別ファイルに
new HtmlWebpackPlugin({
template: "./src/web/index.html",
}),
],
};
export default [main, preload, renderer];
なぜ3つに分ける?
-
main: Node.js環境で動く(
electronモジュールが使える) - preload: 特殊な環境(Node.js + レンダラーのコンテキスト)
- renderer: ブラウザ環境で動く(Reactが動く)
それぞれ実行環境が違うので、個別にビルドする必要があります。
3. メインプロセス(src/main.ts)
import path from "node:path";
import { BrowserWindow, app } from "electron";
app.whenReady().then(() => {
// ウィンドウを作成
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
// プリロードスクリプトを読み込み
preload: path.join(__dirname, "preload.js"),
// webviewタグを有効化(ブラウザ表示に必要)
webviewTag: true,
},
});
// Reactアプリをロード
mainWindow.loadFile("dist/index.html");
});
// すべてのウィンドウが閉じられたらアプリを終了
app.once("window-all-closed", () => app.quit());
たった23行。 これでElectronアプリの基盤が完成です。
webviewTagの重要性
webPreferences: {
webviewTag: true, // これがないと<webview>が動かない
}
今回のブラウザアプリでは、<webview>タグを使って外部サイトを表示します。デフォルトでは無効なので、明示的に有効化する必要があります。
4. レンダラープロセス(src/web/App.tsx)
ここがブラウザのUI部分です:
import { useEffect, useRef, useState } from "react";
import { ArrowLeft, ArrowRight, RotateCw } from "lucide-react";
export const App = () => {
const [url, setUrl] = useState("https://www.google.com");
const [inputUrl, setInputUrl] = useState("https://www.google.com");
const [canGoBack, setCanGoBack] = useState(false);
const [canGoForward, setCanGoForward] = useState(false);
const webviewRef = useRef<Electron.WebviewTag>(null);
// webviewのイベント監視
useEffect(() => {
const webview = webviewRef.current;
if (!webview) return;
const handleDidNavigate = (e: any) => {
setUrl(e.url);
setInputUrl(e.url);
setCanGoBack(webview.canGoBack());
setCanGoForward(webview.canGoForward());
};
webview.addEventListener("did-navigate", handleDidNavigate);
webview.addEventListener("did-navigate-in-page", handleDidNavigate);
return () => {
webview.removeEventListener("did-navigate", handleDidNavigate);
webview.removeEventListener("did-navigate-in-page", handleDidNavigate);
};
}, []);
// ナビゲーション処理
const handleNavigate = (e: React.FormEvent) => {
e.preventDefault();
let targetUrl = inputUrl.trim();
// プロトコルがなければhttps://を追加
if (!targetUrl.match(/^https?:\/\//)) {
targetUrl = "https://" + targetUrl;
}
setUrl(targetUrl);
webviewRef.current?.loadURL(targetUrl);
};
return (
<div className="container">
<div className="toolbar">
<button onClick={() => webviewRef.current?.goBack()} disabled={!canGoBack}>
<ArrowLeft size={20} />
</button>
<button onClick={() => webviewRef.current?.goForward()} disabled={!canGoForward}>
<ArrowRight size={20} />
</button>
<button onClick={() => webviewRef.current?.reload()}>
<RotateCw size={20} />
</button>
<form onSubmit={handleNavigate}>
<input
type="text"
value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)}
placeholder="URLを入力してください"
/>
</form>
</div>
<webview ref={webviewRef} src={url} className="webview" />
</div>
);
};
コードの流れ
-
webviewRefでDOMにアクセス:
<webview>タグを直接操作 - イベント監視: ページ遷移を検知してURL更新
- ナビゲーション機能: 戻る/進む/リロード
- URL入力: プロトコル補完も実装
5. スタイル(src/web/App.css)
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
display: flex;
gap: 8px;
padding: 8px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.webview {
flex: 1;
width: 100%;
}
シンプルなFlexboxレイアウト。 ツールバーとwebviewを縦に並べるだけ。
実際に動かしてみる
1. インストール
npm install
2. 開発モードで起動
npm run dev
このコマンドで:
- webpackがソースコードをビルド(監視モード)
- Electronアプリが起動
- ファイル変更時に自動リロード
3. 動作確認
起動すると、Googleのページが表示されます:
- URLバーに
zenn.devと入力 → Zennが表示される - 戻る/進むボタンでナビゲーション
- リロードボタンでページ更新
4. プロダクションビルド
npm run build
最適化されたビルドがdist/に生成されます。
つまずいたポイント
記事が優秀すぎてなし!
学んだこと
技術面
- Electronの3つのプロセス: それぞれの役割と分離の重要性
- webpackの複数ターゲット: 実行環境ごとに適切なビルド設定
- Reactとの統合: レンダラープロセスは普通のWebアプリと同じ
まとめ:環境構築記事の重要性
今回、@sprout2000さんの記事のおかげで、迷わずElectronアプリを構築できました。
この記事の素晴らしかった点
- セキュリティを最初から考慮: 後から直すより最初から安全に
- 理由が明確: 「なぜこうするのか」が書いてある
- 実践的な構成: 実際のプロジェクトですぐ使える設定
Special Thanks: @sprout2000さんの環境構築記事
今回作ったもの: simple-electron-browser
同じようにElectronを始めたい人の助けになれば嬉しいです。
Discussion