画像をElectron側にキャッシュしておく話

Electronを業務で使っている会社とかあるらしい。
で、自分も活用してみよう。Electronでどんなアプリを作るかって考えたら、

サーバーのデータを参照するElectronアプリも作れるけれど、
でも、それだったら、Webページでも良いわけで、
Electronを使うなら、やっぱりスタンドアローンなアプリにしたいじゃないですか。

しかし、業務で扱うデータって、残念ながら、だいたいサーバー上に置いてある。
そのデータに触れずに業務に役立つアプリを作ることは難しい。

それじゃ、こういう、ローカルとサーバ上、両方にデータがあるElectronアプリにするのが良いのかな、と。

そこで画像キャッシュだ

なぜ、画像かはわからないが、今回は画像だ。
毎回データをサーバーに取りに行かずに、(作るのが重い画像とかだ)
Electronのアプリ内部でキャッシュしておこう、という話。

実装

  • まず最初にimage-cacheを入れておいて、
npm install --save image-cache
  • cacheに画像を渡す処理。cacheから画像を取り出す処理を入れる。
# renderer.js
var imageCache = require('image-cache');

...(中略)...

// tweet.user_image は 画像URL
//
// not found image cache
if (! imageCache.isCachedSync(tweet.user_image)) {
  imageCache.setCache(tweet.user_image, function(error) {
    log.error(error);
  });
  tweet.user_image_link = tweet.user_image;

// found image cache
} else {
  const img = imageCache.getCacheSync(tweet.user_image);
  tweet.user_image_link = img.data;
}
  • angular.jsだけど
  • image-cacheから取り出したcacheは、BASE64な文字になっている。そのままそれを表示して良い。
# renderer.html
<li ng-repeat="item in streams track by item.idx">
  <img ng-src="{{item.user_image_link}}" width="32" height="32">
</li>

まとめ

Electronあまり関係ない話になってしまった。

Electronの画面にHTML落とすと、画面が切り替わっちゃうよね

Electronで立ち上げたBrowserWindowの画面に、
HTMLファイルを落とすと、
そのHTMLがウィンドウ内に表示されてしまう。


格好悪い!!!

これをふせぐには

ドロップした時に、画面が差し替わる問題を防ぐには、次のようなコードを入れておくと良い。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script type="text/javascript">
    document.ondragover = document.ondrop = function(e) {
      e.preventDefault(); return false;
    };
  </script>
</head>

ドロップされたHTML上では、nodeとElectronの機能を使える

この挙動はアプリとして格好悪いが、
それだけはなく、
落としたHTMLではnodeとElectronの機能を利用できてしまう。
(↓のファイルをウィンドウにドロップする。)

# evil.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="js/evil.js"></script>
</head>
<body>
evil
</body>
</html>
# js/evil.js
var fs = require('fs');
var filePath = '/Users/your.name/Desktop/evil.txt';

fs.writeFile(filePath, 'eviiiiiiilll', function(err) {
  if (err) {
    console.log(err);
    return;
  }
  console.log('done.');
});

アプリを操作できるような状況になってるわけだから、
特にセキュリティ的な問題にならないでしょうけどね。

Electron 3.0

結局、この挙動はやっぱり良くないよね、ってことで、
Electron 3.0では、デフォルトで無効にされたのであった。

おわりまる

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

Electronアプリでのエラーの追い方。

アプリが起動もしないで落ちる時

アプリをクリックして起動したら、そのまま落ちる時。
最初に追うのは main processの初期化まわりで問題が起きていないか。
electron-logを入れて、ログ出力するのが良いと思う。


