2020/06/02

あつまれ Electronの森

任意タイムゾーンの日付時刻を複数表示するアプリ作ってみた

screenshot

ちょっとわけあって、デスクトップに複数のタイムゾーンの時刻を表示するだけの小さいアプリが欲しくなったので Electron に入門して、ひとしきりやりたいことは全部やってみたのでメモを残す。次またなんかメンテするときに自分でも忘れてると思うので備忘録。

レシピ

Electron経験値は数年前にHello Worldだけやってそれっきり。JavaScriptはES6からおいて行かれてる感じ。nodejs/npmは多少なじみがある。今回はElectron自体はクロスプラットフォーム向けだけど、自分はWindows環境で動くものが欲しいのでアプリの開発もビルドもWindowsでやる。結果論だけど、nodeのインストールからnpm/Electronのもろもろやビルド含めてWindows だからという理由で問題になることはなかった。思ったものが思った通りすんなり動いてびっくりした。

ちなみにElectronが小さいデスクトップアプリに向いてないというのは承知している。わし、Webエンジニアなんじゃ。

はまりどころ

electron-prebuilt はとうに deprecated

古い入門記事に当たると electron-prebuilt を入れて使えみたいなことが書いてあるけど、electron-prebuilt はとうの昔に deprecated らしいので使わない方がいい。ちまたにあふれる electron 入門記事をやってみるくらいのことは electron-prebuilt でも動いたりするけど、まじバージョンが古いのでちょっと先に進むとあれこれ意味の分からない構文エラーが出始めるので最初から入れない方が良い。

著名ライブラリが Unexpected token っていう構文エラー吐くとき

こういうやつ。

...options
^^^
SyntaxError: Unexpected token ...

ひとつ上で書いた通り、たぶんうっかり electron-prebuilt を入れてしまっていて Electron/node のバージョンが古いと思うので最新版をちゃんと手に入れれば直る。ES6使えてないだけの構文エラー。

electron-packager は動くけど electron-builder の方が良い

見出しの通り。自分の手元のOSでビルドして使うだけなら electron-packager でもよい気もするが、electron-builder の方が細かいことがサポートされている様子なので配布する前提なら electron-builder 使う方が良さそう。

もちろん electron-builder 以外にもパッケージング/ビルドをするモジュールは存在します。

electron-packagerでビルドして生成したファイルはすべて必要

上でbuilder の方使っとけと書きつつTIPS書くことに多少の抵抗を覚えますが、electron-packagerでアプリをビルドするとなんかファイルがいっぱいできて、Windowsならおなじみの .exe が生成されるので、それひとつで動いてくれるかなと思ったらぜんぜんそんなことはなくて、ビルドしてできるディレクトリの中身全部まるっと欠かさず必要でびびる。ちょっとしたアプリなのにこんなにファイル必要なんかい!と誰もが突っ込んでしまうところ。(たぶんはじめてのElectronビルドあるある)

アプリローカルの node_modules/ が消せねえ

管理者権限が必要、と言われ「続行」を選択して削除できるかと思わせたところで、ユーザ名(たぶん自分自身)の許可が必要です、というようなエラーが出て「再試行」を何度やっても削除できないパターン。

開発中のアプリが起動しているかなんかで node_modules 以下がロックされてるので、アプリや開発ツール系を全部閉じると消せる。なんかのエラーで開発アプリが死んでプロセス残っているときはタスクマネージャーから掃除する。(はじめてのWindowsでの開発あるある)

Hello Worldから先のあれこれ

ここからは、公式チュートリアルで生成したアプリから手を入れてお好みにしたあれこれ。(githubにあがっている実装とは乖離している可能性があります)

アプリの位置やウィンドウサイズの保存/復元

electron-window-state というnpmモジュールで一撃。超便利。でもなんかアプリを移動してすぐに閉じたとき、最後の移動分が残らなかったりすることがあるような気がしてるけど、まあそんな頻繁に位置を移動するアプリでもないのでいったん忘れる(アプリ終了時の処理が漏れてるとかかな、、)。

設定を外部ファイル化

使う人によって必要なタイムゾーンは異なると思うので、その辺外部ファイルで好きなように設定できるようにする。

npmモジュール electron-store で一撃。schema設定を書くだけでvalidationもできて便利。

Electron.BrowserWindow で特筆すべきオプション

特筆というほどでもない。公式ドキュメント がとても詳しいです。

