この記事は ドワンゴ Advent Calendar 10日目の記事です。

こんにちは。ニコニコ静画でフロントエンドで開発を行っている @nagisio です。
去年に引き続き、今年も冬コミに落ちて時間があるのでアドベントカレンダーに参加させていただきます。去年の記事は「新卒でもモダンなフロント開発がしたい!」でした。
今年一年の所感ですが、Reactの採用プロダクトが非常に増えてきている印象です。「You Don't Need jQuery」といった記事や、「Reactを使うとなぜjQueryが要らなくなるのか」といった記事が出てきたりと、Reactの勢いの強さが伺えます :muscle:

本記事はますますモダン化が進むフロントエンド界隈において、今もっとも熱い(であろう)フレームワークReactとその他色々なあれこれを駆使して簡単なElectronアプリをゼロから開発(未完)しましたので、ご紹介します1

Yosoro

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

開発経緯など

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

スクリーンショット

image

image

※画像はハメコミ合成です。素材はかわいいフリー素材集 いらすとやさんからお借りしました。いらすとやは神。

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

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

  • 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といった開発向けのサーバ環境も用意されていますし、特にreactですとreact-hot-loaderを使用することでライブリロードも可能なので、こちらを使用すると便利です。
今回はテストを書けていないので :innocent: 使用していないですが、以下のようなテスト環境を整えることを是非おすすめします。

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

モックを作成する

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

AdobeXDは直感的に使用でき、学習コストも低いのでおすすめです4
image
リスト画面のモックのために寿司 :sushi: を並べる図

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

image

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

yosoro-preview.gif

Electronでの開発環境を整える

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

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

Electronを立ち上げるまでは公式のQuickStartが詳しいです。
今回は迅速な開発を進めるためにwebpack-dev-serverとreact-hot-reloadを使用したいので、メインプロセスの index.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を使う」を参考にさせていただきました :pray:
詳しい実装はGitHubを見てもらうとして、ここではOAuth認証によって取得したトークンをレンダラープロセスへ受け渡す方法について解説します。
重要なのはトークンを受け渡すタイミングです。今回レンダラープロセスは DOMContentLoaded のイベントによりインスタンスを生成しますから、DOMのロードが完了した後でトークンを受け取る必要があります。
流れをまとめると、以下のようになります(実際には2と3は同時に行います)。

image

メインプロセス

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
      });
    });
});

レンダラープロセス

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を用いることで静的型付けを導入しています。5
flowを導入することで、 nullundefined によるエラーや、 存在しないメソッドの呼び出しによるエラー等を未然に防ぐことができます。詳しくは「null安全でない言語は、もはやレガシー言語だ」を一読されることをおすすめします。
例として、先ほどのレンダラープロセスのコードにおいて、受け取った token の型を number として定義してみます。

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() の実装は以下です。

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

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

> 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 チェックが必須となります。
例えば、以下のコードはエラーとなりますが

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

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

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

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

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

react-routerによるSPAの実現

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

image

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

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 として伝播させます。

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でデザインからプログラミングまで」や 「Atomic Designの考え方と利点・欠点」が参考になります。

CSSModulesでCSSを書く

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

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

image

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:

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を使用しました。特にスライダーなどは自前実装はかなり大変なので、非常に便利です。7
こういったreactのコンポーネントセットは他にもたくさんあるので、(個人的には流行りすぎて)微妙となってしまったbootstrapデザインからの脱却も可能です。「早く・それなりの UI を実現する React コンポーネントセット 16 選」において多数まとめられています。

完成 :tada:

image

寿司が食べたい。

おわりに

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

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

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

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

最近はNext.jsYarnが出たりと、日進月歩で技術が生まれています。2017年のフロントエンド界隈はどのように進化していくのか、楽しみですね。

宣伝

昨日12/10に、弊社のサービスの1つであるニコニコ静画から、来る12/29-31に開催されるコミックマーケット91のサンプルを投稿できるサービス「ニコニコ漫画@C91」をリリースしました。

image

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


  1. タイトルが Re:ゼロ なのは過去にElectronアプリ開発を一度挫折したからです :innocent:  

  2. 滝本ひふみ派 

  3. http://www.lovelive-anime.jp/uranohoshi/ 

  4. 何より起動がめちゃくちゃ早い! 

  5. TypeScriptの導入も考えましたが、TypeScriptを触ったことがなかったため、今回は見送りました。勉強したい… 

  6. null安全はいいぞ。だって、型安全はいいぞ。 

  7. ただし、material-uiはCSSModulesではなくCSS-in-JSであるため、DOMの style 属性に直接CSSが書かれます。 

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