electron-logを入れる (https://github.com/megahertz/electron-log)

npm install --save electron-log

main processのjavascriptコードの上の方に、エラーを拾うコードを追加する。
便利だから、このコードは入れっぱなしでリリースしても良いと思う。

var log = require('electron-log');

process.on('uncaughtException', function(err) {
  log.error('electron:event:uncaughtException');
  log.error(err);
  log.error(err.stack);
  app.quit();
});

electron-logで出力されたエラーログを見に行く。
↓この場所に出力されます。
アンインストール方法という資料があるなら、このログファイルを消す手順も追加しておきましょう。

view $HOME/Library/Logs/YourAppName/log.log

画面がまっしろになる

アプリの画面がまっしろ。何も表示されない。

  • renderer process側で、読み込めていないリソースがあるか、
  • JavaScriptでエラーが起きているのではないか?

Developer Toolを開いて、エラーが起きていないか確認しましょう。

なんらかの操作をしたら、プチっとアプリが落ちる

なんらかの操作をトリガーとして、突然、アプリが落ちる。
↑で紹介した、electron-logのログに何か出てるのではないか?
例えば、このように。
このログは main process で読み込んでいるnpm moduleが見つからなかったエラー。

[2018-10-17 20:11:26:0174] [error] Error: Cannot find module 'about-window'
    at Module._resolveFilename (module.js:485:15)
    at Function.Module._resolveFilename (/Users/taku-o/Desktop/myukkurivoice/release/myukkurivoice/MYukkuriVoice-darwin-x64/MYukkuriVoice.app/Contents/Resources/electron.asar/common/reset-search-paths.js:35:12)
    at Function.Module._load (module.js:437:25)
    at Module.require (module.js:513:17)
    at require (internal/module.js:11:18)

自分はファンクションのパラメータ名と、
変数のスペル名を間違えていて、アプリが落ちる現象に遭遇したことがある。
エラーログが出なくて苦労した。

ダイナミックライブラリを読み込んで、
誤った操作した時も、もちろんアプリが無言で落ちた。
こちらは実装している人には原因がわかりやすい。

アプリにパッケージングしてない状態では動く。パッケージングすると動かない。

electron .

では正しく動作する。electron-packagerなどでアプリに固めると、動かなくなる現象。


どこで動かなくなっているかにもよるけれど、

  • 片方にしか入っていないファイルは無いか?
  • アプリケーションのapp.asarの中に、想定通りのファイルは入っているか?
# asarコマンドで、app.asarの中を覗ける
npm install -g asar
asar e YourAppName.app/Contents/Resources/app.asar dest
  • electron-packagerのコマンドオプションでasar.unpackDirを指定していて、アプリで実行した時にはファイルパスが間違っていたりしない?
  • 実行権限の必要なファイルを、app.asarの中に入れてしまっていないか
  • electron-packagerのコマンドオプションignoreで、実行に必要なファイルを指定してないか

electron-packagerのignoreオプションはRegExpのmatchです。
typescriptのファイルを除こうとして、".ts"と指定したら

contents

という文字列にマッチして、アプリが動かなくなったことがある。

まとめ

アプリが落ちたり、エラーが出たりはしないけど、うまく動かない、は自力で追って欲しい。
まとめたら、個人開発だけど、いろんなトラブルを拾ったなぁ。

electron-json-storage、electron-storeを使ったElectronアプリのテストの話

経緯とか

Electronでよく使われる(?)設定ファイル読込系ライブラリに

がある。

これらのライブラリは"userData"に設定ファイルを置くけれど、
設定ファイルの位置を変更する機能がないので、
単体テストを実行したら、
"userData"に置いていた私が丹精込めて育てた設定ファイルが上書きされちゃったことがある。

// Macならこの位置になる
$HOME/Library/Application Support/$app_name/

どのように対応する?

調べたら、例えば、このように対応したら良いらしい。
https://github.com/electron/spectron/issues/202

Spectronでapp.start()後に、アプリ側でuserDataを変更する。
これで私の設定ファイルは守護られたのだ…

// テストコード(抜粋)
// mainSpec.ts
import {Application} from 'spectron';
import * as assert from 'assert';
import * as temp from 'temp';
temp.track();

describe('window-main', function() {
  this.timeout(10000);

  beforeEach(function() {
    const fsprefix = `_ollfrow_${Date.now().toString(36)}`;
    const dirPath = temp.mkdirSync(fsprefix);
    this.app = new Application({
      path: 'ollfrow-darwin-x64/ollfrow.app/Contents/MacOS/ollfrow',
      env: {NODE_ENV: 'test', userData: dirPath},
    });
    return this.app.start();
  });

(略)
// electron.ts テストされるコード
// テスト時はuserDataを差し替える
if (process.env.NODE_ENV == 'test' && process.env.userData) {                                  
    app.setPath('userData', process.env.userData);
}

// 設定ファイルを使ったテスト

electron-json-storage 4.0

electron-json-storageの4.0では、setDefaultDataPath()が追加され、
設定ファイルの置き場所を変更できるようになった。

electron-storeの方は相変わらず"userData"を読み書きしに行くから、上のコードはそのまま利用中なのですけれど。

Electron製のアプリの見た目をMacふうのアプリにする electron-photonの話

最近、社内でLT(https://labs.gree.jp/blog/2018/10/17291/)があって、そこで話をしたので、その時の内容を書く。
これは、その一部の話。

photonkit

Electronアプリの見た目をMacアプリふうにする、photonkitというライブラリがかつてありました。


(このようになる)

なのですが、このphotonkit。最近、開発が止まっておりまして、
例えば、表示されないアイコンがあるとか、
Pull Requestが取り込まれないとか、そういう問題が出ていました。

electron-photon

photonkitはGitHubで管理されているのでフォークできるし、それほど問題でも無いのですが、
そのうち、photonkitの替わりとなるライブラリを作る人が現れます。
それが、このelectron-photonです。

photonkitから、electron-photonへの移行

この2つ、実現したいことは同じなのですが、実装は違っています。
残念ながらライブラリを差し替えたら、「はい、移行完了」とはなりません。

  • photonkit
<!DOCTYPE html>
<html>
  <head>
    <title>Photon</title>
    <link rel="stylesheet" href="photon.css">
    <script src="app.js" charset="utf-8"></script>
  </head>

  <body>
    <div class="window">
      <header class="toolbar toolbar-header">
        <h1 class="title">Photon</h1>
      </header>
      <div class="window-content">
        ...
      </div>
    </div>
  </body>
</html>
  • electron-photon
<ph-window class="vibrancy">
  <tool-bar type="header">
    <h1 class="title">Toolbar Header</h1>
  </tool-bar>

  <window-content>
    ...
  </window-content>

  <tool-bar type="footer">
    <h1 class="title">Toolbar Footer</h1>
  </tool-bar>
</ph-window>

(↑)テンプレートの書き方も違いますね。
試しに自分のアプリで書き換えてみましたが、
デザインはかなり崩れたものになってしまいました。

まとめ

ということで、新しいライブラリが出てきましたが、
新規に導入するならまだしも、
試した感じ、
既存のphotonkitアプリを書き換えるのは、少し面倒だったよ、というお話でした。

Electron製のアプリの起動を速くするelectron-linkの話

最近、社内でLT(https://labs.gree.jp/blog/2018/10/17291/)があって、そこで話をしたので、その時の内容を書く。
これは、その一部の話。

経緯とか

どうにかして、アプリを速くできないかなー、って情報探してたら
Atomというエディタの起動時間を高速化した時の情報が載ってていたのであった。

http://blog.atom.io/2017/04/18/improving-startup-time.html

Atomは他にもいろいろやって、最終的に50%速くなった、と。

electron-link

https://github.com/atom/electron-link

  • Atomのビルドツール。ライブラリを遅延ロードするコードに変換する。(試してない)
  • でも、使わなくても良いはず。変換結果は分かっているので、手動で書き換えられますね。
    • (実際に使って試してないから、実は変換方式が違っていたらどうしよう。)

書き換え

var log = require('electron-log');

log.error('error found.');

これ(↑)をこんな感じ(↓)書き換えるのじゃ。

var _log, log = () => { _log = _log || require('electron-log'); return _log; };
 
log().error('error found.');

書き換えの成果

  • 手動での書き換えは、書き換え箇所が多くて、それなりに面倒だった。
  • 自分の管理するコードはずぼら実装だったので、20%ぐらい起動速度が速くなった。
    • (BrowserWindowが立ち上がってから、表示されるまでの時間の範囲)
    • (大半はレンダリング時間)

使っているAltJSやフレームワークに遅延ローディングの仕組みがあったら、
そっち使ってもイイヨネ。

ElectronのBrowserWindow間で直接メッセージをやりとりする

最近、社内でLT(https://labs.gree.jp/blog/2018/10/17291/)があって、そこで話をしたので、その時の内容を書く。
これは、その一部の話。

経緯とか要約とか

複数立ち上げたBrowserWindowのインスタンス間でメッセージをやりとりする。
社内のメモで出来ない。いったん、main processを経由しないといけない、と書いている人が居たので、
ちょっと書いたのであった。

説明

ウィンドウ間でメッセージをやりとりするには、
main processをいったん経由してから、
他のrenderer processにメッセージを送るのが作法のような気がするが


どうにかして、BrowserWindowのインスタンスを取得すれば、
各ウィンドウ間で直接データをやりとりできる。

具体的な実装の例

// https://github.com/taku-o/hello-electron-003-to-sub-msg/blob/master/electron.ts
mainWindow = new BrowserWindow({
    width: 600,
    height: 600,
    acceptFirstMouse: true,
    show: true
});
mainWindow.loadURL(`file://${__dirname}/main.html`);
// !!!!
global.mainWindow = mainWindow;
  • メッセージを投げる側の例
// https://github.com/taku-o/hello-electron-003-to-sub-msg/blob/master/js/main.ts
$scope.sendMsgTo1stWindow = function() {
    // remoteは参照そのものではなく、呼んだ瞬間のコピーのような
    const firstWindow = require(‘electron').remote.getGlobal('firstWindow');                  
    firstWindow.webContents.send('message', 'message from main.');
};
  • メッセージを受ける側の例
// https://github.com/taku-o/hello-electron-003-to-sub-msg/blob/master/js/second.ts
var ipcRenderer = require('electron').ipcRenderer;

// recieve message
$scope.message = '';
ipcRenderer.on('message', (event, message: string) => {
    $scope.message = message;
    $timeout(() => { $scope.$apply(); });
});

こんなに短いサンプルコードなのに、angularやtypescript使っていてわかりにくいとか、
global使うのは反則ではないのか?
とかあるかもしれないけど、あまり細かいことは気にしないで欲しい。

"できる"と"やる"は別の話

でも、結局は、main processで、BrowserWindowのインスタンスを管理することになると思います。
だいたい設計的な理由で。その方がいろいろ利点があるはず。

  • BrowserWindowのインスタンスを管理する場所を散らばらせるのは良くない
  • メニューとかにBrowserWindowのインスタンスを渡せると、処理上、都合の良い場合が多い。

サンプルコード

今回の実験のために作ったサンプルコードです。