Electron

Electronアプリを公開するまでにやったことノウハウまとめ

利用したもの

  • Git
    • npmパッケージでgithubからURL指定でリポジトリから取得してくる時に利用する
  • node.js
    • electronのnode.jsはnpm時に自動的にインストールされる
    • npmをインストールしたり開発向けデバッグ起動に使うものでバージョンは神経質になる必要は無い
  • yarn
    • npmより超高速にインストール出来る
    • npm-shrinkwrap.jsonにあたるyarn.lockがデフォルトで生成されるのでバージョン固定化・統一がしやすい
    • npm install -g yarn
    • yarn && cd src && yarn
      • 本プロジェクトではルートにはelectron他、デバッグ、ビルド用devDependenciesを、実際にパッケージングされる場合に必要なnode_moduleはsrc以下にpackage.jsonを作成しdependenciesに追加して無駄なnode_modulesが本番アーカイブに含まれないように物理的に切り分けを行いました。
      • yarnで一度インストール後、node_modules以下を消してしまった場合は、yarn --check-filesとすれば実際にファイルがあるかどうかをチェックしてインストールしてくれます
  • アプリ起動
    • npm run start
package.json
"scripts": {
  "start": "cross-env NODE_ENV=development electron src"
}
"devDependencies": {
  "cross-env": "^4.0.0",
  "electron": "^1.6.11"
}

パッケージング(electron-packager)

アプリ実行形式のアーカイブを作成しました。ソース難読化など行わず、リリース形式の途中過程ですがパッケージングを行うことによりデモやテスターへの配布を可能にしています。

  • 実行しているOSの実行形式パッケージング
    • npm run package
package.json
"scripts": {
  "package": "electron-packager --package-manager yarn src"
}

yarnによってインストールしている場合、--package-manager yarnを付加しないと正しくnode_modulesを引き継がないので注意が必要です。

  • Win/Macに関しては、バージョンは問いませんが各OS上で実行した
  • WinでMacのパッケージングは実行は出来ますが今のところ正しくパッケージング出来た実績がありません

ソースコード難読化(javascript-obfuscator, crypto)

Electronはアプリソースをapp.asarというファイルにアーカイブして実行しますが、これは高速化の目的でしか無く、単に圧縮している程度でソースは平文のままです。もし、ソースを公開したくない場合は難読化を行うことが必要です。以下の対応を行いました:

gulpfile.js
var gulp = require('gulp');
var js_obfuscator = require('gulp-javascript-obfuscator');

gulp.task('build', () => {
  gulp.src('src/*.js')
    .pipe(js_obfuscator())
    .pipe('dest/')
}) 
  • 解読された場合でも非公開WebAPI等のエンドポイントを即座に読むことはできないよう暗号化(cryptoモジュールを使用)
暗号化例
let encrypt = (text) => {
  let cipher = crypto.createCipher('aes256', 'xxx')
  let crypted = cipher.update(text, 'utf8', 'base64')
  crypted += cipher.final('base64')
  return crypted
}
復号化例
let decrypt = (text) => {
  let decipher = crypto.createDecipher('aes256', 'xxx')
  let decrypted = decipher.update(text, 'base64', 'utf8')
  decrypted += decipher.final('utf8')
  return decrypted
}

デバッグ

Electronはnode.jsで動くメインプロセスとChromiumで動くレンダープロセスで分かれており各プロセスでデバッグ方法が異なります。

レンダープロセス

  • Chromeとほぼ同じDOMインスペクタが使えます。また、画面をリロード出来る様にしておくと便利です。
  • 但し、ソース非公開プロダクトの場合は公開アプリでショートカットが利用出来ると困るので、NODE_ENV環境変数にてdevelopment環境を定義出来るようにします。
  • ウィンドウにフォーカスを当てた際のアクティブウィンドウに対してDOMインスペクタがChromeと同じように起動したり、画面をリロード出来るようにショートカットを次のように登録します:
if (process.env.NODE_ENV==='development') {
  app.on('browser-window-focus', (event, focusedWindow) => {
    globalShortcut.register( process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', () => focusedWindow.webContents.toggleDevTools() )
    globalShortcut.register( 'CmdOrCtrl+R', () => focusedWindow.reload() )
  })
  app.on('browser-window-blur', (event, focusedWindow) => {
    globalShortcut.unregister( process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I' )
    globalShortcut.unregister( 'CmdOrCtrl+R' )
  })
}

メインプロセス

Windows / macOS 共にVSCodeによるデバッグが安定しています。
electron 1.7.4よりinpectorプロトコルに対応し、.vscode/launch.jsonもwindows起動ファイルのクロスプラットフォーム記述方法に対応しており、リポジトリにコミットしておくとデバッガ起動が共通化できます。以下がデバッグ方法の手順です:

  • VSCodeをインストールしてアプリプロジェクトを開く
  • .vscode/launch.jsonを以下のように記載:
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "env": {"NODE_ENV": "development"},
      "cwd": "${workspaceRoot}",
      "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
      },
      "protocol": "inspector",
      "args" : ["src"]
    }
  ]
}
  • VSCodeのデバッグ画面より「デバッグの開始」にてデバッグアプリが起動しデバッグ開始します
  • あとはソースにブレイクポイントを設定してデバッグ可能になります