// アプリの枠を消す
frame: false,
// 透過する 透明度は opacity
transparent: true,
// アプリウィンドウをサイズ変更できなくする
resizable: false,
// 影を付けない
hasShadow: false,

自分の場合、時計を自動で隠れるタスクバーの裏に配置するので、常に最前面に位置させていない(デフォルト)のだけど、時計を置く位置によっては最前面にもっていった方が便利かもしれない。

// 常に最前面に位置させる
alwaysOnTop: true,

CSSでやってること

/* スクロールバー出さない */
::-webkit-scrollbar {
  display: none;
}

/* アプリウィンドウのドラッグを許可。アプリ内テキスト選択禁止 */
html {
  -webkit-app-region: drag;
  -webkit-user-select: none;
}

preload.js と nodeIntegration: false

Electronがmainプロセス(nodejs Native)とrendererプロセス(Chromium)で動いているというあたりの説明は省略しつつ。

mainプロセスのElectron.BrowserWindow のコンストラクタオプションで以下のように指定することで nodeIntegration: false (デフォルトfalse)にしつつ、preloadオプションに指定した js のみでnodejsライブラリを読む。

webPreferences: {
  nodeIntegration: false,
  contextIsolation: false,
  preload: Path.join(__dirname, 'preload.js'),
}

このことによって、nodejsライブラリが preload.js 以外の場所で読まれて実行されることを防げる。セキュリティ向上のための仕組み。

preload.js はこんな感じ。

process.once('loaded', () => {
  global.Moment   = require('moment');
  global.Timezone = require('moment-timezone');
});

rendererプロセスからは global が window として参照できる。

const moment = window.Moment.tz(timezone);

なお、後にもでてきますが、実はセキュリティ上 contextIsolation: false 設定は好ましくなく、これを true にすると global が共有されなくなりますが、たぶん true でやっていく方が良い。設定の意図するところは、mainプロセスとrendererプロセスをそれぞれ別プロセスにする(true)。

メインプロセスとレンダラプロセス間でipc通信

Electronがmainプロセスとrendererプロセスで動いているというのを上で書いておいて詳細を割愛しましたが、まあとにかくmainとrendererというのがいて、いわゆる親子みたいな感じで基本別プロセスになってるっぽいんですけど、このプロセス間でデータをやり取りする仕組みというのもあるわけですね。

mainプロセスでconfigファイルを読んで getClocks イベントを用意しておく。

const config = new Store();
const clocks = config.get("clocks", [
  { name: "Tokyo", timezone: "Asia/Tokyo" },
  { name: "UTC",   timezone: "UTC" },
]);
Electron.ipcMain.on("getClocks", (event, arg) => {
  event.returnValue = clocks;
});

renderer側はこんな感じで値をもらえる。

const clocks = window.Electron.ipcRenderer.sendSync("getClocks");

非常に簡単にmainプロセスとrendererプロセス間で通信できたわけですが、これは BrowserWindowのコンストラクタオプション contextIsolation の設定が false (デフォルト true)を前提にしています。ipc通信まわりというか、グローバルな存在な何かとか、基本的にそういうのはセキュリティの危険が危ない代表格なので、最近のElectron界隈では contextIsolation:true で作るのが王道のようです。知らんけど。

contextIsolation:true の場合、ipc通信を安全に行うのに contextBridge という APIを使うのが良いそうです。(むしろ contextIsolation:false だと contextBridge は使えない)

簡単に言うと、rendererプロセスはいわゆるクライアントビューの処理/DOM周辺処理に集中させて、それ以外の外部との通信やファイル入出力含めたIOなんかはmainプロセスでやるって感じ?かな?だよね?

ちなみに今回作ったアプリの場合、configファイルの内容によってウィンドウサイズを変更したかったりしたので、mainプロセスの方で読んでipc通信でデータをrendererプロセスに渡すようにした。(たぶんこれは今後逆のipc通信もしてウィンドウサイズをいまより良い感じに設定できるようにするつもり)

IPC通信で送受信できるデータ

IPC通信するときのデータはシリアライズして送信される。 シリアライズできる構造体はここに書かれている 。このことに気づかず、以下のようなコードを書いてぜんぜん受け取れなくて2時間くらい泣きそうだった。

const Timezone = require('moment-timezone');

