ElectronでDiffツールを作ってみた

## この記事は
[アイスタイルアドベントカレンダー2016](http://qiita.com/advent-calendar/2016/istyle)の11日目の記事です。
10月入社の[@romiogaku](http://qiita.com/romiogaku)が書きます。（@fagaiさんと順番かわりました）

git不要のテキストdiffツールが欲しかったので、勉強がてらElectron+react+reduxで作ってみました。

## できたもの

[Difftron](https://github.com/romiogaku/difftron)

![difftron](https://github.com/romiogaku/difftron/blob/master/docs/difftron.gif?raw=true)


名前はDifftronにしました。
意外と◯◯tronという名前空間空いてるので早い者勝ちですよ！
gifアニメにあるようにQiitaのmarkdownはdiffフォーマットも対応してるんです。知ってました？

## 利用ライブラリ

### [diff](https://github.com/kpdecker/jsdiff)
diffのjavascript実装です。

### [diff2html](https://github.com/rtfpessoa/diff2html)
diffからgithubのような差分表示HTMLを出力してくれます。

### [react-ace](https://github.com/securingsincity/react-ace)
コードエディタ[Ace](https://ace.c9.io/)のreact版です。

### [redux-saga](https://github.com/yelouafi/redux-saga)
副作用を扱うためのライブラリです。
後述しますが、結果として非同期処理の必要は無くなったのでビジネスロジックの配置場所としての意味合いが強くなりました。

### [redux-persist](https://github.com/rt2zz/redux-persist)
storeを永続化してくれます。
`autoRehydrate` enhancerを追加し`persistStore`にstoreを渡すだけでstoreの保存とアプリ起動時の読み込みを勝手にやってくれます。

```js
import {persistStore, autoRehydrate} from 'redux-persist';
const store = createStore(reducer, undefined, autoRehydrate());
persistStore(store);
```

### [seamless-immutable](https://github.com/rtfeldman/seamless-immutable)
reduxの三原則のうちの一つに「State is read-only」（Stateは読み取り専用）というのがあります。
Stateを変更させる唯一の方法はActionを発行することです。
この原則を守るために、State自体をImmutableにするアプローチがあります。

JavaScriptには[Object.freeze()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)もありますが入れ子のオブジェクトまでfreezeできないといった問題があります。
Immutable Objectを扱うためのライブラリには以下の様なものがあります。
- [Immutable.js](https://facebook.github.io/immutable-js/)
- [seamless-immutable](https://github.com/rtfeldman/seamless-immutable)
- [timm](http://guigrpa.github.io/timm/)

知名度が高いのはImmutable.jsでしょうか。
Immutable.jsは専用のオブジェクトに変換されるので、用途によって
【Immutable.jsオブジェクト】↔【vanilla JS】
の相互変換処理をかます必要があります。
そこでvanilla JSのままImmutableなオブジェクトを扱えるようにするseamless-immutableも人気です。

### [reduxsauce](https://github.com/skellock/reduxsauce)
ReducerやActionの記述をちょっと楽にできます。

### [ava](https://github.com/avajs/ava)
会社でmocha使っているのでavaを試してみました。
デフォルトでpower-assertが使われ、余計な設定無しでES2015記述で書けるのが良いですね。

### [reactotron](https://github.com/reactotron/reactotron)
アプリケーションのインスペクタです。デバッグ用に使います。
reactotronを起動した状態でアプリケーションを立ち上げると、stateに変化があったりイベントが起きるたびにログがゲロゲロ吐き出され内部のstateについて確認できます。

## 内部的な話
### ディレクトリ構成（レンダラプロセス）

![36F527512BE1DBEF61FB5A812B37D3D7.jpg](https://qiita-image-store.s3.amazonaws.com/0/6287/7205fa0d-3517-cfbf-9949-97deb266ac66.jpeg)

#### Action
ActionのタイプとActionCreatorを置きます。
`reduxsauce`を使っています。

#### Components

![FD0D75EA4DE277768FB43435CA040579.jpg](https://qiita-image-store.s3.amazonaws.com/0/6287/e431274a-c48b-19ed-2153-07c9d80639cf.jpeg)

左入力エリア、右入力エリア、下部diff出力エリアコンポーネントとcssを配置します。
コンポーネントは全て`Stateless Function`で記述しています。所謂`Presentational Component`です。
イベントハンドラは後述する`Conteiners`で定義します。

#### Config
主に定数等を定義しています。

#### Containers

![602CF5556E2CD8C6C0DC277D98F46CED.jpg](https://qiita-image-store.s3.amazonaws.com/0/6287/10a18ccf-6c00-a61d-4062-dc1efa298336.jpeg)

各コンポーネントの親にあたります。所謂`Container Component`です。
今回は単一ページのアプリケーションなのでContainerは`PresentationScreen`1つだけです。
stateとdispatcherは`react-redux`の`connect()`を使いprops経由で各コンポーネントに渡しています。

```js
import { connect } from 'react-redux';
import Actions from '../Actions/Creators';

const mapStateToProps = state => (
  {
    left: state.input.left,
    right: state.input.right,
    ......(略)
  }
);

const mapDispatchToProps = dispatch => (
  {
    changeLeft: input => dispatch(Actions.inputLeftChange(input)),
    changeRight: input => dispatch(Actions.inputRightChange(input)),
    ......(略)
  }
);

export default connect(mapStateToProps, mapDispatchToProps)(PresentationScreen);
```

#### Reducers

![292B2AB1389D18AEDA6820DCCA5ED3DD.jpg](https://qiita-image-store.s3.amazonaws.com/0/6287/e7ec01e2-1b22-f0b7-90c1-0e418a73103f.jpeg)

Reducerを置きます。stateは`seamless-immutable`を使っているので、新しいstateへの更新は[`merge`](https://github.com/rtfeldman/seamless-immutable#merge)するだけで済みます。

```js
import immutable from 'seamless-immutable';
const changeLeft = (state, action) =>
  state.merge({
    left: action.input,
  });
```

#### Sagas

![D59AD9648616DF22AB5E501F1E49C054.jpg](https://qiita-image-store.s3.amazonaws.com/0/6287/7be48b13-4483-59a1-d385-e4dbdd218177.jpeg)

ビジネスロジックを配置します。`redux-saga`を使っています。
元々、テキストエリアに何か入力があるたびにdiff計算とhtml変換をするため、変換処理に時間がかかることを想定していたのですが、思いの外サクサク動いたので非同期処理はありません。
アプリケーション全体のアーキテクチャとしては、テキストエリアや設定値の変更を全て一旦Sagaでwatchし、各workerで処理するという流れになります。

```js
export function* leftWatcher() {
  // 左入力エリアの変更を受け、workerへ処理を任せる
  yield* takeEvery(Types.INPUT_LEFT_CHANGE, worker);
}

export function* worker() {
  // 現在のstateを取得
  const leftInput = yield select(left);
  const rightInput = yield select(right);
  const outputFormat = yield select(format);
  const outputSplit = yield select(split);
  // diffの計算(Serviceにまとめています)
  const [rawDiff, contents] = createDiff(leftInput, rightInput, outputFormat, outputSplit);
  // Actionのdispatch
  yield put(Actions.outputDiffResult(rawDiff, contents));
}
```

#### Selectors
`redux-saga`の`select`で利用する[`selector`](https://github.com/yelouafi/redux-saga/tree/master/docs/api#selectselector-args)を置いています。

#### Services
実際のDiff処理を行うメソッド等を配置しています。
helper的な役割に近いかもしれません。

#### Stores
Storeの生成、各Middlewareの設定、永続化設定等を行います。

#### *.global.css
アプリケーション全体のレイアウトファイルです。

#### index.js
レンダラプロセスのエントリーファイルです。
ipcイベントリスナもこちらで定義しています。

### State構造

```js
{
  input: {
    left: '左入力テキスト',
    right: '右入力テキスト',
    language: 'javascript', // 言語設定
    theme: 'github' // テーマ設定
  },
  output: {
    raw: '',  // unified形式のdiff
    contents: '', // 出力エリアに表示するコンテンツ
    format: 0, // 出力フォーマット設定(html or unified)
    split: 'line-by-line' // 出力方式設定
  }
}
```

このへん再考の余地がありそうですが、inputとoutputを`App/Reducers/index.js`で`combineReducers`しています。

### データフロー

![485C66327C287F90646D34D89FB04D13.jpg](https://qiita-image-store.s3.amazonaws.com/0/6287/185dfeca-f428-4372-2f6f-8763ad9773e3.jpeg)

入力が変化したらsagaがそれをwatchしdiffを計算し、outputのstateを更新します。
それ以外は一般的なreduxのデータフローです。

### IPC（プロセス間通信）
ElectronにはメインプロセスとBrowserWindow上で走るレンダラプロセスがあります。
Difftronではメニューバーの「View」から、表示テーマやフォーマットを変更できます。
ユーザが設定を変更したらそれをレンダラプロセス側で受け取る必要があります。

メニューバーの設定は以下のようにメインプロセスで記述します。

`main.development.js`一部抜粋

```js
{
  label: 'DiffFormat',
  submenu: [
    {
      label: 'unified',
      click() {
        mainWindow.webContents.send('set-diff-format', DIFF_FORMAT.unified);
      },
    },
    {
      label: 'html',
      click() {
        mainWindow.webContents.send('set-diff-format', DIFF_FORMAT.html);
      },
    },
  ],
},
```

レンダラプロセスでは以下のように受け取ります。

`App/index.js`一部抜粋

```js
import { ipcRenderer } from 'electron';
ipcRenderer.on('set-diff-format', (event, arg) => {
  dispatch(Actions.outputFormatChange(arg));
});
```

~~IPCを使いたいが為に設定項目をメニューバーに押し込んだので使いづらい...~~
今思えば素直にレンダラプロセスで設定画面を作っても良かったかもしれません。

### yarnの威力
npmからyarnにするとnpm install: 5分半 だったのが yarn install: 1分半 になりました。

## 作ってみて
意外とちゃんとしたものが出来て満足です。
普段の仕事と違ってブラウザ互換性を考えないで良いjavascript開発は最高ですね！
昔からあるようなフリーソフトでも自分好みにモダンな感じでElectronで作り直すと案外覇権取れるかもしれませんよ。

## 今後
- flow導入
- E2Eテスト足す
- フォントサイズ等設定項目を増やす
- ウィンドウサイズの永続化
- アイコンをちゃんとしたやつにする
- ストアに出す？


## 明日は
・・・誰か書きます！
お楽しみに！！！