はじめに
NTTテクノクロス Advent Calendar 2020の14日目担当 @geekduck です🦆
最近はGolangでAPIサーバー、React.jsでSPA、そのSPAを開くElectron製常駐アプリを作るプロジェクトに関わっています。
そのプロジェクトの中で調べた、ElectronのTipsをいくつか紹介したいと思います。
以下のサンプルコードが動作するElectronアプリのソースコード: https://github.com/geekduck/electron-tips
この投稿で使用している言語、ライブラリのバージョン
- Node.js v14.15.1
- npm v6.14.9
- Electron v11.0.4
- TypeScript v4.1.2
TypeScriptでElectronアプリを作りたい&ホットリロードもしたい
TypeScript化
最近はTypeScriptが完全にデファクトになったので、ElectronもTypeScriptで書きたいですよね。
electron-quick-startをベースにTypeScript化するサンプルを載せます。
$ https://github.com/electron/electron-quick-start
$ npm i -D typescript
$ touch tsconfig.json
TypeScriptの設定ファイルをこんな感じで作ります。
srcディレクトリ以下のTypeScriptファイルをビルドして、buildディレクトリにJavaScriptファイルを出力します。
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"sourceMap": true,
"strict": true,
"outDir": "./build",
"rootDir": "./src",
"noEmitOnError": true,
"typeRoots": [
"node_modules/@types"
]
}
}
buildディレクトリにビルドしたJavaScriptファイルが出力されるので、package.jsonのmainプロパティの値を.build/main.js
に変更します。
{
"name": "electron-typescript-sample",
"version": "1.0.0",
"main": "./build/main.js",
"scripts": {
"start": "electron ."
},
"devDependencies": {
"electron": "^11.0.3",
"typescript": "^4.1.2"
}
}
index.htmlにもビルド前のパスが書かれているので変更します。
<!-- 17行目あたり -->
<script src="./build/renderer.js"></script> <!-- ./renderer.js のパスをbuildディレクトリからにする -->
JavaScriptファイルをsrcディレクトリに移動しつつTypeScript化します。
$ mv main.js src/main.ts
$ mv preload.js src/preload.ts
$ mv renderer.js src/renderer.ts
import { app, BrowserWindow, Menu, Tray } from "electron"; // requireをimportに変更した
import * as path from "path";
// 以降のコードはそのままのため省略
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector:string, text:string) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, (process.versions as any)[type])
}
})
renderer.jsは拡張子を.tsに変更するだけでOKです。
TypeScriptをビルドしてみます。
$ ll build/
# ファイルが何もない
$ npx tsc
$ ll build/
total 48
-rw-r--r-- 1 user staff 1.5K 12 10 09:24 main.js
-rw-r--r-- 1 user staff 982B 12 10 09:24 main.js.map
-rw-r--r-- 1 user staff 403B 12 10 09:24 preload.js
-rw-r--r-- 1 user staff 497B 12 10 09:24 preload.js.map
-rw-r--r-- 1 user staff 343B 12 10 09:24 renderer.js
-rw-r--r-- 1 user staff 183B 12 10 09:24 renderer.js.map
ビルドできていますね。
Electronを立ち上げるとちゃんと表示されます。
$ npm start
これでElectronをTypeScriptで作れるようになりました。
ホットリロード
次に、srcディレクトリ配下のファイルを書き換えるたびにtsc && npm start
するのは面倒なのでホットリロード機能を追加します。
$ npm i -D electron-reload electron-is-dev npm-run-all
import { app, BrowserWindow, Menu, Tray } from "electron";
import * as path from "path";
import * as isDev from "electron-is-dev";
// 開発モードの場合はホットリロードする。
if (isDev) {
require("electron-reload")(__dirname, {
electron: require("path").join(__dirname, "..", "node_modules", ".bin", "electron"),
forceHardReset: true,
hardResetMethod: "exit",
});
}
// 以降のコードはそのままのため省略
package.json
のscriptsを変更して、electronの起動と、TypeScriptファイルの監視をパラレルで起動するようにします。
{
"name": "electron-tips",
"version": "1.0.0",
"main": "./build/main.js",
"scripts": {
"start": "run-p -r -l start:watch start:electron",
"start:electron": "electron .",
"start:watch": "tsc -w"
},
/* ...(省略)... */
}
npm start
でアプリを起動して、適当なファイルを編集/保存するとElectronが自動でリロードされます。
JavaScript内で実行環境がElectronかブラウザか判定する
現在関わっているPJではReact.jsでSPAを作り込んでHTTPサーバーで公開し、そのSPAに対してブラウザかElectronアプリでアクセスする構成をとっています。
Electronアプリはガワアプリとして最低限の機能(ウィンドウの表示/フォーカス、二重起動防止、常駐など)だけ作って、他の機能はSPA側で全て作るようにしています。
そのような構成の上で、ブラウザでSPAを開いた場合とElectronアプリでSPAを開いた場合にデザインを変更する必要があり、SPAのJavaScript内で実行環境がブラウザかElectronかを判定する方法がないかを調べました。
nodeIntegration: true
を指定してWebコンテンツを開いた場合、windowオブジェクトにいくつかのAPIが追加されるため、そのAPIの有無で実行環境がElectronかを判定します。
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true, // レンダラープロセスのwindowオブジェクトにNode.jsのAPIを追加する。
preload: path.join(__dirname, 'preload.js')
}
})
window?.process?.versions['electron'];
// Electronで開いた場合 -> "11.0.3"
// ブラウザで開いた場合 -> undefined
nodeIntegration: true
を指定してしまうとセキュリティ的に不安が出てしまう場合は、contextBridgeを使いましょう。
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
worldSafeExecuteJavaScript: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
import { contextBridge } from "electron";
// レンダラープロセスのwindowにelectronというプロパティを作る。
contextBridge.exposeInMainWorld("electron", {
isElectron: true,
});
window?.electron?.isElectron;
// Electronで開いた場合 -> true
// ブラウザで開いた場合 -> undefined
SPA側でこのプロパティを使って表示するコンテンツを切り替えれば、ブラウザで開かれた場合とElectronで開かれた場合によってSPAの動作を変えることができます。
参考:
https://github.com/electron/electron/issues/2288
https://www.electronjs.org/docs/api/context-bridge
https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions
electron-builder紹介
Electronのアプリをビルドする場合、electron-packagerよりもelectron-builderを使うのがオススメです。
electron-packagerを使って*nix系OSでWindows向けのビルドを作ろうとするとwineが必要ですが、electron-builderでは不要です。
また、electron-builderでビルドするとWindows用のインストーラーやmacOS用のdmgまで作ってくれます。
$ npm i -D electron-builder
package.jsonのscriptsにビルド用のスクリプトを追加します。
また、TypeScript化しているため、buildディレクトリ配下をelectron-builderのビルド対象に指定してあげます。
{
/* ...(省略)... */
"scripts": {
"start": "run-p -r -l start:watch start:electron",
"start:electron": "electron .",
"start:watch": "tsc -w",
"build:typescript": "tsc",
"build:installer:macOS": "electron-builder --mac --x64",
"build:installer:windows": "electron-builder --win --x64",
"build": "npm-run-all build:typescript --parallel build:installer:macOS build:installer:windows"
},
"build": {
"files": [
"*.html",
"build/**/*"
]
}
}
npm run build
を実行するとdistディレクトリ配下に成果物が出力されます。
$ npm run build
# ダウンロードとかビルドのログとか色々でる
$ ls -al dist/
total 441144
-rw-r--r-- 1 user staff 131B 12 10 15:39 builder-effective-config.yaml
-rw-r--r-- 1 user staff 53M 12 10 15:39 electron-tips Setup 1.0.0.exe # Windows用インストーラー
-rw-r--r-- 1 user staff 58K 12 10 15:39 electron-tips Setup 1.0.0.exe.blockmap
-rw-r--r-- 1 user staff 72M 12 10 15:40 electron-tips-1.0.0-mac.zip
-rw-r--r--@ 1 user staff 74M 12 10 15:39 electron-tips-1.0.0.dmg # macOS用インストーラー
-rw-r--r-- 1 user staff 81K 12 10 15:39 electron-tips-1.0.0.dmg.blockmap
-rw-r--r-- 1 user staff 527B 12 10 15:40 latest-mac.yml
-rw-r--r-- 1 user staff 354B 12 10 15:39 latest.yml
drwxr-xr-x 3 user staff 96B 12 10 15:39 mac/ # acOS用インストーラーで配置される.appファイル
drwxr-xr-x 21 user staff 672B 12 10 15:39 win-unpacked/ # Windows用インストーラーで展開されるファイル群
おわりに
以上、ElectronのTipsの紹介でした。参考になれば幸いです💪
明日の担当は @moris4ki です。お楽しみに🎉