【入門】ElectronをTypeScriptで手軽に試したい

2022/12/08に公開

この記事はClassi developers Advent Calendar 2022の8日目の記事です。

はじめに

Electronとは、Windows、macOS、Linuxで動作するアプリが作れるソフトウェアフレームワークです。

読者対象者は以下の様な方です。

  • Electronの概要は知っているが、触ったことが無い方
  • TypeScriptで実装し学習したい方
  • Vue.jsやReactなどのライブラリは考慮しないで、Electronが動く環境を試したい方

今回紹介する実装は、出来るだけシンプルな構成を目指しました。一旦依存関係を出来るだけ省きElectronの機能に集中する事を目的としています。

実は、Electron Forgeという簡単にテンプレートが作成できるツールもあります。ある程度Electronに慣れてきたら、こちらを利用してみても良いでしょう。

作ったもの

開発環境

  • macOS 13.0.1
  • Node.js@18.12.1

https://github.com/lowput/electron-typescript

.
├── README.md
├── package-lock.json
├── package.json
├── src
│   ├── main.ts
│   ├── preload.ts
│   ├── render
│   │   ├── index.html
│   │   ├── script
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── .parcelrc
│   └── types
│       └── process
│           └── app.d.ts
└── tsconfig.json

Node.jsが実行できる環境であれば実行して試せます。

git clone https://github.com/lowput/electron-typescript.git
cd electron-typescript
npm install
npm run start

Electronについて

