create-react-appとtypescriptでelectronをやる


2020/08/28 追記

electron-webpackを使った新作を書きました。
electron-webpackでTypescript+Reactプロジェクトを作成する

背景

  • create-react-appは便利
    • typescript対応もしている
    • webpack書かなくて良い
  • electronは便利
    • フロントエンドの表現力
    • ローカルへのアクセス

ということで適当に足したら爆発したため、まとめていきます。

動くプロジェクトはこれです
thalathalaylah/create-react-electron-typescript-app

記事の構成としては、
1.やること
2.やったこと
3.なぜなのか
の3部構成です。何をどうしたかだけ気になる方はやったことだけ読んでいただければ。

参考文献

1.やること

  • create-react-appから作成したプロジェクトにelectronを組み込む
  • Nodeの能力が利用できることを示すために「ボタンを押すとファイルを生成する」機能を実装する
  • ReactDevToolsを利用可能にする

2.やったこと

コミットのリンクと、コミットでやったことを書いていきます

Initial commit from Create React App

  1. yarn create react-app <project name> --typescript でreact+typescriptのプロジェクトを作成

[add] electronからcreate-react-appのページを表示できる

  1. electron-quick-startのmain.jsをsrc_main/entrypoint.jsにコピー
  2. src_main/entrypoint.jsのBrowserWindowを生成している箇所で、webPreferenceを空にする
  3. src_main/entrypoint.jsmainWindow.loadFile('index.html')mainWindow.loadURL('http://localhost:3000')に変更
  4. package.json"main": "src_main/entrypoint.js"を追加
  5. package.jsonのscriptsに"electron": "electron ."を追加
  6. yarn add --dev electronでelectronを追加
  7. この時点でyarn startが起動している状態でyarn electronを実行するとelectronでcreate-react-appのページが表示できる

[add] packageコマンドでappを生成できる

  1. コミットの内容ではないが、Macの場合はbrew install wineしておく
  2. src_main/entrypoint.jsapp.isPackagedによってmainWindow.loadURLの読み込み対象をurlかpathか分岐させる処理を追加
  3. package.json"homepage": "./"を追加
  4. .gitignore/releaseを追加
  5. package.jsonのscriptsに"package": "yarn build && electron-packager . my-app --platform=all --arch=x64 --prune --out=release --overwrite"を追加
  6. この時点でyarn packageによってパッケージを生成し、yarn start行わずに生成されたパッケージからアプリを起動できる

[add] packageコマンドではMac用のビルドのみ行う

  1. package.jsonのscriptsでpackageコマンドをpackage-allにリネーム、packageコマンドはplatformをdarwinのみ指定するようにした

[add] devコマンドからサーバー起動とelectron起動を同時に行うことができる

  1. yarn add --dev foremanでforemanを追加
  2. dev_env/Procfileを追加
  3. dev_env/Procfileから読むdev_env/electron-wait-react.jsを追加
  4. package.jsonのscriptsに"dev": "nf start -j dev_env/Procfile"を追加
  5. src_main/entrypoint.jsで文字列でURLを指定している部分を環境変数から読むように変更

[add] ReactDevToolsの追加

  1. 手元のChromeにReactDevTools extensionをインストール ReactDevTools
  2. dev_env/.dotenvを作成し、ELECTRON_DEV_TOOLS_PATHという環境変数を定義し、インストールしたReactDevToolsのパスを設定(コミット上ではtemplateとして追加している)
  3. package.jsonのdevコマンドがdev_env/.dotenvを読み込めるように変更
  4. app.isPackagedがfalseの時にELECTRON_DEV_TOOLS_PATHからReactDevToolsを読むように変更
  5. app.isPackagedがfalseの時は最初から開発者ツールが開くように設定、付随してウィンドウサイズも大きくする

[add] ボタン作成

  1. src/CreateFileButton.tsxを作成
  2. CreateFileButtonをsrc/App.tsxから読み込む
  3. この時点でelectronアプリのページの下方にクリック可能なボタンが生成されるようになる

[add] uuidで作成するファイル名を生成

  1. yarn add --dev uuid @types/uuid
  2. src/CreateFileButton.tsxでuuidによってファイル名を生成し、console.logで出力するように変更
  3. window.requireでelectronを読み込めるようにdeclare globalでWindow interfaceにrequireを追加
  4. electronからRemoteをimport
  5. window.require('electron').remoteでRemote型のオブジェクトであるremoteを取得

