多くの開発者はアプリ開発時になんらかのフレームワークを利用しているでしょう。フレームワークは複雑なアプリ構築の手間をなくし、時間を節約します。利用者が多いからか、最高のフレームワークや、フレームワークの何を学ぶべきかなどの話題があふれています。
Reactは現在もっとも人気のあるフロントエンドフレームワークで、大きなコミュニティもあります。支持する人もいれば、言われているほどには良くないという人もいまが、私はReactでWebアプリに対する考え方や開発方法が変わりました。
Webアプリの開発方法の検討さえせずReactを使い始めましたが、じわじわと人気が出ている新しいリアクティブ・フレームワーク「Cycle.js」を試すことにしました。そこで学んだリアクティブ・プログラミングや、Cycle.jsの役割、なぜReactよりも優れているかを説明します。
リアクティブ・プログラミングとは
リアクティブ・プログラミング(RP)は非同期データストリーム(ページ更新無しの非同期で往来するデータ)を扱うプログラミングです。すでにWebアプリを開発したことがあれば、リアクティブ・プログラミングの経験があるでしょう。たとえば非同期データストリームで、クリックイベントを監視して、クリック発生時に処理を施すのもRPです。RPの根底には、クリックに限らずどんなところからでもデータストリームを発生させ、操作できることがあります。同じ効果や処理をするにしても、メソッド等の抽象は使いやすく、維持やテストがしやすくなります。
リアクティブ・プログラミングなら、統一されて一貫性のあるコードになります。クリックイベント、HTTPリクエスト、Web Socketなど、どんなデータでも、同じように記述しすべてデータストリームとして扱うので、適切な実装方法について考える必要はありません。さらに、mapやfilterなど便利な関数が多数用意されています。関数は新たなストリームを返し、そのストリームでまた別の処理をします。
リアクティブ・プログラミングはコードをさらに抽象化します。優れたユーザー体験が実現し、ビジネスロジックの構築に集中できるのです。
JavaScriptのリアクティブ・プログラミング
JavaScriptにはデータストリームを扱うための優秀なライブラリーがいくつかあります。有名なライブラリーにRxJSがあります。ReactiveXの拡張モジュールで、データストリームを監視して非同期プログラミングができるAPIです。観察対象(observable)であるデータストリームを作成し、さまざまな関数で処理をします。
ほかにもMost.jsがあります。性能比較からパフォーマンスがいいことが分かります。
Cycle.jsの開発者がCycle.jsのために用意した、軽量かつ高速なライブラリーxstreamは、メソッド数がたったの26個で、サイズはわずか約30KBです。JavaScriptのリアクティブ・プログラミング用ライブラリーでは高速の部類です。
以下の例ではxstreamライブラリーを使用します。Cycle.jsは軽量フレームワークなので、加えるリアクティブライブラリーも軽量にしました。
Cycle.jsとは
Cycle.jsは関数型かつリアクティブなJavaScriptフレームワークです。開発するアプリは関数main()として抽象化されます。関数型プログラミングの関数は入力と出力のみなので、関数の引数と戻り値以外が変化したり、外部との通信などの副作用はありません。main()関数の入力(ソース)は外部からの読み込み、出力(シンク)は外部への書き出しに該当します。外部への入出力はドライバー(DOM効果やHTTP効果、Webソケットなどを扱うプラグイン群)を通して管理します。
ユーザーインターフェイスの作成とテスト、再利用可能なコード記述が簡単にできます。各コンポーネントは独立した純粋な関数です。コアAPIは関数runです。
run(app, drivers);
2つの引数appとdriversがあります。appはアプリのメイン関数で、driversは副作用を扱うためのプラグインです。
Cycle.jsは追加の機能を小さなモジュールとして分離しています。
- @cycle/dom:DOM操作用のドライバー群。バーチャルDOMライブラリーsnabdomがベースのDOMドライバーとHTMLドライバー
- @cycle/history:History API用ドライバー
- @cycle/http:superagentベースの、HTTPリクエスト用ドライバー
- @cycle/isolate:任意のスコープを付与したデータフローコンポーネントを作成するための関数
- @cycle/jsonp:JSONPによるHTTPリクエスト用ドライバー
- @cycle/most-run:mostによる、アプリのrun関数
- @cycle/run:xstreamによる、アプリのrun関数
- @cycle/rxjs-run:rxjsによる、アプリのrun関数
Cycle.jsのコード
Cycle.jsのコードを解説します。動作を解説するための簡潔なアプリとして「昔ながらのカウンター(計数機)」を作成します。Cycle.jsでのDOMのイベントの扱い方やDOMの再描画が分かります。
2つのファイルindex.htmlとmain.jsを作成します。index.htmlは、アプリのすべてのロジックが入っているmain.jsを参照するだけです。また、package.jsonファイルを以下のコマンドで作成します。
npm init -y
続いて、メインの依存オブジェクトをインストールします。
npm install @cycle/dom @cycle/run xstream --save
@cycle/dom、@cycle/xstream-run、xstreamをインストールしました。babel、browserify、mkdirpも必要なのでインストールします。
npm install babel-cli babel-preset-es2015 babel-register babelify browserify mkdirp --save-dev
Babelを使うために、.babelrcファイルを作成します。
{
"presets": ["es2015"]
}
package.jsonファイルに以下のスクリプトを加えます。
"scripts": {
"prebrowserify": "mkdirp dist",
"browserify": "browserify main.js -t babelify --outfile dist/main.js",
"start": "npm install && npm run browserify && echo 'OPEN index.html IN YOUR BROWSER'"
}
Cycle.jsアプリを実行するためにはnpm run startコマンドを使います。
準備は完了です。コーディングを開始します。index.htmlに若干のHTMLを書き加えます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Cycle.js counter</title>
</head>
<body>
<div id="main"></div>
<script src="./dist/main.js"></script>
</body>
</html>
id名mainのDIV領域を作成しました。Cycle.jsを紐づけて、アプリの内容をこの領域に描画します。main.jsから変換・生成されバンドルされたJavaScriptファイルのdist/main.jsファイルも読み込みました。
Cycle.jsのコードを書きます。main.jsファイルを開き、必要な依存オブジェクトを導入します。
import xs from 'xstream';
import { run } from '@cycle/run';
import { div, button, p, makeDOMDriver } from '@cycle/dom';
xstream、run、makeDOMDriver、それとバーチャルDOM(div、button、)操作のための関数を読み込みました。
続いて、main関数を書きます。
function main(sources) {
const action$ = xs.merge(
sources.DOM.select('.decrement').events('click').map(ev => -1),
sources.DOM.select('.increment').events('click').map(ev => +1)
);
const count$ = action$.fold((acc, x) => acc + x, 0);
const vdom$ = count$.map(count =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + count)
])
);
return {
DOM: vdom$,
};
}
run(main, {
DOM: makeDOMDriver('#main')
});
sourcesを受け取ってsinksを返します。sources(ソース、源)はDOMストリーム、sink(シンク、受信側)はバーチャルDOMです。順を追って説明します。
const action$ = xs.merge(
sources.DOM.select('.decrement').events('click').map(ev => -1),
sources.DOM.select('.increment').events('click').map(ev => +1)
);
2つのストリームを、ストリーム「action$」に統合します。ストリームを含む変数の名前の最後には$を付けるのが慣習です。decrement(1減らす)ボタンのクリックとincrement(1増やす)ボタンのクリックのストリームです。2つのイベントをそれぞれ-1と+1とします。これらを統合すると、action$ストリームの中身は以下の通りです。
----(-1)-----(+1)------(-1)------(-1)------
次のストリームはcount$です。
const count$ = action$.fold((acc, x) => acc + x, 0);
fold関数はaccumulateとseed、2つの引数を受け取ります。はじめのイベントが来るまではseedの状態です。次のイベントではaccumulate関数に基づいて計算しseedと合算します。ストリームのreduce()に当たります。
count$ストリームの初期値は0を受け取り、action$ストリームから新しい値を常に合計してcount$ストリームの現在値になります。
サイクルを動作するため、main関数の下にあるrun関数を実行します。
最後にバーチャルDOMを作ります。
const vdom$ = count$.map(count =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + count)
])
);
count$ストリームのデータを振り分けて、ストリームの各データに対し、バーチャルDOMを返します。バーチャルDOMに含まれるのは、メインであるDIVラッパーが1つと、2つのボタン、1つの段落です。Cycle.jsはDOM操作にJavaScriptの関数を使っていますが、JSXも使用できます。
このmain関数はバーチャルDOMを返します。
return {
DOM: vdom$,
};
このmain関数と、ID名「main」のDIV領域に結び付けられたDOMドライバーを渡して、DIVからイベントストリームを取得します。以上で、Cycle.jsアプリの完成です。
以下のように動きます。
以上、DOMストリームの扱い方です。
すべてのコードをGithubリポジトリに掲載しました。ローカル環境で実行してください。
ReactからCycle.jsへ乗り換える理由
リアクティブ・プログラミングの基本を理解し、Cycle.jsの簡単な実例を理解したところで、私がなぜCycle.jsを採用するのかお伝えします。
Webアプリを設計する際の問題は、膨大なコードベースおよび外部から来る膨大な量のデータをどう取り扱うかでした。私はReactが好きでたくさんのプロジェクトに使ってきましたが、Reactではこの問題は解決しませんでした。
データを描画すること、アプリのステートを変更することはReactが適しています。コンポーネントの概念はすばらしく、テストや維持もやりやすい優れたコードが書けました。でも、いつもなにかが足りなかったのです。
Reactの代わりにCycle.jsを使う場合の良い点・悪い点をまとめました。
良い点
1.大きなコードベース
Reactは100個のコンテナ内に、100個のコンポーネントがあり、それぞれが独自のスタイル、メソッド、テストを持っています。無数のフォルダーの中の無数のファイルの中に、何行にもわたるコードがあるわけです。アプリが肥大化すると、これらをかき分けていくのは骨の折れます。
Cycle.jsならプロジェクトを、副作用の無い、隔離されていてテストも可能な独立したコンポーネントに分割するので、大きなコードベースを扱えます。Reduxも副作用もない、純粋なデータストリームなのです。
2.データフロー
Reactの最大の悩みはデータフローです。Reactはデータフローを念頭に置いて設計されていないため、コアにはありません。開発者は対処するために多くのライブラリーや方法論を編み出しました。一例はReduxですが完璧ではありません。設定に時間を費やし、データフローを扱うためのコードを書く必要があります。
Cycle.jsは、データフローが扱えるフレームワークとして設計されています。データを取り扱う関数を書くだけでいいのです。
3.副作用
Reactアプリで副作用についての一貫した方法はありません。対処するためのツールはたくさんありますが、準備し使い方を覚える必要があります。人気のツールはredux-saga、redux-effects、redux-side-effects、redux-loopと、たくさんあります。ライブラリーを選んで、コードベースに実装するのは時間も労力もかかります。
Cycle.jsは必要なドライバー(DOM、HTTP、その他)を読み込んで使うだけです。ドライバーは、記述した関数にデータを送るので、処理して送り返すことで再びドライバーが反映します。ドライバーはCycle.js公式で標準化されているので、サードパーティ製に依存する必要はありません。シンプルですね。
4.関数型プログラミング
Reactの製作者はReactは関数型プログラミングを使っていると主張しますが、実際は異なります。たくさんのオブジェクト、クラス、thisキーワードを使用し、正しく使わなければ悩まされます。Cycle.jsは関数型プログラミングの概念を基に作られています。外部の状態に一切依存しない関数でできています。クラスや、準ずるものもありません。テストや保守が簡単です。
悪い点
1.コミュニティ
Reactは人気のフレームワークで、あらゆる場面で使われています。Cycle.jsは違います。人気とは言えず、予期せぬ状況に陥り、コードの解決策がインターネット上にも見つからず、自力で解決することもあります。サイドプロジェクトで時間がたっぷりあるならいいのですが、締め切りに追われる企業であればコードのデバッグに時間を取られます。
この状況は変わりつつあります。Cycle.jsのユーザーが増え、問題を議論し、力を合わせ解決しています。Cycle.jsには優れたドキュメント類やサンプルが多く掲載されているため、デバッグできないほど複雑な問題には遭遇したことがありません。
2.新たなパラダイムを覚える
リアクティブ・プログラミングは異なるパラダイムなので、学ぶのにはある程度時間を必要とします。一度覚えれば以降はすべてが楽になりますが、もしいま締め切りに追われている状況ならば、新しいことの学習に時間を費やすのは苦しいかもしれません。
3.アプリによってはリアクティブである必要はない
ブログ、マーケティングサイト、初期表示ページ、そのほか限られたやり取りしかしない静的なサイトなら、リアクティブでなくても大丈夫です。リアルタイムに行き交うデータはなく、フォームやボタンも少ししかありません。この場合リアクティブ・フレームワークはかえって遠回りです。WebアプリにCycle.jsが必要かどうか吟味したほうが良いでしょう。
最後に
理想的なフレームワークは、開発者が機能の作成と実装に集中できることです。ひな形のコード入力を強いるべきではありません。Cycle.jsは開発者がより良いコードを書くことと機能を実現することを追求できます。ただしこの世に完璧なものは無く、改善の余地は常にあります。
本記事はMichael Wanyoikeが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Why I’m Switching from React to Cycle.js)
[翻訳:西尾 健史/編集:Livit]