Electronでデスクトップアプリを作ってみて

:::note info
viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方は[こちら](#一緒に二次元業界を盛り上げていきませんか)まで。
:::

知り合いの自転車屋さんに、商品のポップを簡単にデザインできるツールを作れないか相談を受けたので、[Electron](https://www.electronjs.org/)を使ったデスクトップアプリを作りました :bike:
その中で感じた気づきを書きたいと思います。

https://qiita.com/keko/items/83a3c87451fe506122ef

https://qiita.com/keko/items/5fc36b3d9885141c7ddb

# Electronを選んだ理由
自転車屋さんが元々使っていたのは、全時代の遺物と言ってもよいレベルのVB.NET（.NET Framework 4.0）Windowsアプリケーションでした。
これをそのまま引き継ぐのであれば、今だとUWP、WPF、Windows Forms（ちょっと古い？）あたりが候補にあがると思いますが、一旦その他の選択肢も踏まえて検討しました。

|| Electron | Tauri | Flutter | UWP | WPF | Windows Forms |
|-|-----------|------------|------------|------------|------------|------------|
| **言語** | JavaScript, TypeScript | JavaScript, TypeScript, Rust | Dart | C#, C++, Visual Basic | C#, Visual Basic | C#, Visual Basic |
| **ホットリロード** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **クロスプラットフォーム** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **アプリサイズ** | 大きい（数100MB） | 小さい（数10MB） | 小さい（数10MB） | 小さい（数10MB） | 小さい（数10MB） | 小さい（数MB） |
| **アプリの自動更新** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **マルチウィンドウ** | ✅ | ✅ | 🔺 | ✅ | ✅ | ✅ |

### 要件
 - Windowsで動作可能
 - マルチウィンドウ
     - 印刷プレビューを常に表示したい
 - 印刷が可能
 - ローカルファイルアクセス
     - 旧アプリの設定を引き継ぐために、ローカルファイルへのアクセスが必要
 - 自動アップデート
     - 機能修正や追加をした際に、都度exeファイルを渡すような運用は避けたい

## フレームワーク選定
上記の要件を踏まえつつどのフレームワークを利用するか検討しました。

WPF、Windows FormsはモダンなUIとは言い難いので早々に外しました。
（マテリアルUIのライブラリを入れればそこは解消できたかもしれませんが）
また、UWPはWindowsストアからインストールする必要があり、アプリが公開されてしまうため除外しました。

Flutterは以前アプリ開発で利用したことがあったため筆頭候補でしたが、Flutter Desctopにおけるマルチウィンドウの挙動が怪しかったのと、メインウィンドウとサブウィンドウで状態を同期する実装が複雑になりそうだったため除外しました。

そして、next ELectronとの呼び声が高いTauriですが、裏側のコードをRustで書く必要があるようで、Rustの知見が無かったことと時間も限られていたため、Electronに落ち着いたという次第です。

# 開発をしていく中で感じたこと
### HTML + CSS + JavaScriptで開発できる強み
やはり、フロントエンドの慣れ親しんだ技術でデスクトップアプリを開発できるというのは、大きな強みです。
UIライブラリを自由に選択することもできますし、React、Vueといったフレームワークを利用できる点も開発コストの削減に繋がります。

また、Webアプリをデスクトップアプリに移植したいという場合も、大部分を流用することができるので非常に楽だと思います。

### Electronのリリース頻度
ElectronはChromiumを内部的に利用しているため、Chromiumのリリースに合わせて8週間毎にアップデートが行われます。

https://www.electronjs.org/ja/docs/latest/tutorial/electron-timelines

個人開発のようなパブリックでないアプリの場合はそこまでシビアに考える必要はないかもしれませんが、公にアプリを公開するとなると、セキュリティの面などからこのリリースに追従していく必要がありそうです。

また、Electronが公式にサポートを提供するバージョンは最新の3つ前までのメジャーバージョンです。
（34.0.0が最新なら31.0.0までがサポート対象）
それぞれのバージョンにEOLが設定されいますが、約半年アプリのバージョンアップをサボるとサポートバージョンから外れるということになり、これは結構な制約であると感じました。
MicroSoft StoreではChromiumを利用したアプリケーションへのメジャーバージョン制限が設けられていたり、このような点は注意が必要です。

ただ、メジャーバージョンリリース時の破壊的変更に関しては公式ドキュメントに分かりやすく記載があるため、しっかりバージョンに追従していけばバージョン更新作業自体は、それほど大変ではない印象です。

https://www.electronjs.org/ja/docs/latest/breaking-changes

どちらかと言えば、Electronのバージョンが上がったと同時に内部のNode.jsのバージョンが上がり、それによって利用しているライブラリの依存問題がでてくる可能性が怖いですね。

### バンドルサイズとメモリ使用量
これはElectronの明らかなウィークポイントで、アプリをインストールした際のサイズが余裕で200MBを超えてきます。
今回はアプリの容量を気にする必要はなかったので問題ありませんでしたが、大規模に配布することや端末のデータ容量に制限がある場合、大きな懸念になりえると感じました。

また、内部的にChromiumが動いていることもありメモリ使用量が他のデスクトップアプリと比較して多いと言われています。
現在普及している一般的なPCであれば気にするほどではないかもしれませんが、特殊な環境下（PCスペックに制限があるなど）で利用する場合はネイティブアプリに軍配が上がりそうです。

### ログ収集
ファーストバージョンではログ収集の仕組みを仕込まずにリリースしてしまい、不具合の検知に苦労しました。

これは必須レベルで入れておいたほうがよいです。

https://www.npmjs.com/package/electron-log

```javascript
const log = require('electron-log');

log.info('info log');
log.warn('warning log');
```

ただし、ログ自体はローカルにテキストファイルとして溜まっていくだけなので、それをどう収集するかは別で検討する必要があります。
最終的にはエラーをキャッチしたら、Webhook URLを利用してSlackへ通知するようにしています。
（この辺りはFlutterならFirebase Crashlyticsを使えて楽なのにと思ったりはしました）

### 自動更新の仕組み
デスクトップアプリで面倒なことは、アプリの更新をどのようにして配布するかだと思います。
Electronではアプリの自動更新をスムーズに行うための[仕組み](https://qiita.com/keko/items/83a3c87451fe506122ef)が用意されており、簡単に実現することができました。
ファーストリリースの際にこの設定をしておけば、利用者・開発者ともにWin/Winになれるのでマストで対応しておいた方がよいです。


### IPC通信がやや冗長
[こちら](https://qiita.com/keko/items/5fc36b3d9885141c7ddb)でも書きましたが、Electronではセキュリティーを担保するためにOSネイティブなAPIに直接アクセスができないようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84371/e8c67035-3726-991e-3e98-960fd50060f4.png)

このせいでメインプロセス⇔レンダラープロセスの処理のコードがやや冗長になりがちです。

下記例では、`deletePop`というIPC通信APIがありますが、これの引数を変えようとした場合、4か所もコードを修正する必要があります:sweat: 

```javascript:preload.ts
import { contextBridge, ipcRenderer } from "electron";
import { Pop } from "./@types/pop";

contextBridge.exposeInMainWorld("myAPI", {
  deletePop: (name: string) => ipcRenderer.invoke("delete-pop", name),
});

```

```javascript:window.d.ts
import { ProcessResult } from "./global";
import { Pop } from "./pop";

interface IMyAPI {
  deletePop: (name: string) => Promise<ProcessResult>;
}

declare global {
  interface Window {
    myAPI: IMyAPI;
  }
}
```

```javascript:HomeView.vue
const deletePop = async () => {
  const result = await myAPI.deletePop(popName);
};
```

```javascript:background.ts
ipcMain.handle("delete-pop", async (_event, name: string): Promise<ProcessResult> => {
  try {
    ...
  } catch (e) {
    ...
  }
});
```

アプリの規模が大きくなれば、preloadスクリプトも肥大化していきコードの見通しが悪くなることが予想されますし、この辺りをどう実装するかがElectron開発のキモだと感じました。

# 最後に
色々と書きましたが、やはりElectronは中小規模のデスクトップアプリ開発をやるならベターな選択肢であると思います。
日々パフォーマンス面のアップデートも行われているようなので、Electronのこれからに期待したいですね :smile: 

# 一緒に二次元業界を盛り上げていきませんか？
株式会社viviONでは、フロントエンドエンジニアを募集しています。

https://vivion.jp/recruit/

また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。