ぼくのかんがえたさいきょうのElectron 

# はじめに

[Electron](http://electron.atom.io/)はNode.js + HTML5フロントエンドのいいとこ取りな開発が行えるのが特徴です。その分、開発環境もElectronならではの考慮・工夫が必要になります.
今回のエントリでは, Electronアプリを快適に開発するための開発環境Tipsを書いていきます. 

そもそも「Elecronってなんぞや？」とか「どういう風にアプリを作るの？」という方については, 手前味噌で恐縮ではありますが, [Electronでアプリケーションを作ってみよう](http://qiita.com/Quramy/items/a4be32769366cfe55778) を目を通すことをオススメします.

また, 今回のエントリの元として、[Quramy/electron-jsx-babel-boilerplate](https://github.com/Quramy/electron-jsx-babel-boilerplate) のレポジトリが出来上がっています.
React + Bable + Sass + Livereload + Platform用Packaging 入りのBoilerplateですので, これ以降の記事を読むのが面倒な人は使えばいいと思う.

![Electron_App.png](https://qiita-image-store.s3.amazonaws.com/0/8808/b3cfbbb9-763d-30a3-797b-e7dbc7c6e8c4.png)

# 画面系のフレームワーク
僕は日頃の仕事であるWebフロントエンド開発では、AngularJS + TypeScript(or CoffeeScript)で開発をしていますが、今回は敢えてReactを選択しています。

AngularJSはViewと関係の無い機能も含まれた(Webブラウザを動作環境とするという意味においての)Full Stackなフレームワークです。
例えば、通信系の機能について、Ajaxをラップした`$http` や Promise実装である `$q` 等が最初から含まれています。
一方、Electronの開発では、Node.jsで出来ることは素直にNode.jsの機能を使った方が早いことも多く、ライブラリも豊富です。HTTP通信をしたければ、素直に`require('http').request`を呼び出せば良いのです。
そんなことを考えた結果、シンプルなViewフレームワークであるReactを選択しました. 

また, Reactのv0.13.xによりES6のClass記法との相性が格段に上がったので、BabelでES6, JSXをトランスパイルする構成としています.

<!-- {{{ 画面周りのコード -->

```js:src/app.js
'use strict';
import polyfill from 'babel/polyfill';
import app from 'app';
import BrowserWindow from 'browser-window';
import crashReporter from 'crash-reporter';

let mainWindow = null;
if(process.env.NODE_ENV === 'develop'){
  crashReporter.start();
}

app.on('window-all-closed', () => {
  app.quit();
});

app.on('ready', () => {
  mainWindow = new BrowserWindow({width: 580, height: 365});
  mainWindow.loadUrl('file://' + __dirname + '/renderer/index.html');
});
```

```html:src/renderer/index.html
<html>
<head>
<!-- 中略 -->
</head>
<body>
<div id="app"></div>
<script src="bootstrap.js"></script>
</body>
</html>
```

```js:src/renderer/components/main.jsx
'use strict';

import React from 'react';

export class Main extends React.Component {
  state = {
    message: 'Hello, Electron'
  }
  constructor () {
    super();
  }
  render() {
    return (
      <div className="container">
        <div className="jumbotron main">
          <h1>{this.state.message}</h1>
        </div>
      </div>
    );
  }
}
```

```js:src/renderer/bootstrap.js
'use strict';

import polyfill from 'babel/polyfill';
import React from 'react';
import {Main} from './components/main';

React.render(React.createElement(Main), document.getElementById('app'));
```

<!-- }}} 画面周りのコード -->

gulpでトランスパイル部分のタスクを書くと、下記のようになります。

```js:gulpfile.js
var gulp = require('gulp'), $ = require('gulp-load-plugins');

gulp.task('compile:scripts', function () {
  return gulp.src('src/**/*.{js,jsx}')
  .pipe($.sourcemaps.init())
  .pipe($.babel({
    stage: 0
  }))
  .pipe($.sourcemaps.write('.'))
  .pipe(gulp.dest('.serve'))
  ;
});
```

JSX -> JavaScript変換もBabelで出来てしまうので、かなりシンプルです. 
また、通常のブラウザ向けアプリの開発であれば、bootstrap.jsをエントリとしてBrowserify(+ Reactify or Babelify)でbundle.jsを作るのが王道ですが, 何せElectronはRenderer Process用のコードから直接`require`できるので、Browserify等を使わなくても実行可能です.

その他ポイントを挙げるとすれば, `babel`のオプションに`--stage 0`相当を渡している所でしょうか(Electronとは関係ないですが).
このオプションが無い場合, JSXのコード中の`state: {...}`の初期化部分でCompile Errorとなります.
Class宣言でのメンバ変数初期化はES6仕様には含まれず, ES7の仕様として提案されているため, BabelのExperimental機能を使う形になります.

ちなみに, `babel/register`をindex.htmlから読み込んでしまえば, 実行時にtranspileすることも可能ですが, どうせ配布用のアプリケーションを作る際は事前にコンパイルするので, 最初からタスクを用意しています.

# Livereload
Electronでの開発でもLivereloadしたいですよね.

ここで, Livereloadの要件は以下の2パターンがあります.

1. RendererProcessで読み込んでいるコードが変更されたら, 画面をreload.
1. BrowserProcess(MainProcess, app.jsなど)で動作するコードが変更されたら, RendererProcessもろともBrowserProcessを再起動

1.は通常のWeb開発におけるLivereloadのイメージとほぼ同様です. 2.はlivereloadというよりはnodemonのイメージに近いかもしれません.

1.については, [Stackoverflow](http://stackoverflow.com/questions/30445766/live-reload-for-electron-application)などでも取り沙汰されているように, gulp-livereloadあたりで実現できるのですが, 2.との統合も視野に入れた結果として, [electron-connect](https://github.com/Quramy/electron-connect) というnpmモジュールを作成して, 1.と2.を簡単にタスクに組み込めるようにしました.

gulpfile.js側でファイル変更を通知するサーバを立て, ElectronのProcess側からサーバへ接続してファイル変更イベント時に内部的に`BrowserWindow.reloadIgnoringCache`でリロードをかける仕組みです.
2.の機能としては, BrowserProcessを復帰させた際に, 再起動前の表示位置へPositionを復帰させるといった細かい機能も仕込んでいます.
利用する際のコードは以下のようになります.

<!-- livereload task {{{ -->

```js:gulpfile.js
'use strict';
var gulp, electron = require('electron-connect').server.create();

gulp.task('serve', function () {

  // Electronの起動
  electron.start();

  // BrowserProcess(MainProcess)が読み込むリソースが変更されたら, Electron自体を再起動
  gulp.watch(['.serve/app.js', '.serve/browser/**/*.js'], electron.restart);

  // RendererProcessが読み込むリソースが変更されたら, RendererProcessにreloadさせる
  gulp.watch(['.serve/styles/**/*.css', '.serve/renderer/**/*.{html,js}'], electron.reload);
});
```

```html:src/renderer/index.html
<html>
<head>
<!-- 中略 -->
</head>
<body>
<div id="app"></div>

<!-- gulp側で立てたserverへ接続する -->
<script>require('electron-connect').client.create()</script>

<script src="bootstrap.js"></script>
</body>
</html>
```

<!-- livereload task }}} -->

# パッケージング
配布用のアプリのパッケージングには, [electron-packager](https://www.npmjs.com/package/electron-packager) を用います.

このモジュールを使うと, [本家ドキュメントのdistributionガイド](https://github.com/atom/electron/blob/master/docs/tutorial/application-distribution.md)に記載されているパッケージングの作業を自動化することができます.

```js:gulpfile.js
'use strict';

var gulp = require('gulp'), packager = require('electron-packager');

gulp.task('package:darwin', ['build'], function (done) {
  packager({
    dir: 'dist',              // アプリケーションのパッケージとなるディレクトリ
    out: 'release/darwin',    // .app や .exeの出力先ディレクトリ
    name: 'ElectronApp',      // アプリケーション名
    arch: 'x64',              // CPU種別. x64 or ia32
    platform: 'darwin',       // OS種別. darwin or win32 or linux
    version: '0.28.1'         // Electronのversion
  }, function (err, path) {
    // 追加でパッケージに手を加えたければ, path配下を適宜いじる
    done();
  });
});
```

上記以外にも, 追加できるoptionはありますが, electron-packagerのREADMEにある通り, `platform: win32`に対して`icon`のオプションを利用する場合は, `rcedit.exe` が動作する環境でタスクを実行する必要があります.
[wine](https://www.winehq.org/)や[AppVeyor](http://www.appveyor.com/)の利用を検討しましょう.

## Browserify
このエントリを最初に投稿した当初は, browserifyどうでもよくね？と思っていましたが, @mizchi 氏からnode_modules配下のminify効果を具体的な数値としてコメントで教えてもらったため、minify欲求が再び鎌首をもたげてきました(本節は投稿後に書き直しています).

最初にBrowserifyで不要コードの削除に取り組んだ際は、BrowserProcess向けとRendererProcess向けの.jsコードに対して、Browserifyでそれぞれのbundle.jsを作ってみたのですが, これは単純には動きませんでした. 

BrowserProcess側のコードをBrowserifyしてしまうと, RendererProcess側で`require('remote').require('...')` が記載してある場合に動作しなくなるのです.
Electron本体のコード[atom/browser/lib/rpc-server.coffee](https://github.com/atom/electron/blob/4b12a95d370c023e8fbb54b9144b45744ccafdfe/atom/browser/lib/rpc-server.coffee#L91)に記載されている下記が曲者です.

```js
process.mainModule.require(module)
```

BrowserProcessが利用しているモジュールがbundleにまとめられると, `remote.require`実行時にはmoduleが存在しないのが原因です.

現状では, node_modulse内の各モジュール毎にBrowserifyでbundleを作成するようにしています([Appendix](#appendix)の`bundle:dependencies`タスク).
こうすることにより, BrowserProcessとnode_modulesの関係が保たれるため, `remote.require('...')`も正常に動作します. 

### 2016.08.16 追記
bundleの作成については、最近はwebpackに落ち着きつつあります。

```js:webpack.config.js
"use strict";

var webpack = require("webpack");
var path = require("path");

module.exports = {
  target: "electron",
  node: {
    __dirname: false,
    __filename: false
  },
  entry: {
    "main/index": "./src/main/index.js",
    "renderer/app": "./src/renderer/app.jsx"
  },
  resolve: {
    extensions: ["", ".js", ".jsx"]
  },
  module: {
    loaders: [
      { exclude: /node_modules/, test: /\.jsx?$/, loader: "babel" }
    ]
  },
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name].js"
  }
};
```

# こぼれ話
ここからは, 比較的どうでも良さげな内容をオマケ的に書いておきます.

## 

[前述の通り](#livereload), RendererProcessのhtmlから`electron-connect`を利用しています. `electron-connect`は開発補助専用のモジュールであり, 実行時は不要です. わざわざPackagingしたアプリケーションにコピーしたくありません.
下記のように, `<!-- build:remove -->` と[gulp-useref](https://www.npmjs.com/package/gulp-useref) を利用し, Packagingするhtmlからは `<script>` が消去されるようにタスクを組んでいます.

```html:src/renderer/index.html
<!-- build:remove-->
<script>require('electron-connect').client.create()</script>
<!-- endbuild -->
```

## Async/Await

今回作成した[boilerplate](https://github.com/Quramy/electron-jsx-babel-boilerplate) にはソースコードとして含めていませんが, Babelのoptionに`stage: 0` を追加した副次的(?)な効果として, ES7のAsync/Awaitも利用できるようになります.
[とあるアプリケーション](http://qiita.com/Quramy/items/df6415f832a4339716f0) を作成していたときに非同期のコードがAsync/Awaitで綺麗にかけることを実感しました.
Async/Awaitの詳細については, [ES7 の Async/Await を使ってみた](http://qiita.com/mohayonao/items/435665065d997a4cc50c)等を参照してください. 

# Appendix

## 完成形のgulpfile

<!-- gulp all {{{ -->

```js:gulpfile.js
'use strict';

var gulp = require('gulp');
var $ = require('gulp-load-plugins')();
var _ = require('lodash');
var fs = require('fs');
var path = require('path');
var del = require('del');
var mainBowerFiles = require('main-bower-files');
var electronServer = require('electron-connect').server;
var packager = require('electron-packager');
var merge = require('merge2');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var packageJson = require('./package.json');

var srcDir      = 'src';      // source directory
var serveDir    = '.serve';   // directory for serve task
var distDir     = 'dist';     // directory for serve:dist task
var releaseDir  = 'release';  // directory for application packages

// Compile *.scss files with sourcemaps
gulp.task('compile:styles', function () {
  return gulp.src([srcDir + '/styles/**/*.scss'])
    .pipe($.sourcemaps.init())
    .pipe($.sass())
    .pipe($.sourcemaps.write('.'))
    .pipe(gulp.dest(serveDir + '/styles'))
    ;
});

// Inject *.css(compiled and depedent) files into *.html
gulp.task('inject:css', ['compile:styles'], function() {
  return gulp.src(srcDir + '/**/*.html')
    .pipe($.inject(gulp.src(mainBowerFiles().concat([serveDir + '/styles/**/*.css'])), {
      relative: true,
      ignorePath: ['../../.serve', '..'],
      addPrefix: '..'
    }))
    .pipe(gulp.dest(serveDir))
  ;
});

// Copy assets
gulp.task('misc', function () {
  return gulp.src(srcDir + '/assets/**/*')
    .pipe(gulp.dest(serveDir + '/assets'))
    .pipe(gulp.dest(distDir + '/assets'))
  ;
});

// Incremental compile ES6, JSX files with sourcemaps
gulp.task('compile:scripts:watch', function (done) {
  gulp.src('src/**/*.{js,jsx}')
    .pipe($.watch('src/**/*.{js,jsx}', {verbose: true}))
    .pipe($.plumber())
    .pipe($.sourcemaps.init())
    .pipe($.babel({stage: 0}))
    .pipe($.sourcemaps.write('.'))
    .pipe(gulp.dest(serveDir))
  ;
  done();
});

// Compile scripts for distribution
gulp.task('compile:scripts', function () {
  return gulp.src('src/**/*.{js,jsx}')
    .pipe($.babel({stage: 0}))
    .pipe($.uglify())
    .pipe(gulp.dest(distDir))
  ;
});

// Make HTML and concats CSS files.
gulp.task('html', ['inject:css'], function () {
  var assets = $.useref.assets({searchPath: ['bower_components', serveDir + '/styles']});
  return gulp.src(serveDir + '/renderer/**/*.html')
    .pipe(assets)
    .pipe($.if('*.css', $.minifyCss()))
    .pipe(assets.restore())
    .pipe($.useref())
    .pipe(gulp.dest(distDir + '/renderer'))
  ;
});

// Copy fonts file. You don't need to copy *.ttf nor *.svg nor *.otf.
gulp.task('copy:fonts', function () {
  return gulp.src('bower_components/**/fonts/*.woff')
    .pipe($.flatten())
    .pipe(gulp.dest(distDir + '/fonts'))
  ;
});

// Minify dependent modules.
gulp.task('bundle:dependencies', function () {
  var streams = [], dependencies = [];
  var defaultModules = ['assert', 'buffer', 'console', 'constants', 'crypto', 'domain', 'events', 'http', 'https', 'os', 'path', 'punycode', 'querystring', 'stream', 'string_decoder', 'timers', 'tty', 'url', 'util', 'vm', 'zlib'],
      electronModules = ['app', 'auto-updater', 'browser-window', 'content-tracing', 'dialog', 'global-shortcut', 'ipc', 'menu', 'menu-item', 'power-monitor', 'protocol', 'tray', 'remote', 'web-frame', 'clipboard', 'crash-reporter', 'native-image', 'screen', 'shell'];

  // Because Electron's node integration, bundle files don't need to include browser-specific shim.
  var excludeModules = defaultModules.concat(electronModules);

  for(var name in packageJson.dependencies) {
    dependencies.push(name);
  }

  // create a list of dependencies' main files
  var modules = dependencies.map(function (dep) {
    var packageJson = require(dep + '/package.json');
    var main;
    if(!packageJson.main) {
      main = ['index.js'];
    }else if(Array.isArray(packageJson.main)){
      main = packageJson.main;
    }else{
      main = [packageJson.main];
    }
    return {name: dep, main: main.map(function (it) {return path.basename(it);})};
  });

  // add babel/polyfill module
  modules.push({name: 'babel', main: ['polyfill.js']});

  // create bundle file and minify for each main files
  modules.forEach(function (it) {
    it.main.forEach(function (entry) {
      var b = browserify('node_modules/' + it.name + '/' + entry, {
        detectGlobal: false,
        standalone: entry
      });
      excludeModules.forEach(function (moduleName) {b.exclude(moduleName)});
      streams.push(b.bundle()
        .pipe(source(entry))
        .pipe(buffer())
        .pipe($.uglify())
        .pipe(gulp.dest(distDir + '/node_modules/' + it.name))
      );
    });
    streams.push(
      // copy modules' package.json
      gulp.src('node_modules/' + it.name + '/package.json')
        .pipe(gulp.dest(distDir + '/node_modules/' + it.name))
    );
  });

  return merge(streams);
});

// Write a package.json for distribution
gulp.task('packageJson', ['bundle:dependencies'], function (done) {
  var json = _.cloneDeep(packageJson);
  json.main = 'app.js';
  fs.writeFile(distDir + '/package.json', JSON.stringify(json), function (err) {
    done();
  });
});

// Package for each platforms
gulp.task('package', ['win32', 'darwin', 'linux'].map(function (platform) {
  var taskName = 'package:' + platform;
  gulp.task(taskName, ['build'], function (done) {
    packager({
      dir: distDir,
      name: 'ElectronApp',
      arch: 'x64',
      platform: platform,
      out: releaseDir + '/' + platform,
      version: '0.28.1'
    }, function (err) {
      done();
    });
  });
  return taskName;
}));

// Delete generated directories.
gulp.task('clean', function (done) {
  del([serveDir, distDir, releaseDir], function () {
    done();
  });
});

gulp.task('serve', ['inject:css', 'compile:scripts:watch', 'compile:styles', 'misc'], function () {
  var electron = electronServer.create();
  electron.start();
  gulp.watch(['bower.json', srcDir + '/renderer/index.html'], ['inject:css']);
  gulp.watch([serveDir + '/app.js', serveDir + '/browser/**/*.js'], electron.restart);
  gulp.watch([serveDir + '/styles/**/*.css', serveDir + '/renderer/**/*.html', serveDir + '/renderer/**/*.js'], electron.reload);
});

gulp.task('build', ['html', 'compile:scripts', 'packageJson', 'copy:fonts', 'misc']);

gulp.task('serve:dist', ['build'], function () {
  electronServer.create({path: distDir}).start();
});

gulp.task('default', ['build']);
```

<!-- gulp all }}} -->