🙏

超分かりやすい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>
  );
};

コードの流れ

  1. webviewRefでDOMにアクセス: <webview>タグを直接操作
  2. イベント監視: ページ遷移を検知してURL更新
  3. ナビゲーション機能: 戻る/進む/リロード
  4. 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

このコマンドで:

  1. webpackがソースコードをビルド(監視モード)
  2. Electronアプリが起動
  3. ファイル変更時に自動リロード

3. 動作確認

起動すると、Googleのページが表示されます:

  • URLバーにzenn.devと入力 → Zennが表示される
  • 戻る/進むボタンでナビゲーション
  • リロードボタンでページ更新

4. プロダクションビルド

npm run build

最適化されたビルドがdist/に生成されます。

つまずいたポイント

記事が優秀すぎてなし!

学んだこと

技術面

  • Electronの3つのプロセス: それぞれの役割と分離の重要性
  • webpackの複数ターゲット: 実行環境ごとに適切なビルド設定
  • Reactとの統合: レンダラープロセスは普通のWebアプリと同じ

まとめ:環境構築記事の重要性

今回、@sprout2000さんの記事のおかげで、迷わずElectronアプリを構築できました。

この記事の素晴らしかった点

  1. セキュリティを最初から考慮: 後から直すより最初から安全に
  2. 理由が明確: 「なぜこうするのか」が書いてある
  3. 実践的な構成: 実際のプロジェクトですぐ使える設定

Special Thanks: @sprout2000さんの環境構築記事

今回作ったもの: simple-electron-browser

同じようにElectronを始めたい人の助けになれば嬉しいです。

Discussion