contextBridge.exposeInMainWorld(
  "mclocks", {
    Timezone: () => {
      return Timezone; // [native code] は渡ってこない
    },
  },
);

そもそもIPC通信なのですごく大きいデータとかも避けた方が良い模様。

CSP warnings

Electronアプリを起動してconsole を表示すると何やら警告が表示されることがあります。

Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security Policy set or a policy with "unsafe-eval" enabled. This exposes users of this app to unnecessary security risks.

書いてある通りですが、これは CSP と呼ばれる Content-Security-Policy がないから unsafe-eval になってるよ、という警告です。

コンテンツセキュリティポリシー (CSP)

例えば以下のようなタグをHTMLに追加すると警告は消えます。

<meta http-equiv="Content-Security-Policy" content="script-src 'self';">

セキュリティが向上するかわりにいくらかコード上許容されない表現がでてきます。今回の場合は以下の setInterval に関数を直接書いていたのがダメと言われました。

setInterval('tick()', 1000);

以下のように書き換えて回避しました。

setInterval(function(){
  tick();
}, 1000);

eval的なのはだめよってことですね。インジェクションにつながる可能性があるので。ちなみにこの CSP自体は Electronとは直接関係あるわけではない。Webコンテンツ HTML + JSの話。

謎の 8pxマージン

知りうる限りのCSSの知識を活用して、今回のアプリのウィンドウを調整していたが、なぜか body の下部にマージンができてしまう。なんやこいつ。dev toolで確認すると、斜体になった user agent stylesheet というのがいて、これが margin: 8px; を設定しているようだった。

https://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css

ブラウザ依存のCSSということで、少し検索すると無効化するCSSがいくつか見つかるので良さそうなやつ使う。

https://github.com/richclark/HTML5resetCSS/blob/master/reset.css

ビルドフェーズがうまくいかない場合

以下の記事がビルドフェーズのトラブルシューティングにめちゃくちゃ効いた。記事自体はElectronのデバッグ全般を対象としたものだけど。

Electronのアプリが落ちたり、画面がまっしろになっってしまった時の、問題の追い方

  • electron-log 入れる
  • asar モジュールで app.asar (packされた中身) が確認できる

See also:

いまだよくわからんこと

  • mainプロセスとrendererプロセスの使い分けはいまいちわかってない。通信コストもわかってない。
    • 特に接続切断毎度やるわけじゃなさそうだしオンメモリのRPCコールっぽいのでそんな遅いとは思ってない
  • 開発回りとかビルド周りのモダンベストプラクティスとかまったくわからん
    • Googleに頼ると古い記事ばっかひっかかってくる
    • 2018年より前の記事はたぶんあまり信じない方がいい
    • 安定と信頼の公式ドキュメント
  • dev向けのpackage.jsonとビルド用のpackage.jsonの中身の差分がいまいちわからん
  • Mac向けビルドと配信は準備物が多すぎてやる気が出てない

セキュリティ面

セキュリティに関して、自分的にはじめてのElectronアプリではあるものの、取り急ぎ推奨される設定はセキュアな方に寄せて何とかするように実装してみた。取り急ぎ第三者入力になりうるものをザルで扱わないようには気を付けたけどどうだろう。このアプリみたいに時計をいくつか表示するだけのアプリでも、外部設定ファイルをつかったりなんやかんやで第三者入力が入り込むので難しいなとは思った。

あとそもそもElectron自身の中身とかちょっと覗いてみて面白かったのでインターナルもそのうち読み進めてみたい。

まとめ

というわけで、最初、時計をひとつ表示するだけのつもりが、Electronのインストールからアプリ動かすところまで一瞬でできてしまって、なんかやりたいこと全部実装してもそんなに時間かからないんじゃないかな、やってみるか、と思ったのが運の尽きで、時計を表示するだけのことにここまでトータル30時間くらい費やしてしまった(時間が細切れだから効率悪かったのと、案外256x256の.icoファイル作るのに時間がかかったりした)。でもけっこういろんな要素体験できたので良かったし、このアプリ自体はずっと自分のデスクトップに欲しかったものなので満足してる。まだバイナリないし、Windows環境しかビルドできないですけど、良かったら使ってみてください。

mclocks : Multiple timezone clocks🕒🌍🕕

アプリロジックは今後もうちょっとブラッシュアップしたいなと思っている。あとオプションで秒の表示とかできるようにしたい。

では、ごきげんよ~

サイト内検索