Edited at
TokyoSWDay 22

【Electron + React + Flux】マークアップエディタを作ってこと細かく解説してみた

More than 1 year has passed since last update.

この記事はtokyoSWアドベントカレンダーの22日目の記事です。

おはこんばんちわ。

株式会社Gizumoという会社で新人教育しながらフロントエンドエンジニアをやっているゆーひです。

ちょっと前からElectronに対して興味があったんで、業務の合間にこそこそ勉強してました。

実際にできあがったものを見ていただきながら完成に至るまでの流れを書いていきますー。

※Reactの知見がない人が見ることを前提として書いているので割と細かく書いています。ご了承を。


できあがったもの

1.gif

GitHubのurlも置いておきます。

https://github.com/yuyake0084/code_hack

まぁ、あれですね。

JS○iddleとか某Code◯en的なかんじのサービスをデスクトップアプリとして作ってみたみたいなかんじです。

gifだとちょっと見づらいですが、右側のエディタの値の変更を観測して左側のプレビュー部分にその値を流しこんで即時反映されているみたいなことをやってくれています。

この設計方法をObserverパターンというそうなんですが、そのデザインパターンを用いて実装するにあたって非常に相性がいいとされているツールがあるそうなので勉強も兼ねて使ってみることにしました。

そのツールというのがReactとFluxです!


開発環境

こんなかんじですね。

各導入方法などは記事が豊富にあったり公式ドキュメントに記載されていたりするので、詳細は割愛して開発の流れを解説していきたいと思います!


背景


  • ちょっとしたコーディングをしたい時にファイル作って保存するのは…

  • もはや保存時のcmd + Sさえ手間


ちょっとしたコーディングをしたい時にファイル作って保存するのは…

デザイナーさんと、「こういうボタンとかどうよ??」「いや、ここはもうちょっと丸みつけつつ影とか入れたくない?」みたいなちょっとしたデザインの擦り合わせをする機会ってそこそこあると思うのですよ。

その度にHTMLファイル作って、保存して、ブラウザで確認して、擦り合わせ終わったらファイル消して…

ってのも中々に手間だと思うのですよ。

いや、そんぐらいいいっしょみたいな意見もあるかもしれませんが、省ける工程があるなら省きたいじゃないですか!


もはや保存時のcmd + Sさえ手間

見出しの通りです。省きます。


Electronってなんぞ?

Electronとは、簡潔に説明するとWEBの技術を使ってデスクトップアプリを驚くほど簡単に作成することができるフレームワークです。

どれくらい簡単かというとHTMLとJSを1ファイルずつ用意すればそれだけでデスクトップアプリができあがります。

マジです。


app.js

const electron = require('electron');

const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
let mainWindow = null;

app.on('ready', () => {
mainWindow = new BrowserWindow({width: 1200, height: 900});
mainWindow.loadURL(`file://${__dirname}/index.html`);
mainWindow.on('closed', () => mainWindow = null);
});


たったこれだけの記述でindex.htmlがデスクトップアプリとして表示されます。

あとはindex.htmlの中に<h1>Hello World!</h1>とでも書いておけばHello World!と表示されるかと思います。

簡単ですね!

QuramyさんのElectronでアプリケーションを作ってみようという記事が非常に分かりやすかったので参考にさせていただきました。


React

デスクトップアプリを作成すること自体は簡単なのは分かりました。

しかし、中身をどう構築するかとかどういう機能を持たせようかというところで非常に悩みましたね…

とりあえず既に世の中に出ているマークダウンエディタのアプリケーションのように、入力した値がそのままプレビュー画面にリアルタイムで反映させたいなぁという思いが自分にはありました。(保存の手間を省くという要件ですね)

それを実現するにあたって選定したライブラリがReact.jsです。

流行っていますし、触ってみたいという思いもあったので勉強も兼ねて使ってみたというかんじですね。

では、このアプリケーションを元に流れを説明していきます。

※ES2015を使用しています。設定に関する詳細はgulpfile.babel.jsのbrowserifyタスクをご確認ください。


app.js


app/javascripts/app.js

import React from 'react';

import { render } from 'react-dom';

import App from './views/App.jsx';

render(
<App />,
document.getElementById('react')
);


npmでインストールしたreactモジュールと、react-domモジュールのrenderメソッドを読み込んでいますね。

renderメソッドは第一引数にimportしてきているjsxファイルを読み込ませて、第二引数で取得してきたidをターゲットにしてそこに対して第一引数の内容をはきだしています。

