Electron アプリのテンプレート 2021

2021/07/20に公開

https://github.com/hokaccha/electron-template-2021

作ったので公開しておくが、こういうテンプレートはメンテしないとすぐ腐ってしまうけどメンテするモチベーションも特にないのでスナップショットという意味合いも込めて2021をつけている。

electron-next

ベースに electron-next というのを使っていて、これがけっこうよくできていた。やっていることはシンプルで、レンダラプロセスに Next.js を使っていて、開発時は Next.js のサーバーを起動し、production build では next export した HTML をいい感じにナビゲーショできるようになっている。これにより Next.js のいい感じのビルドシステムを使えて、開発時には hot reload も可能になっているし、production build は静的な HTML と bundle されたアセットが利用可能になる。賢い。

Next.js 公式にもこれを使った example がある。

https://github.com/vercel/next.js/tree/canary/examples/with-electron-typescript

これをを元に作ってはいるんだけど、ディレクトリ構成があまりにも気に食わなかったので、ほぼ作り直した。

before

.
├── electron-src # mainプロセスのソース
├── main         # mainプロセスの出力
└── renderer     # レンダラプロセスのソース
    └── out      # レンダラプロセスの出力

after

.
├── out
│   ├── main     # mainプロセスの出力
│   └── renderer # レンダラの出力
└── src
    ├── main     # mainプロセスのソース
    └── renderer # レンダラプロセスのソース

圧倒的にわかりやすくなった気がする。元のやつはなんでこんなことになってるんだろう。

Next.js の SSR

Next.js はスピード狂なので可能なところは SSR してしまう癖がある。速いのはいいことだ。しかし、SSR するということは Node.js で実行するということであり、当然ブラウザでしか使えない API を使うとエラーになる。Web だったら LCP とか FCP を気にして可能な限り SSR/SSG したほうがいいけど Electron なのでそんなことは一切考えたくない。

Next.js が SSR しないようにするためのハックとして、dynamic import で ssr: false にするという技が知られているのでこれを使う。

pages/index.tsx
import dynamic from "next/dynamic";

const App = dynamic(async () => (await import("~/components/App")).App, { ssr: false });

const IndexPage = () => {
  return <App />;
};

export default IndexPage;

こんな感じにして App.tsx にメインの処理を書くようにする。これによって気兼ねなく localStorage を使うもよし document にさわるもよし。となる。

IPC

IPCするのに preload.js というのを main プロセスで呼んで ipcRenderer を global に expose しているコードがある。

https://github.com/vercel/next.js/blob/41b4fc368801146bbbcd235615deac77dbd92c66/examples/with-electron-typescript/electron-src/preload.ts

Next.js 公式にある with-electron-typescript だと @types/node のバージョンが v14 なんだけど、これを v16 にすると上記のコードが

Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature

という TypeScript のエラーを吐くようになったので Node.js の global でなく window を使うようにした。

https://github.com/hokaccha/electron-template-2021/blob/f604aa75ca3773ad11c0fb37e89f56a3f382f90f/src/main/preload.ts

そもそもなんで元のやつが Node.js の global を使っているのかよくわかってない。

ESLint

Electron アプリとはあんまり関係ないけど eslint の設定もだいたい前回作ったプロジェクトからコピペして現代との diff を埋めることが多いのでこのスナップショットに含めている。基本的な方針はてっぺいさんのを参考にしている。

https://zenn.dev/teppeis/articles/2021-02-eslint-prettier-vscode

Next.js の lint を使ってもよかったのかもしれないけど main プロセスもあるので変にはまるよりは自前で書いたほうがいいかと思って自前で書いている。

https://github.com/hokaccha/electron-template-2021/blob/f604aa75ca3773ad11c0fb37e89f56a3f382f90f/.eslintrc.js

プラグインの構成はこんな感じ。

extends: [
  "eslint:recommended",
  "plugin:import/recommended",
  "plugin:@typescript-eslint/recommended",
  "plugin:react/recommended",
  "plugin:react-hooks/recommended",
  "prettier",
]

いたってフツーだけど、久しぶりに設定すると全部忘れてるので難しい。いったい人生であと何回 eslint の設定をやればいいんだ。

はまったところとして、path alias を設定したときに import/no-unresolved のエラーがでて解決できなかった。eslint-import-resolver-typescript とか色々試してみたんですけどね。eslint 難しいですね。そもそも import が resolve できるかって tsc で見てくれるので import/no-unresolved いらないね?って気づいたので off にした。

結論

Electron で Next.js 便利でした。

Discussion