[add] ボタンを押すとファイルが生成される

  1. .gitignore/testを追加
  2. fspathfsTypepathTypeとしてimport
  3. remoteからfspathをrequireし、typeof fsTypetypeof pathTypeで型付けする
  4. ファイルを生成する処理を書く

これで無事、ボタンを連打するとファイルが大量に生成されるアプリが完成しました!

3.なぜなのか

Initial commit from Create React App

特筆することは無いです、スッと作る

[add] electronからcreate-react-appのページを表示できる

ここはBuilding an Electron application with create-react-appとほぼ同じですが、typescriptを使うために一部調整しています。
まず、webPreferenceを空にしているのはelectron-quick-startにpreload.jsを読み込む仕組みが組み込まれたためです。
今回のアプリにはpreloadする処理は無いため、preloadの処理を消しています。
また、entrypoint.jssrcに入れずsrc_mainに入れています。これは、src下のファイルはcreate-react-app内部で設定されたwebpackの支配下にあるため、適当にjsを入れておくとエラーが発生してしまうためです。

[add] packageコマンドでappを生成できる

brew install wineはWindows用のビルドのために必要となっています。
app.isPackagedはdev環境か否かの判別に使っています。Package化している場合はProduction、Package化していない場合はDevelopという判定です。
package.json"homepage": "."を加えるのはBuilding an Electron application with create-react-appの通りで、これが無いとpackage化した際にindex.htmlから他のファイルが読めなくなります。
packageコマンドの内容はReact+Electronアプリを作ってみようの通りです。

[add] packageコマンドではMac用のビルドのみ行う

electronのv5.0.3で試したところ、packageコマンドにかかる時間がv5.0.1と比べて2倍くらい長くなったため開発中はMacのみをターゲットにビルドすることにしました(利用しているOSを対象にすると良いでしょう)。

[add] devコマンドからサーバー起動とelectron起動を同時に行うことができる

Building an Electron application with create-react-appの通りです。
React+Electronアプリを作ってみようではnpm-run-allを使うと良い、と書いてありますが、yarn startが起動し終わるのを待たずにyarn electronが走ってしまうため、electron側をリロードせねばならないのが良くないと思いました。
そのため、今回はforemanの方を採用しています。

[add] ReactDevToolsの追加

DevTools Extensionにある通りです。
このアプリでは先述の通り、app.isPackagedでdev、prodを区別しているため、app.isPackagedがfalseの場合のみ有効にしています。
ここで、devでReactDevToolsを有効にした場合、有効にしたことが~/LibraryApplication Support/<App名>に記録されるのですが、package化したprodも同じ場所を参照するため、prodでも有効なままになります。
同ディレクトリを削除した状態でprod環境のものを起動した場合は、無効なまま正常に起動するのでご安心を。

[add] ボタン作成

シュッとボタン作るだけです。

[add] uuidで作成するファイル名を生成

ここはuuidでファイル名を生成するというコミットメッセージになっていますが、うっかりelectronのremoteオブジェクトを取得するところまでやっています。
electronの能力にアクセスするためにelectronをimportしたいのですが、普通にimportを書いてしまうとtsからのトランスパイル時にcreate-react-app側のwebpackが解決しようとするため、失敗します。
これを回避するために、window.requireというものがelectronでは定義されています。
しかし、create-react-app+typescriptの文脈ではwindowはWindowインターフェースを実装したものなので、window.require('electron')などと書いてもトランスパイル時に「window.requireなんてメソッドは無いよ」と言われてしまいます。
そこで、

declare global {
  interface Window {
    require: any;
  }
}

と書いてやることでrequireを呼べるようにします。
また、window.requireは上に書いた通りanyが返ってしまうため、electronのRemoteの型情報はimportしておいて、型付けを行っています。

[add] ボタンを押すとファイルが生成される

ここからはwindow.requireからではなくremote.requireでnodeの機能を呼び出しています。
window.requireでも呼び出せるのですが、Breaking Changesを見た感じremote.requireから呼ぶ方が良いのかな、特にv6.0.0以降は、という感じです。
fspathをimportしていますが、これはremote.requireでこれらを取得した際に型をつけるためです。
型定義ファイルを見るとわかるのですが、fspathはモジュールであってinterfaceとかではないので、typeofで無理やり型にして型付けをしています。

コンクルージョン

これで無事electronからcreate-react-app+typescriptを利用することができるというわけです。
electron+create-react-appの場合の罠とelectron+typescriptの場合の罠が複合して出現したため厳しい気持ちになりましたが、これで型付きelectron生活を無事送ることができるようになりました。