クラッシュ(例外)レポート

  • ElectronにはcrashReportというクラッシュレポート機能がありますが、利用してもElectronプロセスを本当にクラッシュさせないと報告してくれません。
  • また、Mac App Storeにてアプリ公開する場合、クラッシュレポートを実装しているとAppleの審査で外してくれとなって現状ではリリース出来なくなります。
    • パッケージング時に--platform masと指定することでAPIが実装されていても呼び出されなくなる仕組みはElectronでは用意はされています。
  • そこでuncaughtExceptionやunhandledRejectionイベントが発生した場合にcrashReportで利用されているPOSTリクエストをnetモジュールを用いて自分で発生させています。
process.on('uncaughtException', (exception) => {

  let report = {
    application_version: app.getVersion(),
    electron_version: process.versions.electron,
    chrome_version: process.versions.chrome,
    platform: process.platform,
    user_agent: session.defaultSession.getUserAgent(), 
    process_type: process.type,
    _version: app.getVersion(),
    _productName: (アプリ名)
    prod: 'Electron',
    _companyName: (ベンダー名),
    exceptionStack: `UnhandledRejection: ${exception}`,
  }
  let request = net.request({
    method: 'POST',
    url: (crashReportサーバーURL),
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    }
  })
  request.end(JSON.stringify(report))

})

自動更新(autoUpdater)

ElectronはautoUpdaterという自動更新機能があり、アプリケーションに新しいバージョンが存在したら自動でダウンロードして再起動するとアプリが更新されているサイレントアップデートが出来ます。

autoUpdater.on('checking-for-update', () => {
  console.log('checking-for-update')
})
autoUpdater.on('update-available', (info) => {
  console.log('update-available')
})
autoUpdater.on('update-not-available', (info) => {
  console.log('update-not-available')
})
autoUpdater.on('download-progress', (progress) => {
  console.log(`download-progress: ${JSON.stringify(progress)}`)
})
autoUpdater.on('error', (error) => {
  console.log(`error: ${error}`)
})
autoUpdater.on('update-downloaded', (info) => {
  console.log('update-downloaded')
})
autoUpdater.on('login', (authInfo, callback) => {
  console.log('login')
})

autoUpdater.setFeedURL('https://example.com/update/')
autoUpdater.checkForUpdates()

Electronの標準のautoUpdaterは更新の仕組みとしてSquirrel Frameworkを採用しており、macOSに関しては最新バージョンチェックの為のAPIを提供する必要がありサーバが必須となっています。本格的なクラウドを持たない場合でも、heroku+github や Amazon API Gatewary+Lambda+S3等で提供可能ですがデスクトップアプリケーション更新のためだけに構築するのは運営コスト的にかなりきついです。

自分達のプロジェクトではelectron-builderモジュールによるインストーラの作成とそれに付随するeletron-updaterモジュールによる標準のautoUpdater APIと互換性のある自動更新を利用することにより、Web上にアプリ更新情報と更新用アプリケーションを公開することでファイルのデプロイのみで自動更新出来る様にしました。

electron-builder

インストーラの作成が出来るモジュールで自動更新のためのelectron-updaterモジュールのための更新情報ファイルの生成なども考慮されています。本プロジェクトではWindowsはNSISインストーラ、macOSはdmgによる配布を選択しました。

electron-updater

electron-updaterはelectron標準で提供されているautoUpdaterとAPI互換のサーバーレス自動更新のための仕組みです。利用するにはautoUpdater APIをelectron-updaterよりrequireしておきます。

const autoUpdater = require('electron-updater').autoUpdater

執筆時点でv2.8.9を利用した場合、Web上で参照可能なフォルダにWindowsはlatest.yml、macOSはlatest-mac.ymlを自動更新情報として設置し、同一フォルダに更新ファイルを置くことでElectronはバージョンが更新されていたら自動的に更新ファイルを取得してきて準備が出来たら再起動時にサイレントインストールされてバージョンアップする仕組みになります。

latest.yml
version: 1.0.0
path: Electron Application Setup.1.0.0.exe
sha2: 890c9d95fc54af79db57dbfd17d793940f4f88d4b8050dfdfd623e0ee5b182e0