実装の紹介に入る前にElectronについて簡単に説明します。
Electronにはメインプロセスとレンダラープロセスの二種類があります。

  • メインプロセス(src/main.ts):アプリケーションのライフサイクルの制御をしています。Node.jsが提供しているライブラリもメインプロセスで実行されることになります。
  • レンダラープロセス(src/render/*,src/preload.ts):Webコンテンツを制御します。Chromiumが使われており、Webの知識でUIが実装できるのはこの為です。

本記事では、ローカルのHTML(src/render/index.html)をロードしていますが、インターネット上のWebを指定することもできます。
また、メインプロセスは1つですが、レンダラープロセスを複数起動してマルチウィンドウを実装することも可能です。

参考

環境準備

今回導入したパッケージと、各バージョンは以下のようになっています。

package.json
  "devDependencies": {
    "@parcel/transformer-typescript-tsc": "^2.8.0",
    "@types/node": "^18.11.9",
    "electron": "^22.0.0",
    "npm-run-all": "^4.1.5",
    "parcel": "^2.8.0",
    "typescript": "^4.8.4"
  }

メインプロセスの実装

Electronのエンドポイント、src/main.tsの実装です。以下抜粋します。

src/main.ts
import { BrowserWindow, app, ipcMain, IpcMainInvokeEvent } from 'electron'
import path from "path";

const mainURL = `file://${__dirname}/render/index.html`

function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            preload: path.join(__dirname, "preload.js"),
        }
    });
    win.loadURL(mainURL);
    win.on('ready-to-show', () => {
        win.webContents.send('receive:message', "Hello Electron!!");
    });

    win.webContents.openDevTools();
}

app.whenReady().then(() => {
    createWindow();
});

appモジュールではメインプロセスのイベントを制御しています。
イベントの一つreadyは初期化処理が完了した状態を指します。whenReady関数はreadyイベント時のPromisを返す為、コールバック関数内でウィンドウを表示します。

createWindow関数で行っていること

  1. new BrowserWindowではウィンドウの表示やイベントの登録ができるオブジェクトを生成します。
    • webPreferencespreload.jsを指定しています。詳細は後述します。
  2. win.loadURL(mainURL)ではローカルのHTMLファイルの描画を指定します。
  3. win.on()ではready-to-showイベント時のコールバックを実装しています。
    • ready-to-showイベントはレンダラープロセスが初めてレンダリングし終わったときを指します。
    • win.webContents.sendreceive:messageという自作のchannel名で文字列を送っています。
  4. win.webContents.openDevTools()で開発者向けツールが開きます。

参考

プロセス間通信を実装する

src/preload.tsでメインプロセスとレンダラープロセス間で情報をやり取りする関数を実装しています。前述のnew BrowserWindow()のオプションには、トランスパイルされたpreload.jsを指定しています。preload.jsはレンダラープロセスで実行されることになります。

srs/preload.ts
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'

contextBridge.exposeInMainWorld(
    "app", {
	sendMainSend: (msg: string): void => {
            ipcRenderer.invoke('send:message', msg);
        },
        receiveMessage: (callback: (msg: string) => void) => {
            ipcRenderer.on('receive:message', (_: IpcRendererEvent, msg: string) => {
                callback(msg);
            });
        }
    }
);

contextBridge.exposeInMainWorld関数で、レンダラープロセスで実行できる関数を登録しています。第一引数の内容appがAPIキーと呼ばれ、windowオブジェクトのプロパティ名となります。
すなわち、Web上では、window.app.sendMainSend関数とwindow.app.receiveMessage関数が定義され、呼び出すことが可能となります。

やっていること

  • sendMainSendipcRenderer.invokeはメインプロセスへメッセージを送信します。第一引数はchannel名で任意に決めることができます。第二引数が送信するメッセージです。よって、メインプロセス側で同じchannel名で受け入れる実装が必要です。
src/main.ts
ipcMain.handle('send:message', (_: IpcMainInvokeEvent, msg: string) => {
    console.log(`ipcMain on : ${msg}`);
});

ここでのconsole.logはElectron画面の開発ツール上ではなく、アプリケーションを実行させたターミナル上に表示されることになります。

  • receiveMessageipcRenderer.onは逆にメインプロセスからメッセージを受信するたびに呼び出される関数を登録する関数です。createWindow関数で行っていることで紹介したとおり、webContents.sendが送信元になります。
src/main.ts
    win.on('ready-to-show', () => {
        win.webContents.send('receive:message', "Hello Electron!!");
    });

レンダラープロセス側で動作するTypeScriptの実装を見てみましょう。

src/render/script/index.ts
window.onload = () => {
    window.app.receiveMessage((msg: string) => {
        const contents = <HTMLDivElement>document.getElementById('contents');
        contents.innerHTML = msg;
    });
    window.app.sendMainSend('Start Renderer proces.');
}

window.app.receiveMessageでメインプロセスからのメッセージをIDがcontentsである要素の内容を書き換えています。無駄にトリッキーな作りにしてますが、アプリケーションの状態によって、レンダラープロセス(Web)側へメッセージが送れ、描画の制御が可能です。ページのロードが完了するまでバックグラウンドの色を設定するなどの工夫も公式資料で紹介されています(下記の参考URLより)。

参考

型定義をする

ところで、src/render/script/index.tsでは独自の関数を記述しています。よって型定義ファイルが必要です。

src/types/process/app.d.ts
declare global {
    interface Window {
        app: IMainProcess;
    }
}

export interface IMainProcess {
    sendMainSend: (msg: string) => void;
    receiveMessage: (callback: (msg: string) => void) => void;
}

Windowインターフェイスにプロパティappを追加しています。そしてその型としてIMainProcessを定義しています。作成した定義ファイルはsrc/render/tsconfig.jsonにて追加してあります。

セキュリティについて

ElectronはWeb技術を使ってアプリケーションを開発することができる便利な仕組みです。その為、Web開発時同様のセキュリティリスクも合わせて考えなくてはいけません。Electronの公式ドキュメントにもチェックリストがあるので目を通しておくと良いでしょう。今回はその一つを紹介します。

Content Security Policy(CSP)の定義

CSPはクロスサイトスクリプティング(XSS)やデータインジェクション攻撃などの攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤーです。
今回ロードしているsrc/render/index.htmlでは<meta>タグを使って、セキュリティポリシーを設定しています。

<meta http-equiv="Content-Security-Policy" content="default-src 'self'" />

contentdefault-src 'self'とすることで同一オリジンからのコンテンツのみ読み込みを許可することとなります。

ちなみに、上記<meta>タグを削除して起動すると、開発ツールのコンソールに以下のように表示されます。

参考

ビルド定義

ビルドにはParcelというツールを使っています。また、npm-run-allを使ってビルドコマンドをまとめて実行してくれるようにします。

package.json
  "scripts": {
    "build": "run-p build:main build:render",
    "build:main": "tsc -p tsconfig.json",
    "build:render": "parcel build src/render/index.html --public-url ./ --dist-dir ./dist/render",
    "start": "run-s build start:electron",
    "start:electron": "electron dist/main.js"
  },

parcelは主にレンダラープロセス(Web側)のビルドに使っています。tsconfig.jsonと同様にdistディレクトリに出力するよう指定しています。

parcel build src/render/index.html --public-url ./ --dist-dir ./dist/render

最後に

最後まで読んでいただきありがとうございます。
本記事がElectronの理解の一助になれば幸いです。

Discussion