[ Electron + React + Redux ] クラウド管理式マークダウンエディターを作ってみた

クラウドで管理出来るマークダウンエディターって中々いいものがないですよね。
Webで作ってもいいんですが、Electronというものを使えばJavaScriptでデスクトップアプリが作れるようなので使ってみようと思います。

※ React+ReduxとElectronメニューの連携についてだけ知りたいという方は、最初の方は読み飛ばしてください

## Electronとは

* テキストエディターである「Atom」を開発するために生まれたデスクトップアプリケーションエンジン
* HTML+CSS+JavaScriptでクロスプラットフォームなデスクトップアプリケーションを開発出来る
* 著名なアプリケーションでも採用されている
    * Atom
    * Slack
    * VisualStudio Code 


## 作ったアプリ

* github [https://github.com/DaikiInaba/electron_react_rails_sample](https://github.com/DaikiInaba/electron_react_rails_sample)
(ElectronアプリとRailsアプリが入ってます。)

* 動作例
![gif](https://i.gyazo.com/38ce8442150e09089064b56a5d3bc415.gif)
![gif](https://i.gyazo.com/d55837a521c5fb36a5c1bd0f01853d66.gif)

## 機能

マークダウン編集・保存・削除


## Electronの基本

まずはElectronをインストールしましょう。

```bash
yarn global add electron-prebuild
```


Electronはpackage.jsonの`main`で指定しているJSファイルをエントリポイントとして使用します。
ここでは、`index.js`とします。

```package.json
...
"main": "index.js",
...
```

index.jsにはこのように記述します。

```index.js
const { app, BrowserWindow } = require('electron');

let mainWindow;

function createWindow (){
  mainWindow = new BrowserWindow({width: 800, height: 600});
  mainWindow.loadURL(`file://${__dirname}/frontend/index.html`);
  mainWindow.on('closed', function(){
    mainWindow = null;
  });
}

app.on('ready', function(){
  createWindow();
});

app.on('window-all-closed', function(){
  if(process.platform !== 'darwin'){
    app.quit();
  }
});

app.on('activate', function(){
  if(mainWindow === null){
    createWindow();
  }
});

```

7行目に`mainWindow.loadURL(.../index.html)`という記述がありますね。
Electronはこの`loadURL`に指定されているHTMLファイルをデスクトップアプリケーションとして表示してくれます。
なので`index.html`に

```index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Markdown Editor</title>
  <link rel="stylesheet" href="./index.css">
</head>
<body>
  <div id="root"></div>
  <script src="./bundle.js"></script>
</body>
</html>
```
と記述し、(Reactなどを使用している場合はコンパイル後の)JSファイルを読み込ませれば簡単にデスクトップアプリケーションを作成することが出来ます。

しかし、このままではただHTMLを表示するだけなのでWebアプリケーションと大差ないですよね。
Electronはデスクトップアプリケーション特有の**メニュー**や**ステータスバー**にアクセスするAPIを提供してくれています。

今回作ったアプリケーションでも**メニュー**を使用しているので、React+Reduxとメニューを組み合わせる方法を紹介しようと思います。


## React+Redux+Electron

React+Reduxを使ってElectronを実装している場合、メニューからActionを発行したくなることがあると思います。(自分はありました。)

メニューを実装するだけであれば`index.js`に

```javascript:index.js
...
function createMenu (){
  const template = [
    {
      label: 'Electron',
      submenu: [
        {
          label: 'Quit',
          accelerator: 'CommandOrControl+Q',
          click: function() { app.quit(); }
        }
      ],
    },
  ];

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}

app.on('ready', function(){
  createWindow();
  createMenu();
});
```

以下のような記述を追記するだけで簡単に実装出来ます。
labelとsubmenuの対応は以下の通りです。

![png](https://i.gyazo.com/e96903bef9b561a19c1bfeb5e0927b5a.png)

Actionを発行するメニューを実装する場合は

```index.js
...
submenu: [
  {
    label: 'Action',
    accelerator: 'CommandOrControl+Shift+A',
    click: function(){
        mainWindow.webContents.send('dispatch', { type: 'action_type' });
    }
  }
]
```

React側は

```./lib/ipcRenderer.js
const ipcRenderer = window.require('electron').ipcRenderer;

export default ipcRenderer;
```

```./index.js
import ipcRenderer from './lib/ipcRenderer' 
...
const store = configureStore();

ipcRenderer.on('dispatch', (e, action) => {
  store.dispatch(action);
});

render(
  ...
);
```
と記述することで実装することができます。
Electronはメインプロセスとレンダラプロセスとプロセスが分かれているため、プロセス間通信を行う場合はipcというものを使う必要があります。

しかし、引数が動的に設定したい場合はこの実装だと実現不可能ですね。
今回作成したアプリケーションでも、`Command+S`でエディターの値をデータベースに保存する機能を実装しているんですが、上記の方法だと引数にエディターの値を設定できなかったので実現不可能でした。

その解決方法も紹介します。
結論から言うと、設定したキーバインドが押下されると、値を保存する関数が発火する方法を取りました。

コードは以下の様になっています。

```index.js
...
label: 'Edit',
submenu: [
  {
    label: 'Save',
    accelerator: 'CommandOrControl+S',
    click() {
      mainWindow.webContents.send('save');
    }
  },
]
...
```

```js:/component/Editor.jsx
import ipcRenderer from '../lib/ipcRenderer';
...
export default class Editor extends Component {
  constructor (props){
    ...
    ipcRenderer.on('save', () => {
      this.onSave();
    });
    ...
  }

  onSave(){
    const { content } = this.props;
    this.props.onSave(content);
  }
}
```

こうすることで`コマンド+S`が押下された際にComponentのonSave関数が動く実装が可能です。


## まとめ

Electronは今回始めて触ってみましたが、非常に使いやすい印象を受けました。React+Reduxと組み合わせて使用していて、尚且つメニューを使用している参考がなかったので少し難しかったですが、同じようなことをしている方がいれば参考になると嬉しいです。

ReactNativeを使えばReactでスマホアプリも作れるので、Reactが書ければデスクトップ・スマホ・Web全て作ることが可能ですね。素晴らしい限りです。