electronアプリをbrowserifyしてAndroid移植したmemo

現実世界のelectronでnodejsな趣味コードをbrowserifyした際の知見。
====

世の中のまともなプロjsエンジニアは最初からボイラープレートでgulpなビルドパイプラインを構築してまっさらなプロジェクトにjsxコードをおもむろに書き始めるとかしているのだと思います。
しかし現実のワークフロー、というか開発フロー、もとい移植フローというのはそんなに綺麗なものではありません。
例えばelectronのためにnode.js的なmodule.exportsで書いていた移植性を考えていないjsコードをどうにかブラウザもといWebViewで動かしたい向きもあるわけです。
つまり私です。個人プロジェクトなんですよlina_dictoは。
(あ、移植性はちょっと考慮してた。そこは嘘になっていますが嘘でなくて機能追加の際に一度移植性について諦めたのです。)





成果物:
https://github.com/MichinariNukazawa/lina_dicto_for_android_2018


# 前置き
electronで書いたエスペラント辞書アプリケーション lina_dicto をAndroidのWebViewに移植したのが lina_dicto_for_android です。
以前は、
- electron側のコードを出来るだけ、require()などのnode固有機能を使わない、ピュアなブラウザjsで書く
- ファイルIOなどどうしてもブラウザで出来ない部分はMakefileによるビルドプロセスでファイル上書き・置換等を行う
など、ある程度移植を意識したelectronコードを書くことで対処してきました。
しかしunittestにワンクッション入れたり、機能追加が面倒だったりしたため、lina_dicto(electronデスクトップ版)はこの前の機能追加の際にこの方針を諦めました。
そのためAndroid版の更新追従は諦めなければならなかったのですが、そうは言っても新機能は魅力的で、Android版での提供を諦めきれるものではありませんでした。
というわけで、electron向けコードをbrowserifyでブラウザjsに変換する方向で対処してみた次第です。

## browserifyがlocalなnodejsモジュールを読み込まない

lina_dictoのディレクトリ構成は下記の通り。require()さえ解決すればブラウザ実行できてしまう構成になっています。
```
lina_dicto/
    index.js        # electronのエントリポイント
    index.html      # メインページ(ピュアHTML)
    js/
        index.js    # 辞書アプリケーションのエントリポイント(HTMLページに読み込まれる)
        *.js
        ...         # その他jsファイル(scriptタグで読まれるブラウザjsやrequire()で読み込むmodule.exportsモジュールが混在)
```

lina_dictoは js/index.js が主要機能のエントリポイントで、以下の通りrequire()で`module.exports`を使ったローカルなjsファイルのモジュールを読み込んでいます。
```js
  1 'use strict';¬
  2 ¬
  3 var extension = new Extension();¬
  4 var platform = new Platform();¬
  5 // const Language = new Language();¬
  6 const Language = require('./js/language');¬
  7 const Esperanto = require('./js/esperanto');¬
  8 const Dictionary = require('./js/dictionary');¬
  9 let dictionary = new Dictionary();¬
 10 const Linad = require('./js/linad');¬
 11 let history = new History();¬
 12 ¬
 13 var timeline_item_id = 0;¬
 14 let dictionary_handle = null;¬
 15 ¬
 16 window.onload = function(e){¬
 17 ¬

```

しかしこれをこのようにbrowserify にかけると、
`node ./node_modules/browserify/bin/cmd.js js/index.js -o bundle.js`
下記エラーが出ます。

```js
lina_dicto/$ node ./node_modules/browserify/bin/cmd.js js/index.js -o bundle.js
Error: Cannot find module './js/dictionary' from '/home/nuka/lina_dicto_for_android/app/src/main/assets/lina_dicto/js'
    at /home/nuka/lina_dicto_for_android/app/src/main/assets/lina_dicto/node_modules/browser-resolve/node_modules/resolve/lib/async.js:55:21
    at load (/home/nuka/lina_dicto_for_android/app/src/main/assets/lina_dicto/node_modules/browser-resolve/node_modules/resolve/lib/async.js:69:43)
    at onex (/home/nuka/lina_dicto_for_android/app/src/main/assets/lina_dicto/node_modules/browser-resolve/node_modules/resolve/lib/async.js:92:31)
    at /home/nuka/lina_dicto_for_android/app/src/main/assets/lina_dicto/node_modules/browser-resolve/node_modules/resolve/lib/async.js:22:47
    at FSReqWrap.oncomplete (fs.js:152:21)
```
悩ましいのが`Cannot find module `のmoduleがビルド毎にランダムに変わることです。わけがわからない。多分中で並列ビルドなりなんなりしているのだと思いますが。
ともあれ解決は以下のとおりです。

```
mv js/index.js .
node ./node_modules/browserify/bin/cmd.js index.js -o bundle.js
```
つまりは相対パスの解釈の問題らしいです。
これlina_dictoでは偶然これで解決できるパス構成だったからよかったものの、もっと複雑なパスを組んでるアプリケーションだったらパスの書き換えまで必要だったのでは?

