【入門】ElectronをTypeScriptで手軽に試したい
この記事は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
.
├── 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つですが、レンダラープロセスを複数起動してマルチウィンドウを実装することも可能です。
参考
環境準備
今回導入したパッケージと、各バージョンは以下のようになっています。
"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
の実装です。以下抜粋します。
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
関数で行っていること
-
new BrowserWindow
ではウィンドウの表示やイベントの登録ができるオブジェクトを生成します。-
webPreferences
にpreload.js
を指定しています。詳細は後述します。
-
-
win.loadURL(mainURL)
ではローカルのHTMLファイルの描画を指定します。 -
win.on()
ではready-to-show
イベント時のコールバックを実装しています。-
ready-to-show
イベントはレンダラープロセスが初めてレンダリングし終わったときを指します。 -
win.webContents.send
でreceive:message
という自作のchannel名で文字列を送っています。
-
-
win.webContents.openDevTools()
で開発者向けツールが開きます。
参考
プロセス間通信を実装する
src/preload.ts
でメインプロセスとレンダラープロセス間で情報をやり取りする関数を実装しています。前述のnew BrowserWindow()
のオプションには、トランスパイルされたpreload.js
を指定しています。preload.js
はレンダラープロセスで実行されることになります。
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
関数が定義され、呼び出すことが可能となります。
やっていること
-
sendMainSend
:ipcRenderer.invoke
はメインプロセスへメッセージを送信します。第一引数はchannel名で任意に決めることができます。第二引数が送信するメッセージです。よって、メインプロセス側で同じchannel名で受け入れる実装が必要です。
ipcMain.handle('send:message', (_: IpcMainInvokeEvent, msg: string) => {
console.log(`ipcMain on : ${msg}`);
});
ここでのconsole.logはElectron画面の開発ツール上ではなく、アプリケーションを実行させたターミナル上に表示されることになります。
-
receiveMessage
:ipcRenderer.on
は逆にメインプロセスからメッセージを受信するたびに呼び出される関数を登録する関数です。createWindow関数で行っていること
で紹介したとおり、webContents.send
が送信元になります。
win.on('ready-to-show', () => {
win.webContents.send('receive:message', "Hello Electron!!");
});
レンダラープロセス側で動作するTypeScriptの実装を見てみましょう。
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
では独自の関数を記述しています。よって型定義ファイルが必要です。
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'" />
content
をdefault-src 'self'
とすることで同一オリジンからのコンテンツのみ読み込みを許可することとなります。
ちなみに、上記<meta>
タグを削除して起動すると、開発ツールのコンソールに以下のように表示されます。
参考
ビルド定義
ビルドにはParcel
というツールを使っています。また、npm-run-all
を使ってビルドコマンドをまとめて実行してくれるようにします。
"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