Edited at

React と Flux で Tumblr を再構築する話。

More than 3 years have passed since last update.


はじめに

今回話すのは、React を触ってみて僕としては、中々いい感じだなと思ったところで。

今度はちょっとしたアプリケーションを組んでみようと思ったとき話です。

非常に長い。スライドにすればよかった。LTとかで


Tumblr

毎度お世話になっております。無料でブログができちゃう神サービス。


問題点

ただし神といっときながらも、幾つか残念な点も多数。


  • カスタマイズの管理がしにくい

  • テストが辛い(毎回アップロードも嫌だ)

  • 案件で使用するとなると、鍵かけなきゃいけないし。何かと死にたい。

言いたいことは、 Tumblr の仕様にのっとった更新の手間につきます。


今回で可能な解決

今回はすべて TUmblr API から値をとってくるので、いろいろな問題を解決できます。


  • カスタマイズページで更新せず localhost での確認が可能

  • テストも毎回テンプレート更新をしないので良いのでしやすい

  • 案件では、本家のテンプレートを空にすれば情報の漏洩はほぼほぼ心配せず開発できます。

最悪、tumblr 上にサイトを展開しなくてもよいです。

でもTumblrを利用しているからには、tumblr にテンプレートを展開しましょう。


React と Flux

React と Flux を用いてアプリケーションを構築します。

これを採用した点は、 はじめに書いた通り、 React を選択しながら アプリケーションを書くには Flux のフローを導入するのが今のところよいとされると思ったからです。


Flux

React は View を提供してくれますが、それだけだと データのやりとり等を含めた、アプリケーションは構築しにくいです。

といっても、今回私の作成した例では、React 単体でも構築可能です。


Flux における アプリケーションフロー

flux-diagram-white-background.png

Flux では有名な図。一方通行なデータフローが素敵ってよく言われる。

React が提供してくれるのは 図の React Views

Flux が提供してくれるのは 図の Dispacher

そして、他の部分は自分で作成していくことになります。


フォルダ構成

先に述べた通り、Flux はデータのやりとりを示してくれているだけで、構築方法は人それぞれ(と思っているよ)です。

今回 私は Flux 公式が提供している

Flux-chat を参考に構築していきます。

https://github.com/facebook/flux/tree/master/examples/flux-chat

これに則ると js は次のようなフォルダ構成になります。

 js

├── actions
├── app.js
├── components
├── constants
├── dispacher
├── stores
└── utils

非常に単純明解。図の大項目に合わせて対応した名前のフォルダを作成します。


各フォルダの役割


app.js

アプリケーションを呼び出します。

必要なら、ここで直接 Web API Utils を叩いて初期データを取得します。


Components

tumblr のテンプレートに値する部分を記述する部分です。

コンポーネントに何かしらインタラクションがあったとき、例えば


  • 次の20件を読み込む

  • 記事詳細を読む

とかあるときに Action Creators に命令をだします。

Action Creators


Action Creators

Web API Util に命令を出します。今回だと


  • 記事を offset 0 から 20 件とってこい (Web API Utils に)

  • 記事がとれたから Store に保存・追加してくれと指示をだす (Dispacher に)

ですね。


Web API Utils

Tumblr とやりとりします。

コールバックとして Action Creators に命令をだしてあげます。

この時点である程度データを整形してしまうこともあります。


Dispacher

Action Creattors から、なんかして呼ばれたときに、各所に(主に Store )に命令をだします。

Flux 提供の Dispacher の素晴らしいところは waitFor の実装ですね。

これは、他の store で起こった処理をの終了を待ってから自身の処理(いわゆる Promise?)をするといった目的をスマートに解決してくれます。

先ほど 今回私の作成した例では、React 単体でも構築可能 と書いたのは特に Dispacher が飛ばす命令を受け取るのが一箇所しかないからあまり有効な使いかたとは言えません(EventEmitterで済む話)。

とはいえ、今回はあくまで Flux の勉強のためあえて使用します。


Store

取得したデータを保存・整形をします。