## scriptタグで読んだブラウザjsコードから、browserifyしたfunctionを呼ぶことができない
早速ブラウザで実行すると、下記エラーで止まりました。
```js
platform.js:9 Uncaught ReferenceError: query_input_element is not defined
    at Platform.init (platform.js:9)
    at window.onload (bundle.js:26)
```

該当箇所のコード
```js
  3 class Platform{¬
  4 >-------init()¬
  5 >-------{¬
  6 >------->-------// 検索ボタンを有効化¬
  7 >------->-------let button = document.getElementById("query-area__query-input__button");¬
  8 ¬
  9 >------->-------button.addEventListener("click", query_input_element, false);¬
 10 ¬
 11 >------->-------return true;¬
 12 >-------}¬
```

nodeモジュール化して一緒にバンドルすれば解決するかとおもったのだが、ならなかった。
```js
bundle.js:2625 Uncaught ReferenceError: query_input_element is not defined
    at Function.init (bundle.js:2625)
    at window.onload (bundle.js:26)
```

仕方ないのでlina_dicto本体コードに直接追加した。
```js
   53 >------->-------{¬
   54 >------->------->-------// browserifyで別ファイルから関数を指定できない問題を、¬
   55 >------->------->-------// 解決するのが面倒だったため、ここへ処理を追加¬
   56 >------->------->-------if('android' === Platform.get_platform_name()){¬
   57 >------->------->------->-------// 検索ボタンを有効化¬
   58 >------->------->------->-------let button = document.getElementById("query-area__query-input__button");¬
   59 >------->------->------->-------button.addEventListener("click", query_input_element, false);¬
   60 >------->------->-------}¬
   61 >------->-------}¬
   62 >-------});¬
   63 }¬
```
lina_dicto本体コードに不要コードが増えたことになるが、コードの美しさがlin_dictoのゴールではない。この程度なら許容範囲とした。

## browserifyしたコードから、バイナリファイルを読み込めない

```js
bundle.js:10347 Access to XMLHttpRequest at 'file:///node_modules/kuromoji/dict/base.dat.gz' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
```

https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control/Errors/CORSRequestNotHttp?utm_campaign=default&utm_medium=firefox-cors-errors&utm_source=devtools

エラーチェックを緩和することで関係部分以外の基本的な辞書動作はブラウザで確認できる。そこから先をchromiumでのこれ以上の動作確認は諦めて、Androidでやることに。

Android WebViewではCORSを緩和できる。
```
        webView1.getSettings().setAllowFileAccessFromFileURLs(true);
```
https://developer.android.com/reference/android/webkit/WebSettings
https://stackoverflow.com/questions/4666098/why-does-android-aapt-remove-gz-file-extension-of-assets

Androidのassetから拡張子 .gz ファイルは取り除かれる。
これは拡張子を変えることで回避できる。
```bash
 cd node_modules/kuromoji/dict/
 rename 's/\.gz/.bin/' *
```
どうやら.gz.binでは駄目で、.gzを除く必要がある。


## HTMLから、browserifyしたfunctionを呼ぶことができない

イベントコールバックの登録に、HTMLへ書き込む古い方法を使っていました。
```html
    <input id="query-area__query-input__input" type="text"¬
        value='wakeup...' disabled='true'¬
        onkeypress="on_keypress_by_query_input_element(event);" onkeyup="on_keyup_by    _query_input_element(event);" />¬
```

しかしこれは下記エラーを起こす。
```js
index.html:27 Uncaught ReferenceError: on_keypress_by_query_input_element is not defined
    at HTMLInputElement.onkeypress (index.html:27)
onkeypress @ index.html:27
index.html:27 Uncaught ReferenceError: on_keyup_by_query_input_element is not defined
    at HTMLInputElement.onkeyup (index.html:27)
```

index.js(bundle.js)でaddEventListener()するよう書き換えて処置した。
```js
 52 >------->-------{¬
 53 >------->------->-------let input = document.getElementById("query-area__query-input__input");¬
 54 >------->------->-------input.addEventListener("keypress", on_keypress_by_query_input_element, false);¬
 55 >------->------->-------input.addEventListener("keyup", on_keyup_by_query_input_element, false);¬
 56 >------->-------}¬
```

## その他
AndroidStudioのエミュレータで日本語入力
https://blog.webarata3.link/android-emulator-japanese

WebView側でスクロール管理して画面いっぱいに表示されたい場合に、Layoutを挟みサイズのオプションもしなければならなかったのが、WebViewを貼り付けてデフォルト設定で適用すればOKになっていた。
(古い設定は使えなくなっていた。)

タイトルバーの消し方が変わっていた。前の方法(androidmanifest.xmlのandroid:theme)は使えなくなっている模様。
http://ntoshi1900.hatenablog.jp/entry/2018/01/21/%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB%E3%83%90%E3%83%BC%E3%82%92%E6%B6%88%E3%81%99%EF%BC%88Android_Studio_3.0.1%EF%BC%89

###
ソフトウェアキーボード表示時にアクティビティ(WebView)がリサイズで追従しない問題。
タイトルバーを消すときに、不要な`<item name="android:windowFullscreen">true</item>`を指定していたのが原因。
```
        <item name="windowNoTitle">true</item>
        <item name="windowActionBar">false</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowContentOverlay">@null</item>
```

