comp_cover

lab

September 5, 2016    ]

だれでも簡単にリッチコンテンツを作れる WebエディターをReact.jsで作った話

こんにちは。ediplexで主にフロントエンドを担当してるホセ マリア カンパニャです。メキシコから来ました、趣味としてギターを引いたり、写真を撮ったり、プログラミングを勉強したりしてます。好きな日本語は「平穏」という言葉です。今回ediplexで開発してるリッチエディターの話をします。


今開発してるプロジェクトで、一般ユーザーがテキスト、ビデオ、地図などをブラウザ上で編集して記事を書くエディターが必要になりました。HTMLを出力するリッチエディターはネットに沢山ありますが、ほとんどはアウトプットのカスタマイズがしにくい。ユーザーがマークアップのことを意識しないで自由に様々なメディアを追加できるためには、どこにどのように出力するかをコントロールする必要があります。

その問題を解決するために、「エディタコンポーネント」というものを組み立てて記事を作るエディターを作りました。エディタコンポーネントはテキスト、ビデオ、画像などの一つの要素をあらわします。ユーザーはエディタコンポーネントを好きなように組み合わせ、記事を編集します。これにより、リテラシーのあまり高くないユーザーでも簡単な操作でリッチなコンテンツを作成できます。

エディタコンポーネントのデータはHTMLにしやすいようにJSON形式で保存されます。コンテンツを直接触らずに、出力するマークアップはいつでも調整できるようになり、デザインのカスタマイズや調整が楽になります。エディタコンポーネントで作られた記事のプレビューを見るために、中身が変わると自動的に更新していくプレビューコンポーネントを持つプレビューエリアも作りました。

エディタコンポーネント


現在、このエディターはテキスト、ビデオ、地図、フォトギャラリー、リンク集、ファイルアップロードのできる「エディタコンポーネント」を持っています。説明が長いのですべてを説明できませんが、代表的な三つのエディタコンポーネントを紹介します!

テキストコンポーネント


テキストコンポネント

「テキストコンポーネント」は、テキストブロックを作るときに使います。ユーザーはMarkdown形式でテキストを入力します。MarkdownはHTML形式などに変換しやすくするために開発されたシンタックスです。Markdownを使うことで、細かいレベルで出力のマークアップの調整ができます。上のテキストコンポーネントの画像では、左にユーザーが入力する実際のエディタコンポーネントがあって、右にプレビューがでています。

フォトコンポーネント


フォトコンポネント

イメージギャラリーを作るのに使う「フォトコンポーネント」です。ユーザーは写真をドロップするだけで、画像がアップされて、写真スライドショーが作れるようになります。もちろん、写真の順番もドラッグするだけで調整ができます。過去にアップロードした画像から選べる機能もついています。

マップコンポーネント


マップコンポネント

名前通りに、マップコンポーネントは地図を表示したいときに使います。左の編集側には入力項目と確認ボタンしかありません。入力項目にユーザーが位置情報や住所、場所の名前などを入力すると、検索して右側のプレビューエリアにマップとして表示されます。

Reactによる実装


このエディターを実装するのにFacebookが開発してる「React」というライブラリを選びました。Airbnb、Netflix、Facebook、SoundCloudのようなよく知られたサービスで使われています。ブラウザー上で直接DOMを操作することは、複雑な処理が必要なことから特に遅くなります。多くのJavaScriptライブラリでは、何か変更があったらブラウザーに直接出力して、全てをレンダーし直します。ReactはWebサイト(DOM)のバーチャルコピーで計算してから、変わった部分だけを出力し直します。それで沢山の計算と処理時間が省略されて、Webサイトはもっと速くなります。

リアクトで作られた部品もComponentと呼ばれます。React Componentは親子関係が持てます。このプロジェクトの最初から、Reactと同じように、エディターの部品をコンポーネントという名前を付けました。それで、React Componentとエディターコンポーネントの二つの概念ができました。エディターコンポーネントはReact Componentではありますが、React Componentはエディターコンポーネントではない場合もあります。少しわかりにくいとおもいますので、この記事ではエディターコンポーネントはカタナカで表します。

Fluxアーキテクチャ


ReactはMVCモデルでViewの部分にあたりますが、MVCアーキテクチャに合わせる必要はありません。Reactで人気のパターンはFluxとReduxです(他も沢山あります)。 このエディターを作るのにFacebookで開発されたシンプルなFluxアーキテクチャを選びました。

Fluxアーキテクチャは一方向のデータフローを採用していて、アクション、ディスパッチャー、ストア、ビューの4つの部分があります。ストアの中にあるステート(エディターの中身)は、リードオンリーなプロパティとしてReactのビューに渡されます。Reactでのデータ変更は次のように行います。ビューがアクションを発生して、データがディスパッチャーを経由してストアに届きます。ストアはデータを更新し、新しく更新されたデータがビューに戻ります。

article

ReactのComponentはステートとプロパティという2つの情報を持っています。ステートはComponentの現在状態を表し、Componentは自分のステートを変えることができます。プロパティは親のComponentから受け継ぐリードオンリーな情報です。基本的に、トップのComponentしかステートを持たないで、そのステートを子Componentにプロパティとして渡します。

では、テキストコンポーネントを例にして、Componentのステート(テキストComponentの文書)はリードオンリーとして渡されるなら、入力したときにどうやってテキストの変更ができるのでしょうか?

