Re:ゼロから始めるElectron開発生活

この記事は [ドワンゴ Advent Calendar](http://qiita.com/advent-calendar/2016/dwango) 10日目の記事です。

こんにちは。[ニコニコ静画](http://seiga.nicovideo.jp/)でフロントエンドで開発を行っている @nagisio です。
去年に引き続き、今年も~~冬コミに落ちて時間があるので~~アドベントカレンダーに参加させていただきます。去年の記事は「[新卒でもモダンなフロント開発がしたい！](http://qiita.com/nagisio/items/d8e3b2cbd404e4aafe03)」でした。
今年一年の所感ですが、[React](https://facebook.github.io/react/)の採用プロダクトが非常に増えてきている印象です。「[You Don't Need jQuery](http://qiita.com/tatesuke/items/b9548dd484b01b139b74)」といった記事や、「[Reactを使うとなぜjQueryが要らなくなるのか](http://qiita.com/naruto/items/fdb61bc743395f8d8faf)」といった記事が出てきたりと、Reactの勢いの強さが伺えます :muscle: 

本記事はますますモダン化が進むフロントエンド界隈において、今もっとも熱い（であろう）フレームワークReactとその他色々なあれこれを駆使して簡単なElectronアプリをゼロから開発（未完）しましたので、ご紹介します[^re0]。
[^re0]: タイトルが `Re:ゼロ` なのは過去にElectronアプリ開発を一度挫折したからです :innocent: 

# Yosoro

「**[Yosoro](https://github.com/nagisio/yosoro)**」は[SoundCloud](soundcloud.com)の曲を再生できるアプリです。
※ただし現状はまだ開発版（リリースしていない）であり、アプリとして最低限の機能がある状態です。また、動かすには[SoundCloud for Developers](https://developers.soundcloud.com/)よりApp登録を行うことで取得できるclientIdが別途必要で、バグもそれなりに含んでいますので、動作確認したい方はそのあたりをご了承ください。

## 開発経緯など

私は普段開発中は音楽を聴く派[^hifumin]なのですが、作業用BGMとしてSoundCloudを愛用しています。SoundCloudはプロ・アマ問わず大量の楽曲を無料で無制限で聴くことができる最高のサービスです。
しかしながら一方で、Webサービスという特性上、楽曲のコントロールをどうしてもブラウザ内で行う必要があるという問題を抱えています。開発を行っている途中で、ブラウザにわざわざ戻る（しかもタブを切り替えて！）のは面倒です。常に近くにおいて（可愛いジャケットを表示させて）開発に集中できるようにYosoroを作りました。
ちなみにYosoroは、静岡県沼津市の某高校で結成されたスクールアイドルの奮闘と成長を描く高尚なアニメにおいて、主人公 :tangerine: の幼馴染ポジションという最高の立ち位置を持つキャラ :anchor: にも関わらず、東京から来た突然の転校生 :cherry_blossom: に幼馴染を危うく奪われかけるという、所謂三角関係に悩まされながらも、最終的には幼馴染とのｷﾏｼﾀﾜｰ展開というお約束をきっちり果たしてくれるキャラが良く発する決め台詞です。観てね。[^sunshine]

[^hifumin]: 滝本ひふみ派
[^sunshine]: http://www.lovelive-anime.jp/uranohoshi/

## スクリーンショット

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/75883d93-8188-4a3f-6cc8-f28a126e459f.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/51df59e1-4a8d-d8ca-2319-c73a14efc1e5.png)

_※画像はハメコミ合成です。素材は[かわいいフリー素材集 いらすとや](http://www.irasutoya.com/)さんからお借りしました。いらすとやは神。_

# 本開発で導入および使用するもの

本開発では以下のツールやフレームワークを使って開発を進めていきます。

* Webpack(1.13.3)
    * ビルドツール
* Babel(6.18.2)
    * トランスパイラ
    * ES2015で書きます
* Electron(1.4.10)
    * クロスプラットフォームデスクトップアプリケーションエンジン
* **React**(15.4.1)
    * JSフレームワーク
* **Flow**(0.36.0)
    * 静的型付けの導入とチェック
* **CSSModules**(css-loader: 0.26.0)
    * React向けのCSSフレームワーク
* ESLint(3.11.1)
    * JS構文チェックツール

去年の記事ではWebpack+gulpを併用していましたが、主にJSのみの開発であれば、大抵Webpackのみで構築できます。[webpack-dev-server](https://github.com/webpack/webpack-dev-server)といった開発向けのサーバ環境も用意されていますし、特にreactですと[react-hot-loader](https://github.com/gaearon/react-hot-loader)を使用することでライブリロードも可能なので、こちらを使用すると便利です。
今回はテストを書けていないので :innocent: 使用していないですが、以下のようなテスト環境を整えることを是非おすすめします。

* Mocha
    * テストフレームワーク
* PowerAssert
    * assertionライブラリ
* Enzyme
    * React向けのテスティングツール

# モックを作成する

Electronアプリ開発作成にあたって、いきなりコードを書き始めても良いのですが、予めモックを作成しておくことをおすすめします。モックを作成しておくことで、後のマークアップ時に楽になりますし、UI/UXを考慮したデザインを組むことができます。また、モチベーション向上にも繋がります :smile: 
モック作成のアプリケーションとしては[Sketch](https://www.sketchapp.com/)が有名ですが、私は[AdobeExperienceDesignCC(AdobeXD)](http://www.adobe.com/jp/products/experience-design.html)を使用しています（現状ベータ版で、MacOSのみの公開）。
AdobeXDに関しては以下の記事が詳しいです。

* [Adobe Experience Design（XD）を使ってアプリのプロトタイプを作ってみた
](http://www.webcreatorbox.com/webinfo/adobe-xd/)
* [Adobe XDでLINEデザインを作ってみた【使い方解説】](http://stand-4u.com/tool/adobe_xd.html)

AdobeXDは直感的に使用でき、学習コストも低いのでおすすめです[^xd]。
[^xd]: 何より起動がめちゃくちゃ早い！

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/b3e30f20-9db3-1949-ebaa-5a9d85c08074.png)
_リスト画面のモックのために寿司 :sushi: を並べる図_

AdobeXDにはプロトタイプという機能があり、この機能を使うことで画面遷移を設定できます。

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/7decea83-53c6-a093-7830-463b7504390d.png)

また、プレビューモードにすることで、実際の画面遷移を確認できます。AdobeXDだけで画面設計と遷移のチェックができるのは良いですね。

![yosoro-preview.gif](https://qiita-image-store.s3.amazonaws.com/0/13956/c564775d-687a-4e2f-871e-affaf3fdbed1.gif)


# Electronでの開発環境を整える

それではElectronの開発をはじめていきます。
Electronはメインプロセスとレンダラープロセスに分かれており、今回のアプリケーションでは以下のようになります。

* メインプロセスでは `BrowserWindow` によってウェブページの作成とSoundCloudへのOAuth認証のプロセスを行います。
* レンダラープロセスではreactによってページの描画を行います。

Electronを立ち上げるまでは公式の[QuickStart](https://github.com/electron/electron/blob/master/docs/tutorial/quick-start.md)が詳しいです。
今回は迅速な開発を進めるためにwebpack-dev-serverとreact-hot-reloadを使用したいので、メインプロセスの `index.html` に設定を仕込みます。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Yosoro</title>
  <!-- ライブリロードが適用されるようにwebpack-dev-serverを読み込む -->
  <script src="http://localhost:8088/webpack-dev-server.js"></script>
  <!-- webpack-dev-serverによって生成されるjs/cssを読み込む -->
  <script src="http://localhost:8088/js/renderer.bundle.js"></script>
  <link href="http://localhost:8088/css/bundle.css" rel="stylesheet">
</head>
<body>
  <div id="view"><!-- reactで描画される部分 --></div>
  <script>
    // reactで描画するためにDOMContentLoadedを待つ必要がある
    document.addEventListener('DOMContentLoaded', function() {
      var Renderer = window.yosoro.Renderer;
      if (Renderer != null) new Renderer('view');
    });
  </script>
</body>
</html>
```

webpack-dev-serverが生成するJS/CSSを読み込むことで、ライブリロードが可能になります。
*※メインプロセスに関してはビルドし直してElectronを再起動させる必要があります。*

# メインプロセスの実装

## IPCによるプロセス間通信

メインプロセスは `BrowerWindow` の生成とSoundCloudへのOAuth認証を実装します。
OAuth認証に関しては「[ElectronでSoundCloud SDKを使う](http://qiita.com/shimpeiws/items/919bba12d3f45dacc30a)」を参考にさせていただきました :pray: 
詳しい実装は[GitHub](https://github.com/nagisio/yosoro)を見てもらうとして、ここではOAuth認証によって取得したトークンをレンダラープロセスへ受け渡す方法について解説します。
重要なのはトークンを受け渡すタイミングです。今回レンダラープロセスは `DOMContentLoaded` のイベントによりインスタンスを生成しますから、DOMのロードが完了した後でトークンを受け取る必要があります。
流れをまとめると、以下のようになります（実際には2と3は同時に行います）。

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/c72f58a3-21ef-3402-9086-fb23d0cdb1d8.png)

メインプロセス

```js:main/index.js
import { ipcMain } from 'electron';

// 1.レンダリング完了を待機
ipcMain.on('RENDERER_DID_FINISH_LOAD', (evt: Event) => {
  this.authManager.getToken()
    .then((token: string) => {
      // 4.トークンを飛ばす
      evt.sender.send('SC_CONFIG', {
        token,
        clientId: this.clientId
      });
    });
});
```

レンダラープロセス

```js:renderer/index.js
import { ipcRenderer } from 'electron';

// 3.トークン受け取りを待機
ipcRenderer.on('SC_CONFIG', (evt: Event, scConfig: Object) => {
  const { clientId, token }: { clientId: string, token: string } = scConfig;
  SoundCloudApi.initialize(clientId, token);

  ...
});
// 2.レンダリング完了イベントを飛ばす
ipcRenderer.send('RENDERER_DID_FINISH_LOAD');
```

## flowによる静的型付けの導入とチェック

上記コードにおいて、変数や引数に型が記述されていることにお気づきでしょうか。今回は[flow](https://flowtype.org/)を用いることで静的型付けを導入しています。[^ts]
flowを導入することで、 `null` や `undefined` によるエラーや、 存在しないメソッドの呼び出しによるエラー等を未然に防ぐことができます。詳しくは「[null安全でない言語は、もはやレガシー言語だ](http://qiita.com/koher/items/e4835bd429b88809ab33)」を一読されることをおすすめします。
例として、先ほどのレンダラープロセスのコードにおいて、受け取った `token` の型を `number` として定義してみます。

[^ts]: [TypeScript](https://www.typescriptlang.org/)の導入も考えましたが、TypeScriptを触ったことがなかったため、今回は見送りました。勉強したい…

```js:renderer/index.js
import { ipcRenderer } from 'electron';
import SoundClouadApi from 'soundCloudApi';

ipcRenderer.send('RENDERER_DID_FINISH_LOAD');
ipcRenderer.on('SC_CONFIG', (evt: Event, scConfig: Object) => {
  // tokenの型はnumberである
  const { clientId, token }: { clientId: string, token: number } = scConfig;
  SoundCloudApi.initialize(clientId, token);

  ...
});
```

`SoundCloudApi.initialize()` の実装は以下です。

```js:soundCloudApi.js
export default class SoundCloudApi {
  // tokenの型はstringを期待している
  static initialize(clientId: string, token: string): void {
    ...
  }
}
```

`flow` コマンドを実行すると、型の相違によるエラーが出ているのが確認できます。

```bash
> flow

renderer/index.js:69
 69:         SoundCloudApi.initialize(clientId, token);
                                                ^^^^^ number. This type is incompatible with the expected param type of
  5:   static initialize(clientId: string, token: string): void {
                                                  ^^^^^^ string. See: soundCloudApi.js:5
```

また、flowでは `nullable` を扱えます。つまり `null` かも （`undefined` かも）しれない型です。
`nullable` を表すには型の前に `?` を付けます。
`nullable` な型を定義すると、基本的にメソッド呼び出し時に `null` チェックが必須となります。
例えば、以下のコードはエラーとなりますが

```js
const token: ?string = getToken();
if (token.length > 0) { // tokenはnullableなのでErrorとなる
 ...
}
```

次のコードはエラーとなりません。

```js
const token: ?string = getToken();
if (token != null && token.length > 0) { // tokenはstringであることが保証されている
 ...
}
```

`nullable` を定義することで、実行時のエラーが事前に防げるのはとても良いですね！
null安全はいいぞ。[^null]

[^null]: [null安全はいいぞ。だって、型安全はいいぞ。](http://qiita.com/takasek/items/119218a2bae2f75062c0)

# レンダラープロセスの実装

## react-routerによるSPAの実現

今回のアプリは複数の画面が存在するので[react-router](https://github.com/ReactTraining/react-router/)を使用してSPAを実現しています。
ルーティングと画面遷移は以下になります。

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/967ea9ee-362b-284d-0c72-26bfb2d368c0.png)

ルーティング定義を行います。この際、各画面で共通の `state` を保持したい場合があります。
これは[redux](https://github.com/reactjs/redux)等を導入すれば `Store` として実現が可能ですが、reactのみで完結させたい場合は親のコンポーネントを定義しておく必要があります。
次のコードでは、 `App` コンポーネントが親として存在し、各画面のルーティングを子コンポーネントとして定義しています。

```js:routes.jsx
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import { App, Player, Splash, Stream } from 'renderer/components/index';

const routes = (
  <Route path="/" component={App}>
    <IndexRoute component={Splash} />
    <Route path="stream" component={Stream} />
    <Route path="player/:trackId" component={Player} />
  </Route>
);

export default routes;
```

親コンポーネントは共通の `state` を持ち、子コンポーネントに `props` として伝播させます。

```js:app.jsx
import React, { Component, PropTypes } from 'react';

// Stateの型定義
type State = {
 ...
}

export default class App extends Component {
  state: State; // this.stateの型はState

  constructor(props: Object) {
    super(props);
    this.state = {
      ...
    };
  }

  render() {
    // 子コンポーネント(children)にstateを渡す
    return cloneElement(this.children, this.state);
  }
}

App.propTypes = {
  children: PropTypes.node
};
```

ちなみに今回reactのみで開発を行いましたが、SPAの場合は `state/props` のやりとりが非常に多くなるため、redux等を使ったほうが幸せになれるかもしれません… :sob:

ルーティング定義ができたら、あとはひたすらコンポーネントを書くだけです！
コンポーネントをどこまで切り分けるかについては、AtomicDesignを意識しているものの、今回はそれほど細かくは分割していません。
AtomicDesignの考え方については、「[珍しいワークフロー：Atomic Designの原則とSketchでデザインからプログラミングまで](http://postd.cc/the-unicorn-workflow-design-to-code-with-atomic-design-principles-and-sketch/)」や 「[Atomic Designの考え方と利点・欠点](http://blog.kubosho.com/entry/using-atomic-design)」が参考になります。

## CSSModulesでCSSを書く

コンポーネントができたらCSSを書いていきます。バラバラになっているコンポーネントを綺麗に整理させていく過程は、私にとっては最も楽しい時間です :relaxed: 
reactコンポーネントにおいてCSSを書く場合、小規模なプロダクトであれば最近はCSSModulesをよく導入しています。CSSModulesを導入することで `BEM(MindBEMding)` 等のCSS設計を考える必要がなくなり、グローバル汚染も起きにくくなります。
解説は「[CSSモジュール ― 明るい未来へようこそ](http://postd.cc/css-modules/)」が詳しいのでそちらをご覧ください。

例えばタイムライン（下図）の実装は以下のようになります。

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/6bea8cdd-59e6-1aef-1a92-d937c390fc41.png)

```js:components/timeline.jsx
import React, { Component, PropTypes } from 'react';
import Slider from 'material-ui/Slider';
import Styles from 'components/timeline.css';

export default class Timeline extends Component {
  ...

  render() {
    return (
      <div className={Styles.wrapper}> // .wrapper
        <div className={Styles.duration}> // .duration
          {this.state.currentDuration}
        </div>
        <Slider
          className={Styles.progress} // .progress
          onChange={this.seekChange}
          onDragStart={this.seekStart}
          onDragStop={this.seekEnd}
          value={this.state.currentSliderValue}
        />
        <div className={Styles.duration}> // .duration
          {this.state.duration}
        </div>
      </div>
    );
  }
}
```

CSSは特に階層などを何も考えずに記述できます。
*Electronでは`display: flex;`を何も考えずに使えるのが最高ですね* :wink:

```css:components/timeline.css
/**
 * Timeline Component Styles
 * --------------------------------------------------
**/

.wrapper {
  width: 242px;
  padding: 0 5px;
  display: flex;
  align-items: center;
  justify-content: space-around;
  text-align: center;
}

.duration {
  font-size: 10px;
}

.progress {
  width: 130px;
}
```

ちなみに、今回コンポーネントの一部は[material-ui](https://github.com/callemall/material-ui)を使用しました。特にスライダーなどは自前実装はかなり大変なので、非常に便利です。[^m-ui]
こういったreactのコンポーネントセットは他にもたくさんあるので、（個人的には流行りすぎて）微妙となってしまったbootstrapデザインからの脱却も可能です。「[早く・それなりの UI を実現する React コンポーネントセット 16 選](http://qiita.com/kyrieleison/items/39ce30dd2d204791a9ea)」において多数まとめられています。
[^m-ui]: ただし、material-uiはCSSModulesではなくCSS-in-JSであるため、DOMの `style` 属性に直接CSSが書かれます。

# 完成 :tada: 

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/51df59e1-4a8d-d8ca-2319-c73a14efc1e5.png)

寿司が食べたい。

# おわりに

本記事ではフロントエンド界隈における最先端のフレームワークやライブラリを使用して、SPAなElectronアプリを開発しました。
本アプリは~~やっぱり時間がなかったので~~まだまだ機能が少ない状態で、完成とは言えないので、今後も開発を続けていきます。以下のような機能を増やしていく予定です。

* 検索機能
* プレイリスト機能
* シャッフルやリピート機能
* キーボードショートカットによる操作

完成の際にはまた何らかの形で紹介できればと思います。

全体の感想としては、Electronは難しくないよ！でもSPAの構築は難しいよ！といった感じです。Electron自体はメインプロセスさえ作ってしまえばほぼ終わったようなものです。その後はほぼSPAにおけるコンポーネントの実装がメインになり、 `state/props` をひたすらセットしたりリレーしたりしていきます。そのため、やはりreact単体ではどんどん苦しくなっていくであろうと思われます。
ちなみに、reactとは別の勢いのあるフレームワークとして[Vue.js](https://jp.vuejs.org/)が挙げられますが、最近 `v2.0` 系がリリースされたりしているので、こちらも試してみたいです。
flowによる静的型付けの導入はESLintと並んで非常に頼りになりました。個人開発においては、レビュワーの存在がいませんので、バグやコーディングの間違いを指摘してくれる環境はとても大事です。

最近は[Next.js](http://qiita.com/nkzawa/items/1e0e93efd13fb982c8c0)や[Yarn](https://yarnpkg.com/)が出たりと、日進月歩で技術が生まれています。2017年のフロントエンド界隈はどのように進化していくのか、楽しみですね。

# 宣伝

昨日12/10に、弊社のサービスの１つである[ニコニコ静画](http://seiga.nicovideo.jp/)から、来る12/29-31に開催されるコミックマーケット91のサンプルを投稿できるサービス「**[ニコニコ漫画＠C91](http://seiga.nicovideo.jp/manga/expo/c91/)**」をリリースしました。

![image](https://qiita-image-store.s3.amazonaws.com/0/13956/5cebb345-b594-047b-db28-e1d6cef62122.png)

私はアプリ版の特設サイトの（一部）デザインおよび開発（React+Redux+CSSModules）を行っています。
私はコミケに落ちたので残念ながら投稿はできませんが[^rakusen]、冬コミには一般参加するのでサークルチェック用途として活用したいと思います。
サークル参加される方は、是非投稿してくださいね！

[^rakusen]: 自分が開発に携わったサービスを活用できないのはとても悔しい…