過去数回に渡って RxJS の基本的な使い方をご紹介してきました。
RxJS 自体はリアクティブ・プログラミングを実現するためのいちライブラリであり、いわゆる Angular や Vue.js、React といったタイプのものとは毛色が異なります ( 強いて言うなら lodash.js のようなタイプが近いかもしれません ) 。そんな Rx をフレームワークの域にまで昇華させた Cycle.js なるものを学びはじめたのでご紹介したいと思います。
一言で言うなら Observable と VirtualDOM を良い感じに組み合わせて提供してくれるフレームワークです。
『フレームワーク』と公式で謳っていますが、Angular のようにあらゆる機能を備えたフルスタックフレームワークというわけではなく、Cycle.js 自体が提供している機能は非常に薄くて少ないものとなっています。RxJS のような Observable 機能を web アプリケーション開発において使いやすくするために薄くラップしたものと捉えていただければ OK です。
GitHub のグラフによると2014年11月2日に最初のコミットがなされています。React の最初のコミットが2013年5月26日、AngularJS ( 1.x系 ) が2010年1月3日、Vue.js は少なくとも2014年2月には世に出ていたので1)First Week of Launching Vue.js、比較的後発の新しいフレームワークということが分かります。
Cycle.js のメイン・コントリビューターである André Staltz 氏は RxJS にも積極的にコミットしており、JavaScript 界隈のリアクティブ・プログラミング領域において強い存在感を放っているようです。
Staltz 氏は Cycle.js をリリースする前に Redux の作者である Dan Abramov(@dan_abramov)氏らと『Flux で良い設計をするためにはどうすればよいか』というテーマについて議論をしています。かねてから Flux に対して課題を感じていた Staltz 氏はその解決案として Cycle.js を開発したとのことです。
If you're doing Flux and feeling confused, hang this on your desk pic.twitter.com/HQQRpgkW27
— Dan Abramov (@dan_abramov) 2015年4月29日
@ryanflorence @dan_abramov Have you heard of Cycle? Is that different enough? https://t.co/dxpeJwICRo
— André Staltz (@andrestaltz) May 4, 2015
Cycle.js がどのようなものなのか、実際に動かして体験してみるとしましょう。公式サイトにある Getting Started のチュートリアルを動かすところまでやってみたいと思います。
Ver.7.0 以降、Cycle.js は TypeScript で実装されていることから、プロダクションコードもTypeScriptで書いていきます。Observable を扱う関係上、型のある TypeScriptの方が適しています。
完成予想イメージはこちら。
サンプルコードはこちらから取得できます。
ディレクトリ構成は以下の通り。
. ├── README.md ├── bin/ │ └── watch.sh ├── bs-config.js ├── gulpfile.js ├── node_modules/ ├── package.json ├── public/ │ ├── index.html │ └── main.js ├── src/ │ └── scripts/ │ └── main.ts ├── tsconfig.json └── yarn.lock
今回の環境構築に必要な Node パッケージをインストールします。任意のディレクトリ ( 今回は hello-cyclejs
とします ) を作成し、 npm プロジェクトを作成します。
# ディレクトリを作成 $ mkdir -p path/to/your/directory/hello-cyclejs # ディレクトリに移動 $ cd path/to/your/directory/hello-cyclejs
次に package.json
を作成します。サンプルなので細かい項目は入力せず全て初期値のままにしてしまいましょう。
$ yarn init ⋮ success Saved package.json ✨ Done in 18.71s. ➜ hello-cyclejs
{ "name": "hello-cyclejs", "version": "1.0.0", "main": "index.js", "license": "MIT" }
npm initでも構いませんが、yarn の方が圧倒的に高速なのでおすすめです。
Ver.7.0以降、Cycle.js は依存ライブラリを RxJS から xstream というものに変更しました。xstream は RxJS の軽量版といった位置づけで、RxJS よりもオペレータの数が厳選されていたり命名に若干変更が入れられたライブラリです。jQuery に対する Zept のようなものだと思っていただければ OK です。開発者である Staltz氏は xstream の使用を強く推していますが、RxJS もこれまで通り使えるよう互換性は担保されています 2)2017年2月9日のアップデートで一部互換性がなくなった箇所があり、そこに関してはハックまがいのことをする必要があります。。今回は RxJS を使うので、これも追加でインストールします。
ターミナルから以下のコマンドを実行して yarn 経由でインストールします。
$ yarn add rxjs xstream @cycle/{dom,run,rxjs-run}
Angular 同様、 Cycle.js もまた単一のファイルではなくモジュールとして機能ごとに分割されており、使用者が必要とするモジュールを組み合わせて使う仕組みとなっております。今回は最低限必要となる3つのモジュールをインストールしました。
@cycle/dom |
DOM とのやり取りを可能にする Cycle.js ドライバ。Cycle.js を使って画面描画をする際は必須。 |
---|---|
@cycle/run |
リアクティブ・プログラミングによって様々な値を加工するアプリケーションの世界 ( main関数 ) と、その値を受け取ってDOM操作やHTTP通信といった副作用を扱う外部世界 ( ドライバ ) を結びつける機能。いわゆる Cycle.js のコアな部分。 |
@cycle/rxjs-run |
RxJS で記述したプロダクションコードを Cycle.js で実行するためのモジュール。 |
他にもいろいろと便利なモジュールが提供されていますが、まずはこの3つから始めていくとしましょう。
公式サイトにある Example コードを書いてみます。
import {Observable} from 'rxjs'; import {div, label, input, hr, h1, VNode, makeDOMDriver} from '@cycle/dom'; import {run} from '@cycle/rxjs-run'; import {DOMSource} from '@cycle/dom/rxjs-typings'; type Sources = { DOM: DOMSource; } type Sinks = { DOM: Observable<VNode>; } /** * アプリケーション * @param sources * @returns {{DOM: Observable<VNode>}} */ function main(sources: Sources): Sinks { // キー入力イベントを取得 ( Intent ) const input$: Observable<Event> = sources.DOM.select('.field').events('input'); // 入力イベントから現在の状態ないし値を取得 ( Model ) const name$: Observable<string> = Observable.from(input$) .map((ev: Event) => (ev.target as HTMLInputElement).value) .startWith(''); // 現在の状態を画面に描画 ( View ) const vdom$: Observable<VNode> = name$.map(name => { return div('.well', [ div('.form-group', [ label('Name: '), input('.field.form-control', {attrs: {type: 'text'}}), ]), hr(), h1(`Hello ${name}`) ]); }); // 結果をドライバに出力する ( Sinks ) return { DOM: vdom$ }; } // アプリケーションからの戻り値を受け取るドライバ群を定義 const drivers = { DOM: makeDOMDriver('#app-container') // DOM をレンダリングするドライバ }; // アプリケーションとドライバを結びつける run(main, drivers);
Cycle.js の仕組みについては追って解説しますので、ここでは簡単に処理の流れだけを解説します。まず Cycle.js は『アプリケーション世界 ( main()
関数 ) 』と『外部世界 ( ドライバ層 )』と世界を大きく二つに分割しており、アプリケーションからの戻り値 ( Sinks ) を外部世界が受け取って、その結果 ( Sources ) を再びアプリケーションが受け取るという、その名の通り処理が循環するような動きをします。
main() 関数では、sources に含まれる DOM 情報の中にあるテキストインプットの入力イベントを取得し ( Intent
) 、そこから現在の入力値を取り出して目的のための処理を行い ( Model
) 、それを元に描画したい DOM 構造を記述しています ( View
) 。最後にその結果を外部世界に放出 ( Sinks ) 。
ドライバはアプリケーションから放出された値を受け取り、主に副作用を伴う処理を担います。ここでは DOM をレンダリングする DOMDriver
という Cycle.js 公式のドライバを使います。makeDOMDriver
という関数に#app-container
という引数を渡します。これはDOMのレンダリング結果を表示するコンテナ要素のセレクタです。
最後にrun
関数を実行してアプリケーションとドライバを結びつけ、処理を循環させます。
DOMSource
からのイベントストリームは完全に xstream クラスとなりました。そのため RxJS ベースで書くには25行目にあるようにObservable.from(input$)
と書いて RxJS の Observale クラスに強制変換してあげる必要があります。
ベースとなる HTML を作成します。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Hello Cycle.js</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> </head> <body> <div id="app-container"></div> <script src="main.js"></script> </body> </html>
body には <div id="app-container" />
のみ記述されています。makeDOMDriver の引数に渡した要素がこちらです。中身は Cycle.js にて全て動的に描画するので、HTMLはこれだけで充分です。
TypeSscript をコンパイルするための Node パッケージとローカルサーバを起動するための browser-sync をインストールします。
$ yarn add -D typescript tsify browserify watchify browser-sync
先ほど書いたコードと Cycle.js のコードを Bundle ( 依存関係を解決して単一のファイルに結合 ) するために Browserify もインストールします。Browserify はそのままでは JavaScript ファイルしか扱えないため、tsify を使って TypeScript ファイルも扱えるようにします。更にファイルの編集を監視して自動でコンパイルしてくれるように Watchify もインストールします。
Browserify や Watchify につきましては以下の記事で詳しく解説していますので、併せてご参照ください。
ビルドタスクを定義します。Gulp や webpack を使うのが昨今の流行りですが、この程度の規模であれば npm script を使って直接コンパイルとサーバ起動を実行するのでも何とかなります。ということでシェルスクリプトでタスクを定義しましょう。
#!/usr/bin/env bash # Compile TypeScript sources nohup watchify -d src/scripts/main.ts -p [ tsify ] -o public/main.js & browserify_pid=$! trap "kill -15 $browserify_pid $>/dev/null" 2 15 # Run Server nohup browser-sync start --config bs-config.js & browserSync_pid=$! trap "kill -15 $browserSync_pid &>/dev/null" 2 15 tail -f nohup.out
詳細は割愛しますが、Watchify を使ってTypeScript のコンパイルと変更を監視し、browser-sync でローカルサーバを起動させています。browser-sync の設定は bs-config.js
という設定ファイルに定義しています。
package.json
に以下のコードを追記して npm script コマンドを定義します。
"scripts": { "watch": "bash ./bin/watch.sh" },
以下のコマンドを実行して、コンパイルとサーバ起動を実行します。
$ npm run watch
ブラウザが起動し、冒頭に紹介したデモが表示されましたでしょうか。なんてことのないデータバインディングのデモですが、これで Cyclejs デビューが出来ました。
ちなみに Gulp を使ってコンパイルとサーバ起動をする場合はこちら。
const gulp = require('gulp'); const browserify = require('browserify'); const watchify = require('watchify'); const source = require('vinyl-source-stream'); const browserSync = require('browser-sync'); const runSequence = require('run-sequence'); /** Compile & Bundle TypeScript sources by Watchify */ gulp.task('script', () => { const b = browserify({ cache: {}, packageCache: {}, debug: true }); const w = watchify(b); const bundle = () => { return w .add('./src/scripts/main.ts') .plugin('tsify') .bundle() .pipe(source('main.js')) .pipe(gulp.dest('./public/')) .pipe(browserSync.reload({ stream: true })); }; w.on('update', bundle); return bundle(); }); /** Run Web server */ gulp.task('serve', () => { return browserSync.init(null, { server: { baseDir: './public/' }, reloadDelay: 1000 }); }); gulp.task('default', () => runSequence('script', 'serve'));
内容は先ほどのシェルスクリプトと全く同じです。タスクの見通しはこちらの方が良いですが、処理スピードはシェルスクリプトの方が圧倒的に上です。
次のページでは Cycle.js の設計思想と大まかな仕組みについて解説します。
脚注
1. | ↑ | First Week of Launching Vue.js |
2. | ↑ | 2017年2月9日のアップデートで一部互換性がなくなった箇所があり、そこに関してはハックまがいのことをする必要があります。 |
リクルートマーケティングパートナーズではたらく Web フロントエンド・エンジニア。サプリシリーズをはじめとした内製サービスの開発メンバーとして、仕様策定や技術選定にも携わる。ブログなどで積極的にWeb技術の情報発信に取り組んでいるが、実はプログラミングよりも洋服を自作する方が得意だったりする。
この執筆者の記事一覧
※ コメントはこちらのに同意の上、投稿ください。