ComponentはどんなにComponentツリーが深くても、ストアにアクションを発行できます。この場合はユーザーがテキストを入力したとき、テキストコンポーネントは「コンポーネント編集アクション」をストアに向けて発行します。このアクションはコンポーネントIDと、入力されたテキストデータを持っています。ストアはこのアクションを受け取って、アクションのデータを使って自分のステートを更新してから、ビューに新しいデータを戻します。親Componentはそのデータを受け取ったらステートを更新し、テキストコンポーネントが表示できるようにプロパティを渡します。このフローは1文字でも実行されます。

この方法で、すべてのデータ処理がストア側でまとめられます。Componentはデータを表示するだけです。そうすると、ストアはデータがどうやって表示されるか知らなくてもよく、Componentはどうやって処理されるか知らなくてもよくなります。親のComponentが必要なプロパティを渡すことさえできれば、ReactのComponentはViewのどこに置いてもちゃんと動きます。また、新しい機能の実装を実装するのも、アクションとストアハンドラーの追加だけで良いため、簡単に素早く行うことができます。

JSXはテンプレートシステムとして


ReactのComponentはJSXというシンタックスで書けます。JSXはHTMLとJavaScriptを一緒にかけるようにするJSシンタックスの拡張です。純粋なテンプレートエンジンとして作られたものではありませんが、ここではテンプレートシステムとして活用しました。

このプロジェクトのワークフローでは、デザインチームとシステムチームの2つのチームがありました。デザインチームはデザインとテンプレートを担当し、システムチームではバックエンドとフロントエンドの一部を担当してます。そのため、デザインチームはSmartyやVoltのようなテンプレートシステムには慣れています。

Reactではロジックとマークアップを同じJSXファイルに入れるパターンをよく目にします。しかし、デザインチームがコンポーネントのの中身を知らなくても修正ができるようにするために、一つのReact Componentを二つのファイルに分けました: マークアップしか入っていないJSXファイル(他のテンプレートシステムと似ている)と、Componentのロジックを持つJSファイル。この仕組みのおかげでデザインチームだけでもReact Componentのマークアップをある程度調整できるようになりました。

Reactでトラブルになったこと


Reactでの実装は、現在のWebの主流なベストプラクティス(主にjQueryとか)と違う場合があります。普通のHTMLで簡単にできるものが、Reactでは複雑になることがあります。特に、ページの一部にだけReactを使い、同じページ内のReact以外の部分とやりとりする場合に問題が多く発生します。

初めの問題は、React Componentで入力した情報とReact以外のフォームを合わせてajaxで通信したいが、そのままでできないということでした。Reactのステートを input フィールドに json としてを出力することで React Component の全てのデータをカプセル化し、コールバックでReact以外からもステートを受け取れるようにして、この問題を解決しました。

もう一つの問題は、jQuery(とそのプラグイン)と合わせることです。jQueryとReactは一緒に使えることは使えますが、React Componentの面倒な調整になることがよくあります。実装方法にもよりますが、Reactはページの読み込みが終わって(それとajaxのリクエストが終わって)からレンダリングし始めます。そのためjQueryプラグインの実行タイミングではまだDOMがないことがあり、見つけにくいバグの温床になります。この問題はReactのレンダリングが終わってから、jQueryプラグインを実行するようにすれば解決ができます。

Reactはマークアップを変えるjQueryプラグインが好きではありません。ReactはマークアップとバーチャルDOMをidを使って管理してます。あるjQueryプラグイン(特にフォームのフォーマットを変えるようなもの)はDOMの一部を切り取って自分のマークアップにラップします。そのときにidの関連が壊れてしまいます。ReactがViewを更新しようとしたら、バーチャルDOMと本当のDOMが違って、更新対象が分からなくてエラーになります。全てのReactの実行が止まる場合もあります。

その解決方法として、Reactの更新の前にマークアップを元の状態にもどし、Reactの更新が終わるとまたプラグインを実行する、という方法があります。しかし、あまりきれいじゃありませんね。ですので、できるだけjQueryプラグインを使わずに、React用のプラグインを使った方が良いです。デバッグ時間が短くなりますし、多くの人気プラグインはReact版があります。

まとめ


1年ほど前にReactを使い始めたときは、まだドキュメントやチュートリアルなどの情報が見つけにくかったですが、Reactの開発速度は速く、今はもうそこまで見つけにくくありません。簡単に入門できるようなチュートリアルサイトや本はどんどんでてきてます。最初はステートやFluxのような概念はわかりにくいと思いますが、一度理解できれば、Reactでの開発はstructured、ナイス、きれい、そして楽しい!と気づくはずです。

ediplexでは、Reactのような新しい技術も積極的にプロダクションに採用しています。エンジニアの裁量で先端技術も採用できます。興味を持たれた方は是非一度遊びに来てください




リソース


Unidirectional data flow: https://www.youtube.com/watch?v=i__969noyAM
Rethinking Best Practices: https://www.youtube.com/watch?v=x7cQ3mrcKaY
Getting Started With React: http://shop.oreilly.com/product/9781783550579.do
Flux Architecture: https://egghead.io/courses/react-flux-architecture-es6
Egghead: https://egghead.io/technologies/react