Win用のlatest.ymlはelectron-builderで自動生成されるものにはreleaseDate、githubArtifactName、sha512も付加されてくるが、electron-updaterにはsha2かsha512どちらかがあれば良く、sha512チェックで一致しなくて自動更新出来ない場合があったのでsha512側を省略しました。

latest-mac.yml
version: 1.0.0
path: Electron Application.1.0.0.app.zip
sha512: >-
  093e6d9fc176dc9949d4c72a1a39ba2082a599d1d3093c736f4276e8562db0a193b1b29dc60dda05907f83a2c1c8c14c1f75d26e7ba6b75fc3576fca94c13fb1

Macの方は逆にsha512のみチェックサム対応していました。

一通りリリースまで行ってみて感じた良い点・懸念点

良い点

クロスプラットフォーム開発基盤にありがちなランタイムを必要としない

Electronはnode.js及びChromiumを内包してWindows/macOS/Linux環境にNativeバイナリを提供出来るようになっています。これは、ランタイム依存やChromeブラウザを必要とせずスタンドアロンで起動出来る為、ユーザー環境差異起因の不具合が起こりにくくなります。

Webアプリケーションのデスクトップアプリケーション化が容易

Chromiumをレンダリングエンジンとして採用している為、Chromeで動作するものはほぼ動きます。Webアプリケーションが既にあって、それを元に少し機能拡張するようなデスクトップアプリケーションを作りたい場合は、殆どのフロントエンド側のソースコードを流用、または共通化することが可能になるので開発基盤選定の第一候補となり得そう。

フロントエンド開発リソースのデスクトップアプリケーション開発への転用が容易

GUI表示部分はレンダープロセスによってHTML/CSS/JavascriptをChroniumブラウザを利用して表示させる為、Webアプリケーション開発を行った経験がある場合は殆どやり方に違いはありません。また、メインプロセスにおいてもnode.js、つまりJavascriptで記述する為、デスクトップOS固有のファイル操作等はありますが、学習の敷居は低いと思います。フロントエンジニアリソースを強みとするような開発体制の場合にデスクトップアプリケーション開発への転用を行いたい場合には非常に魅力的な選択肢になりそう。

懸念点

アプリケーション構築の為のアーキテクチャを自分で決める必要がある

Electronは起動すると必ずnode.jsが動作するメインプロセスと、Chromiumエンジンを使って画面レンダリングが出来るレンダープロセス、必要最低限のAPIが用意されているシンプルな仕組みになっています。巷のフレームワークの様な規約や規定は無く、なにも設計思想なく実装していくと、メインプロセスでもレンダープロセスでもどちらでも殆どの機能が実現出来てしまい、可読性も下がっていく為、拡張・修正等を繰り返す内にいずれメンテナンスしにくいコードになりそうです。

今回私たちが作ったプロダクトでは試行錯誤を繰り返した結果、以下のような設計指針を作り、なるべく統一感のあるコードになることを心がけました:

  • レンダープロセスでは表示と画面操作に関わる処理のみに極力抑える
    • Chromiumブラウザによってレンダリングしている為、Chromeブラウザと同じように何らかの処理を行って万が一例外が発生したとしてもブラウザコンソールにエラーが表示されるだけで処理が止まり不具合を検知しにくいので、ファイル操作や通信、長い処理は全てメインプロセスで行う。
  • MVCライクなBrowserWindowをラッピングするXxxWindowクラスを作成し、編集→進捗→完了画面の様な機能的に同一な画面遷移と捉えられるものを同一クラスのメソッドで管理して処理する。
  • 各WindowクラスはApplicationWindowクラスを基底クラスとして継承させ、uncaughtExeptionのハンドリングやダイアログの表示など、共通化できるものを管理する。

ソースコード隠蔽が困難

Electronではnode.js及びChromiumを利用してJSコードをロードしてアプリケーションを動作させる仕組み上、難読化は行えても物理的に解読が不可能な状態まで行うことは出来ません。ハックされると大きな損害を生むようなアプリケーションでは選択肢としては外す方が無難です。

セキュリティ対策の仕組みがElectron自身にはない

Electronはデスクトップアプリケーションであるので、Chromeで開発していたときのようにWeb脆弱性の対策のみで無くコマンドインジェクションやOS固有のセキュリティ対策が必要となります。それ向けの対策の仕組みは残念ながら無いので個別に自分達で一つ一つ脆弱性を潰していく必要があります。

セキュリティ対策の整ったWebフレームワークを採用する等でも軽減は出来ますが、メインプロセスでOS側へのアクセスを試みた場合はやはり対応を行っていく必要があります。