例えば


  • タグに応じてフィルターをかける

  • 時系列的に古い順に並べ直す

などなど、 Tumblr API が解決できない問題をここで解決します。

各コンポーネントは、マウント時に一旦 Store に EventEmitter 経由で 何か情報に変更が生じたときのコールバックとして、自身の関数を登録します。

コールバックの中で、再度 Store に最新のデータを渡すように記述します。

そういう意味では、完全に一方通行な流れというわけではないかもしれません。( vm を構成に加えれば完全に一方通行にできます。Flux-chat の例を見たときという話)


constants

constants は ActionCreators や WebUtils, Dispacher で使用する共通の変数等を定義するためのファイルが格納されます。図にはないですが、アプリケーションの挙動を明確にすることができるので非常に重要です。


実装

今回の実装をかいつまんで説明します。ちなみに es6 を使用してます。

一応、構築環境は http://qiita.com/monpy/items/16281218b52be0043010 に晒してます。

あとは github みてね。


app.js


app.js

import React from 'react';

import Articles from './components/articles.jsx';
import TumblrWebApiUtils from './utils/tumblr-webapi-utils.js';

TumblrWebApiUtils.getArticleFromOffset();

React.render(
<Articles />,
document.getElementById('container')
);


Web API Utils の関数(記事取得)と render を 順次読んでいます。

先に初期データを読んでおいてそのコールバックとしてレンダリングを走らせるという作りも良かったんですが、そのための処理を書くのが面倒そうだったのでやめました。

今回は、初期状態だと <Articles /> は Store にまだ何も情報がありませんので、空の状態でレンダリングされます。

その後、 Tumblr からデータを引っ張ってくると、図のサイクルが起きて <Articles /> に記事が反映されてきます。

Articles のマウント時かに呼べばいいのではとも考えたのですが、フロー的にはコンポーネントから能動的に呼ぶのは筋が悪そうだと感じたので、 もっとも上のレイヤーから叩いたというわけです。


Components


article.jsx__抜粋

import ArticleStore from '../stores/article-stores.js';

function getStateArticleStores() {
return {
articles: ArticleStore.getAllArticles(),
articlesNum: ArticleStore.getArticlesNum()
};
}

export default class Articles extends React.Component {
 constructor(props) {
  super(props);
this.state = getStateArticleStores();
ArticleStore.addChangeListener(this.onChange.bind(this));
 }
...
onChange() {
this.setState(getStateArticleStores());
}
}


Store と components が双方向にやりとりしているように見えるところ。

やっぱし、components は onChange の際にはデータを一緒に渡してもらった方が良いかもと思う。


Action Creattors


tumblr-action-creators.js

import TumblrAppDispacher from '../dispacher/tumblr-app-dispacher';

import TumblrWebApiUtils from '../utils/tumblr-webapi-utils.js';
import {ActionTypes} from '../constants/tumblr-constants.js';

export default {

addArticle(blogInfo, articles, offset) {
TumblrAppDispacher.dispatch ({
actionType: ActionTypes.ADD,
blogInfo: blogInfo,
articles: articles,
offset: offset
});

},

orderUtilGetArticleFromOffset(offset) {
TumblrWebApiUtils.getArticleFromOffset(offset);
}
}


Web API Util に命令を飛ばす部分 と

Dispacher に信号を送らす部分が定義されている。

actionType は EventEmitter でいうところのイベント名ですが、に使用するのは constants で定義した共通の変数です。


Web API Util


tumblr-webapi-utils.js__抜粋

import TumblrActionCreators from '../actions/tumblr-action-creators.js';

import Jsonp from 'jsonp';

export default {

getArticleFromOffset: function(offset = 0) {
let url = BASE_URL + '?api_key=' + API_KEY + '&offset=' + offset;
Jsonp(url, {} , function(err, res) {
var blogInfo = res.response.blog;
var articles = res.response.posts;
TumblrActionCreators.addArticle(blogInfo, articles, offset);
});
}

}