```
        <activity android:name=".MainActivity"
            android:windowSoftInputMode="adjustResize"
```
`android:windowSoftInputMode="adjustResize"`は不要だった。

https://github.com/delight-im/Android-AdvancedWebView/issues/113

spectron electron UIテストの実用例と書き方memo





spectron公式にはtitleをチェックするなどという気の抜けたリアリティのないユースケースしか書いてないし、リンクで示された先のWebDriverIOには2通りの書き方が書いてあってどちらを選べばよいかわからなかったりする。


リアルに動くelectronのUIテストコードはlina_dictoで使っている。githubに公開している。
https://github.com/MichinariNukazawa/lina_dicto
https://github.com/MichinariNukazawa/lina_dicto/blob/master/lina_dicto/test/spec.js



## mochaについて
https://mochajs.org/ に書いてある。
- done()を用いた非同期なテストと待ち受け
- timeout
あたりにお世話になっている。

retry 使いたいのだがサンプル通りに書いてみて使えているのかよくわからないので使ってない。

## Travis CIでtimeoutテスト
うちの現在のtimeout系テストについて、Travis CI環境で実行すると1.5掛けで遅い。
タイムアウト時間はCI環境の性能を考えて指定しないと、向こう側でエラーになる。

## テストの水増し
実行時間の平均化を目的に、テストを水増ししたい。
内側でループする方法と、テストデータを増やす方法がある。
うちでは現在、併用している。テストデータを増やす方法のほうが平均化の効果があったように思う。(平均とってないからわからないが)

```
 23 it ("Dictionary.get_item_from_keyword timeout", function(done) {¬
 24 >-------// ループによる水増しは、平均化にあまり効果があるように見えない。¬
 25 >-------// データ数を増やしたほうが良さそうに見える。¬
 26 >-------const EO_TO_JA_DICT_DATAS = [¬
 27 >------->-------['voc^o',>------>-------{'isMatch':true}],¬
 28 >------->-------['bonan',>------>-------{'isMatch':true}],¬
 29 >------->-------['bonan matenon',>------{'isMatch':true}],¬
 30 >------->-------['kafo',>------->-------{'isMatch':true}],¬
 31 >------->-------['kafolakto',>-->-------{'isMatch':true}],¬
 32 >------->-------['vitra',>------>-------{'isMatch':true}],¬
 33 >------->-------['zorgo',>------>-------{'isMatch':true}],¬
 34 >------->-------['boc^o',>------>-------{'isMatch':false}],¬
 35 >------->-------['zzz',>>------->-------{'isMatch':false}],¬
 36 >------->-------['xxxxxxxxxx',>->-------{'isMatch':false}],¬
 37 >-------];¬
 38 ¬
 39 >-------const datas = EO_TO_JA_DICT_DATAS;¬
 40 >-------for(let c = 0; c < 10; c++){ // 水増しと平均化¬
 41 >------->-------for(let i = 0; i < datas.length; i++){¬
 42 >------->------->-------const data = datas[i];¬
 43 >------->------->-------let res;¬
 44 >------->------->-------res = Dictionary.get_item_from_keyword(dictionary_handle, data[0]);¬
 45 >------->------->-------if(! data[1].isMatch){¬
 46 >------->------->------->-------assert(null === res);¬
 47 >------->------->-------}else{¬
 48 >------->------->------->-------assert(null !== res);¬
 49 >------->------->-------}¬
 50 >------->------->-------//assert(data[1] === res[2]);¬
 51 >------->-------}¬
 52 >-------}¬
 53 ¬
 54 >-------done();¬
 55 }).timeout(1500);¬
```

## mochaでtimeout時間の指定
公式の記法。
```
it('should take less than 500ms', function(done){
  this.timeout(500);
  setTimeout(done, 300);
});
```

うちで現在使っている記法。
```
it('should take less than 500ms', function(done){
  setTimeout(done, 300);
}).timeout(500);
```


## mochaで実行時間が表示されない

早すぎると実行時間が出力されない。
```
  ✓ Dictionary.init_dictionary timeout (201ms)
  ✓ Linad.initialize timeout (778ms)
  ✓ Dictionary.get_index_from_incremental_keyword timeout
  ✓ Linad.getResponsesFromKeystring timeout (416ms)

  4 passing (1s)
```
だいたい100msくらい。
末尾にsetTimeout()を入れて対処。
```
it ("speedy timeout test", function(done) {
    setTimeout(done, 100);
})
```

```
  ✓ Dictionary.init_dictionary timeout (214ms)
  ✓ Linad.initialize timeout (718ms)
  ✓ Dictionary.get_index_from_incremental_keyword timeout (111ms)
  ✓ Linad.getResponsesFromKeystring timeout (393ms)

  4 passing (2s)
```

技術書典5「クロス本」売り上げ報告ほか

この記事は 技術同人誌 その2 Advent Calendar 2018 Advent Calendar 2018 の17日の記事です。 #技術書典 daisy bell #技術書典5 | 一般 当落状況:当選(き08) 16:00 時点で、事実上の完売というこ...