※補足:取得してきたidというのはElectronで表示させるHTMLに記載されているidを指しています。


app/index.html

<body>

<div id="react">
</body>


App.jsx

app.jsで読み込んできたApp.jsxでは何をしているのかというのを見ていきましょう。

※少し見づらいですけど、役割ごとに分割して説明していきます。


app/javascripts/views/App.jsx

import React, { Component } from 'react';

import { EventEmitter } from 'events';

// ...以下続く



React.Component

reactモジュールからComponentメソッドをimportしてきています。

Componentメソッドの中には後述するpropsstateという変数や、setStateという機能が入っています。


EventEmitter

EventEmitterとは、onメソッドとemitメソッドを使ってイベントの受け取りと転送ができます。


app/javascripts/views/App.jsx


// ...以下続き

// View
import Editor from './Editor.jsx';
import Header from './Header.jsx';
import Preview from './Preview.jsx';

// Action
import ActionCreator from '../actions/ActionCreator';

// Store
import EditorStore from '../stores/EditorStore';

const dispatcher = new EventEmitter();
const store = new EditorStore(dispatcher);
const action = new ActionCreator(dispatcher);

// ...以下続く



View

エディタ、ヘッダー、プレビューのそれぞれのコンポーネントを読み込んでいます。

その中にはView(見た目の部分となるHTML)が入っています。


Action

Viewの変更をEventEmitterで受け取って、データ(値)をStoreに流し込む機構。


Store

データの管理は全てここでします。

データの初期値をViewに流したり、Actionから更新データを受け取ってそれをそのままViewに流したりする機構です。

このViewActionStoreというのは後述するFluxの概念の元に構成されている機構になりますので、一旦ここでは説明を省きます。


Class定義


app/javascripts/views/App.jsx


// ...以下続き

class App extends Component {
constructor() {
super();
}

render() {
return (
<div>
<Header name="Code Hack" />

<div className="c-main">
<Preview
store={ store }
action={ action }
initialHtmlValue={ store.getValue() }
/>

<Editor
store={ store }
action={ action }
initialVal={ store.getValue() }
/>
</div>
</div>
);
}
};

export default App;


みんな大好きClass定義の時間です。

constructorメソッドはさておき、renderメソッドの中身を見てみると何やらHTMLっぽいものが書かれています。よく見るとimportしてきたものの変数と全く同じ名前ですよね?

既にお察しかと思いますが、importしてきたコンポーネントをタグとして埋め込むことによってその内容がタグの部分に展開されます。

そして最終的には冒頭でgetElementById('react')をしてきた箇所に流れこんでいくのです!すごい!

このオリジナルタグは子コンポーネント(呼び元は親コンポーネント)とか呼ばれていたりするんですけれど、子コンポーネントにはいくつか属性が書かれています。

これも通常の属性ではなくて、親コンポーネントでimportしてきたメソッドだったり値だったりを子コンポーネントに渡すこともできるのです!

例えばEditorタグでは、

<Editor

store={ store }
action={ action }
initialVal={ store.getValue() } />

store属性とaction属性にはそれぞれの機能が入っていき、initialVal属性にはstoreの中にあるgetValueメソッドを実行して返ってきた値を渡しています。

ここでEditor.jsxを見てみると中ではApp.jsxと似たような事(オリジナルタグによる値の受け渡し)をしているのが分かるかと思います。

なぜこんなに細かい単位でコンポーネントを分けているのか。

それぞれの機能はそれぞれのコンポーネントで定義する事によってスパゲッティコードにならないようにするという考え方が存在するのです。

これを、コンポーネント指向と呼んでいたりします。

「助長だ」とか「ファイル数が多くなりがちだから見るのが面倒」とか色々な声を聞いたりもしますが、個人的にはソースコードを追いやすいと感じるので好きです。

では、改めてEditor.jsxの中で呼ばれているHtmlEditor.jsxを見てみましょう。


app/javascripts/views/HtmlEditor.jsx

import React, { Component } from 'react';

import AceEditor from 'react-ace';
import 'brace/mode/html';
import 'brace/theme/monokai';

class HtmlEditor extends Component {
constructor(...args) {
super(...args);

this.state = {
value: this.props.initialVal
}

this._changeText = this._changeText.bind(this);
}

_changeText(value) {
this.setState({ value });
this.props.action.htmlEdit(this.state.value);
}

render() {
return (
<div className="c-editor__detail__container">
<h2 className="c-editor__head">{ this.props.name }</h2>

<AceEditor
mode="html"
theme="monokai"
name="editor"
width="100%"
height="100%"
tabSize={2}
ref="htmlArea"
showPrintMargin={false}
highlightActiveLine={false}
editorProps={{ $blockScrolling: true }}
onChange={this._changeText}
value={this.state.value}
/>
</div>
);
}
};