特に書くことないですが、 es6 の初期値の指定はいい感じですよね。

一応、データを最低限ここでバラして送ってます(むしろ悪手だったか。。)


Dispacher

import {Dispatcher} from 'flux';

export default new Dispatcher();

ここは Flux の機能に甘えるので特に記述することはないです。


Store


articles-sotre.js__抜粋

import TumblrAppDispacher from '../dispacher/tumblr-app-dispacher';

import {ActionTypes} from '../constants/tumblr-constants.js';
import Emitter from 'component-emitter';

var CHANGE_EVENT = 'change';
var _blogData = {};

class ArticleStore extends Emitter {

constructor() {
super();
TumblrAppDispacher.register(this.onAction.bind(this));
}
emitChange() {
this.emit(CHANGE_EVENT);
}

addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
}

removeChangeListener(callback) {
this.off(CHANGE_EVENT, callback);
}

getAllArticles() {
// データがない場合
if ( is.empty( _blogData ) ) {
return [];
}
return _blogData.articles;
}

saveData(blogInfo, articles, offset) {
...
}

onAction(action) {
switch( action.actionType ) {
case ActionTypes.ADD:
this.saveData(action.blogInfo, action.articles, action.offset);
this.emitChange();
break;
default:

break;
}
}
}


まず、 EventEmitter には component-emitter 個人的な理由で採用してます(実際なんでもよし)。

コンストラクタで Dispacher に呼ばれたときのコールバックを登録しておきます。

そのなかで 受け取った actionType によって処理を適当に振り分け、emitChange から view の更新を促します。

save と状況によってはデータを整形して返す部分をここに随時記述していきます。


constants


tumblr-constants.js

import keyMirror from 'keymirror';

export default {

ActionTypes: keyMirror({
// 記事をStoreへ追加する
ADD: null
})

};


今回は、記事を取得するだけのリードオンリーなアプリケーションなのでこんなもんでしょうか?

keyMirror は Flux-chat 流


一時完成品

とりあえず宣伝も込めて(urlが誤字ってるのは秘密)

http://awwwardkun.tumblr.com/


github

現在アップデートなうですが。参考までに。

変数名とか色々間違っているけど。これからまた勉強して修正していこう。

https://github.com/monpy/awwwardskun


感想


  • 一応これを出汁に component をちょっと自分流で弄れば、どこでも tumblr を利用したブログを展開できるはずだ。
    と思っているのだがどうなのだろうか?

  • 記事の詳細ページ(こんなものは使わない方が人類のためだが)の問題もあるし。まぁ課題は多いけど個人的には満足。

  • Flux は始めは本当にどう構築していっていいかわからなかった。けど、フローを理解してみると、
    実際 import しているファイルは、図のように矢印で関係しているものだけですんでいた。当たり前なのだが、それを理解すると役割と内容が完全一致してスラスラ書けるようになった。

  • ということで Flux 一方通行なフローな良さは書いてみないとわかんなかったなぁ。

  • 多分、複数人で共同開発するときもファイルごとに役割が明確だから迷うこともないし、constants がお互いの合言葉になっているので齟齬も起きないだろう。

  • というので、なかなかイカしている気がしてきた。

  • とはいっても、今回のようなシングルページな枠組み・シンプルな状態しかない話である気がする。もっと大きなアプリケーションをつくるときはどうだろうか?(もともとそんなもん react だと無理?)

  • なにはともあれ、react に興味があって 次のステップに行きたい系男子にしては、手頃な実験ができたので満足である。


おわび

さてさて、長々と駄文(主にコード)を書いてしまったが、参考になってくだされば何より。

読んでいただき感謝。

ちなみに、tumblr上で react コードを inline で展開するとバグる(泣)。


参考

お世話になっております。

主に es6 への変換 export の記述の仕方など。これがなかったら絶対完成しなかったし es6 使ってるように見せかけて module export つかっちゃうことになっていた。。感謝!!!!

code grid に書かれていましたが、つかったら良かったので感謝の意をこめて