タイトルはあれですが riotとReduxの組み合わせのアプリの素振りをまとめました。
ということでこんなの作りました。
題材
普通のWebアプリでもいいかなぁって思ったのですがどうせならザクザク動くものがいいなぁと思って適当にサーフィンしていたら、Matrix 風のEffectを再現したスクリプトを見つけたのでそれをRedux , riot に落とし込んでみようかなぁっと考えました。1
元のスクリプト
元になるスクリプトは[こちら]
(http://thecodeplayer.com/walkthrough/matrix-rain-animation-html5-canvas-javascript)になります
ソースを抜粋しておきます
//drawing the characters
function draw()
{
//省略
//looping over drops
for(var i = 0; i < drops.length; i++)
{
//a random chinese character to print
var text = chinese[Math.floor(Math.random()*chinese.length)];
ctx.fillText(text, i*font_size, drops[i]*font_size);
if(drops[i]*font_size > c.height && Math.random() > 0.975)
drops[i] = 0;
//incrementing Y coordinate
drops[i]++;
}
}
setInterval(draw, 33);
フォントサイズ分のカラムを用意してカラム分の配列にY座標を記録していきます。
setInterval 毎に座標を一つづつ更新していき下まで達した段階でランダムに座標をゼロに戻します。
仕様
- 初期値: 0 - 9 までの文字列
- 30秒毎にランダムな絵文字を混入させていく
- 外部からのフォントサイズの変更
- 外部からの文字列の混入
せっかくStateをRedux管理にするので出来心から外部から動的に変数を変えられるようにしてみました。
実装
setInterval の部分
元のスクリプトではdraw関数をそのままsetIntervalに登録しているとStateの管理をReduxに預けることができないのでまずはupdate用のアクションクリエータを登録するようにしました。
setInterval(draw, 33)
=> setInterval(あくしょんくりえーた, 33)
Storeに保持するものと保持しないもの
正直どこまでStateに保存すべきか悩みましたが描画に関するものは全てState管理するようにしました。
State管理にしたもの
- 各カラムの座標
- 各カラムの文字
- fontSize
- canvas のwidth, height
- context
Stateから外したもの
- バックグラウンドのスタイル
基本動的な変更を付け加えたい(今回の実装では残念ながら固定値でいいようなケースでも)項目は全てStateの管理にすることにしました。
ここまでくると draw関数は入力された値を元に描画するだけの関数となりかなりシンプルになります。
const draw = ({ctx, textArray, fontSize, fontColor, height, width, drops}) => {
ctx.fillStyle = "rgba(0, 0, 0, 0.05)"
ctx.fillRect(0, 0, width, height)
ctx.fillStyle = fontColor
ctx.font = fontSize + "px arial"
for (let i = 0; i < drops.length; i++) {
ctx.fillText(drops[i].text, i * fontSize, drops[i].pos * fontSize)
}
}
const store = this.opts.store
store.subscribe(() => {
draw(store.getState())
})
Storeの変更を検知してそのままdraw関数に流すだけとなり描画部分とデータ部分を切り離すことができました。
Randomな振る舞いをどう扱うか
Randomな座標や生成する文字列をどこで計算すべきかということです。
案1. Reducersの中で処理する
Reducers内では前回のStateを参照できるので、そこからrandomな文字列や座標を計算して新しいStateを返すという処理が比較的簡単に書けそうです。
しかしドキュメントによればReducersは純関数であることが期待されます
Randomという冪等制を持たない関数をreducer内で行うのは違うと思いやめました。
案2 ActionCreator内で処理する
ActionCreatorで処理して変数を Reducersに引き渡すのが良さそうです。
ただActionCreator内でMath.randomを各々呼ぶとテストがしにくいと思ったので少し違う方法をとるようにしました。
案3 RNGを設定できるMiddlewareを自作する
テストのことを考えると外部からRandom-Number Generator(RNG)を渡せるような構造にしておきたいところです。
と考えると実装そしてはこんな感じになります
export const randomTest = createAction(TEST, (value, rng = Math.random()) => ...process...)
各アクションにランダムの引数入れるのイケテないなぁっと思って特定のActionの場合だけRNGを引き渡すようなMiddlewareを自作しました。
こんな風に使います
export const moveDrops = () =>
randomAction((rng, state) =>
rng() <- この返り値が丸ごとpayloadになる
)(updateDropsAction)
randomActionのActionを作成して引数にRNG, とStateを渡すようにしました。
RandomActionの第一引数の処理が終わった後に次のアクションであるupdaeDropsActionを叩くきます
ソースはこんな感じ
const RANDOM = 'RANDOM'
const randomColumns = (rng = Math.random.bind(this)) => store => next => action => {
if(action.type === RANDOM && typeof action.fn === 'function'){
const r = action.fn(rng, Object.assign({}, store.getState()))
action.nextAction && store.dispatch(action.nextAction(r))
}
return next(action)
}
export const randomAction = fn => nextAction => ({
type: RANDOM,
fn,
nextAction
})
export default randomColumns
こうすることでrngを外部から指定するということをあまり意識せずにランダムな関数を使えるようになります。
ちなみにこれ以外でもcomponentで頑張るってのもアリです。
Middlewareの作り方はReduxのドキュメントを参考にしました。
すごくわかりやすいです
http://redux.js.org/docs/advanced/Middleware.html
デモ
ソース
GitHub Pages へのデプロイ
そういえばgh-pagesに自動デプロイをしたことなかったのでここを参考にした
https://gist.github.com/domenic/ec8b0fc8ab45f39403dd
http://qiita.com/dora56/items/cafae475daec802b6b8f
断念したこと
Flow
riotの型の部分でサクッと導入できなかったので断念した。もうちょっと粘りたかったがタイムオーバー。
redux-loop
非同期な部分がなかったので導入しなかった。けど少し興味のあるライブラリ。
まとめ
今のコードだとStoreをどこからでも参照できるので若干危険な匂いがするコードになっています。その辺をうまく閉じ込めつつコンポーネントやActionCreaterで触らなくてもいいような感じにしたいところです。
react-reduxなんかはそこらへん上手くやっててStateを扱う部分は割と限定的にしつつ実装ができた気がします。
-
ックスの部分がかかってたからというのが本音 ↩