export default HtmlEditor;



constructor()

引数の...argsの中にはpropsという変数があり、その中には親コンポーネントで定義した属性がオブジェクトで入っています。更にthis.stateではhtmlというkeyに対して、valueにはpropsで受け取った値を渡しています。(ここで初期値を渡している)

propsやstateに関してはkunikenさんの今からはじめるReact.js〜propsとstate、それからrefs〜という記事が非常に分かりやすかったので、ぜひ見てみてください!


_changeText()

AceEditorの値が変更された時に実行される関数。

この_はプライベート関数であることを明示的に表しています。

引数にAceEditorの編集後の値が入ってそれをsetStateというReact.Componentのメソッドを利用してthis.state.valueに対してアップデートをかけています。


render()

名前の通りレンダリングするHTMLの定義ですね。

AceEditorというのはエディタの機能を簡単に実装することができるライブラリです。

JSX上で素のHTMLがそのまま書けるわけでは無いのは前述した通りですが、ひとつ付け加えるとclass属性が使えません。何故ならclassという単語はJavaScriptにとって、予約語だからです。

代わりにclassName属性を使いましょう。


Flux

FluxとはMVCと同じで概念です。言語やフレームワークではありません。

複雑になりがちなデータフローを単一方向に流すことでソースコードの可読性、メンテナンス性の向上を図るのがこの概念の特徴になります。

…と、実装を終えてようやく理解しましたが、中々腹にストンと落ちてこなくて悶々してました

自分と同じような思いをしている人に向けて、Fluxについては細かく説明していきたいと思います。(間違ってたら教えてください)

※今回はFluxの最小構成で作成しています。

本来ActionとStoreの間にはDispatcherという機構がありますが、そこはEventEmitterで賄っています。


1. 初期値の取得

1.png

まず最初にアプリケーションを起動した時点でActionがStoreに対してViewに初期値を展開するよう通知を送ります。

通知を受け取ったStoreは自身で管理しているデータをViewに流しこんで、初期値を受け取ったViewはそのデータを元にアプリケーションの見た目となる部分をレンダリングします。


2. Viewで更新された値をActionで受け取る

2.png

今回作成したアプリケーションのエディタの値というのは、同コンポーネント内で常に観測されています。一文字でも変更があればonChange属性で定義されているメソッドが実行されてActionに対してViewの最新の値を送ります。


app/javascripts/views/HtmlEditor.jsx

// ...

_changeText(value) {
this.setState({ value });
this.props.action.htmlEdit(this.state.value); // ここ
}

// ...



3. Actionで受けとった値をStoreに渡して更新する

3.png

Actionの中にあるhtmlEditの引数のdataの中には最新の値が入っていてEventEmitterのemitメソッドを実行します。第一引数の中にはイベント名を、第二引数にはdataを入れてあげてStoreに対して管理しているデータに対して更新をかけるようEventEmitterを使って通知しています。


app/javascripts/actions/ActionCreator.js

// ...

htmlEdit(data) {
this.data = data;
this.dispatcher.emit('htmlEdit', this.data);
}

// ...



4. 更新されたStoreの値をViewに渡す

4.png

Actionからの通知を受けたら今度はStoreからViewに対して値を乗せて通知します。ここでいうViewとはプレビューのコンポーネントですね。


app/javascripts/stores/EditorStore.js

_onHtmlEdit(value) {

this.value = value;
this.emit('HTMLCHANGE', this.value);
}

そして値を受け取ったView側では自身で保持しているstateに更新をかける為setStateメソッドを実行して、その更新された値をプレビュー側でレンダリングしているのです。

この円環の理に導かれてデータが単一方向に流れていく概念こそが、Fluxなのです。

5.png


まとめ

今回のアプリケーションを作成するにあたってすごく多くの事を学ぶことができました。

自分はフロントエンドエンジニアなんで、正直サーバー周りの事を全くと言っていいほど理解していないので、このアプリケーションをうまいこと使ってサーバー周りの勉強もしていきつつ機能もたくさん増やしていきたいなと思いました。

結構自分はまだReactやFluxに関してはふんわりとしか理解できていないので、うまく説明できていなかったところとか、ここ間違ってるみたいな箇所があれば遠慮なくご指摘ください。

最後まで読んでいただきありがとうございましたっ!