1. masakielastic
Changes in body
Source | HTML | Preview

Cycle.js の勉強を始めて、RxJS の習熟度不足を痛感したので、基本練習に取り組むことにしました。

セットアップ

RxJS

npm install rx
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.7/rx.all.min.js"></script>

RxJS-DOM

npm install rx-dom
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs-dom/7.0.3/rx.dom.min.js"></script>

RxJS-jQuery

npm install rx-jquery
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs-jquery/1.1.6/rx.jquery.min.js"></script>

Fetch API

Can I use のサイトで Fetch API に対応するブラウザーの一覧 を調べることができます。古いブラウザーのために、Github がpolyfill を公開しています。

定義されるメソッドの一覧

RxJS は こちら、RxJS-DOM はこちら、RxJS-jQuery はこちらを参照。

プロミス

var promise = new Promise(function (resolve, reject) {
    resolve(10);
});

var source = Rx.Observable.fromPromise(promise);

var subscription = source.subscribe(
  function (x) { console.log('onNext: %s', x); },
  function (e) { console.log('onError: %s', e); },
  function () { console.log('onCompleted');
});

var promise2 = new Promise(function (resolve, reject) {
    reject(new Error('reason'));
});

var source2 = Rx.Observable.fromPromise(promise2);

var subscription2 = source2.subscribe(
  function (x) { console.log('onNext: %s', x); },
  function (e) { console.log('onError: %s', e); },
  function () { console.log('onCompleted'); }
);


ジェネレーター
------------

```js
function* range() {
  var index = 0;
  while(true) {
    yield index++;
  }
}

Rx.Observable.from(range())
  .take(10)
  .subscribe(function (x) {
    console.log('値: %s', x);
});

FizzBuzz

FizzBuzz から練習をはじめよう。1 から 100 までの整数で構成されるシーケンスを生成するには Rx.Observable.range を使う。

var Rx = require('rx');
var source = Rx.Observable.range(1, 100);

source.subscribe(function(value) {
  if (value % 15 === 0) {
    console.log('fizzbuzz');
  } else if (value % 3 === 0) {
    console.log('fizz');
  } else if (value % 5 === 0) {
    console.log('buzz');
  } else {
    console.log(value);
  }
});

map を使って FizzBuzz の判定を複数の関数に分割してみよう。

var Rx = require('rx');
var source = Rx.Observable.range(1, 100);

source
  .map(function(value) {
    return value % 15 === 0 ? 'fizzbuzz' : value;
  })
  .map(function(value) {
    return value % 3 === 0 ? 'fizz' : value;
  })
  .map(function(value) {
    return value % 5 === 0 ? 'buzz' : value;
  })
  .subscribe(function(value) {
    console.log(value);
  });

複雑なシーケンスを生成できるように Rx.Observable.generate が用意されている。

var Rx = require('rx');

var source = Rx.Observable.generate(
  1,
  function (value) { return value < 101; },
  function (value) { return value + 1; },
  function (value) { 
    if (value % 15 === 0) {
      return 'FizzBuzz';
    } else if (value % 3 === 0) {
      return 'Fizz';
    } else if (value % 5 === 0) {
      return 'Buzz';
    } else {
      return value;
    }
  }
);

source.subscribe(function (x) {
  console.log(x);
});

今度は配列を繰り返す方式に取り組んでみよう。ES6 のジェネレーターで無限のシーケンスを生成し、take で最初の100をとる。

var Rx = require('rx');

function *list()
{
   var list = [
    '', '', 'Fizz', '', 'Buzz',
    'Fizz',  '', '', 'Fizz', 'Buzz',
    '', 'Fizz', '', '', 'FizzBuzz'
  ];

   while (true) {
     for (var e of list) {
       yield e;
     }
   }
}

Rx.Observable.from(list())
  .take(100)
  .map(function(value, index) {
    return value === '' ? index + 1: value;
  })
  .subscribe(function(value) {
    console.log(value);
  });

今度は Fizz と Buzz の生成を分割してみよう。それぞれ別のシーケンスを生成した後で Rx.Observable.zip を使って統合する。

var Rx = require('rx');

function *fizz()
{
   var list = ['', '', 'Fizz'];

   while (true) {
    for (var e of list) {
      yield e;
    }
   }
}

function *buzz()
{
   var list = ['', '', '', '', 'Bizz'];

   while (true) {
    for (var e of list) {
      yield e;
    }
   }
}

var source = Rx.Observable.zip(
  Rx.Observable.from(fizz()).take(100),
  Rx.Observable.from(buzz()).take(100),
  function (a, b) {
    return a + b;
  }
);

source
  .map(function(value, index) { return value === '' ? index : value; })
  .subscribe(function (x) {
    console.log('%s', x);
  });

現在時刻を表示する

1秒単位で現在時刻を表示してみましょう。

<div id="result"></div>
var result = document.getElementById('result');
var source = Rx.Observable
    .interval(1000)
    .timeInterval();

source.subscribe(
  function (x) {
    result.innerHTML = (new Date()).toLocaleString();
  },
  function (err) {
    console.log('エラー: ' + err);
  },
  function () {
    console.log('完了');
});

ユーザーの入力を表示する

テキストボックスの入力をそのまま表示させてみましょう。Cycle.js のトップページにもコードサンプルが掲載されています。

<input id="textInput" type="text">
<div id="result"></div>
var textInput = document.querySelector('#textInput');
var result = document.querySelector('#result');

Rx.Observable.fromEvent(textInput, 'input')
  .pluck('target', 'value')
  .startWith('')
  .subscribe(function(value) {
    result.innerHTML = value;
  });

pluck の代わりに map を使うこともできます。

.map(function(ev) { return ev.target.value; })

クリックカウンター

ボタンをクリックするとカウンターの値が1つ増えるものとします。

<button id="plus">増やす</button>
<div id="result">0</div>
var plus = document.getElementById('plus');
var result = document.getElementById('result');

var source = Rx.Observable.fromEvent(plus, 'click')
  .map(function() { return +1; })
  .startWith(0)
  .scan(function(acc, value) { return acc + value; })
  .subscribe(function(value) { result.innerHTML = value; });

次にクリックするとカウンターの値が1減るボタンおよびカウンターの値が0になるリセットボタンを追加してみましょう。

<button id="plus">増やす</button>
<button id="minus">減らす</button>
<button id="reset">リセット</button>
<div id="counter"></div>
var counter = document.getElementById('counter');

var plus = Rx.Observable
  .fromEvent(document.getElementById('plus'), 'click').map(function() { return +1; });
var minus = Rx.Observable
  .fromEvent(document.getElementById('minus'), 'click').map(function() { return -1; });
var reset = Rx.Observable
  .fromEvent(document.getElementById('reset'), 'click').map(function() { return 0; });

Rx.Observable.merge(plus, minus, reset)
  .startWith(0)
  .scan(function(acc, value) { return value === 0 ? 0 : acc + value; })
  .subscribe(function(value) { counter.innerHTML = value; });

Ajax

httpbin.org で練習します。

GET

ES6 Promise に対応している Fetch API を使ってみましょう。Promise から Observable を生成するには fromPromise を使います。

var promise = fetch('http://httpbin.org/get')
  .then(function(response) {
    return response.json();
  }).then(function(json) {
    return json.origin;
  }).catch(function(ex) {
    return ex;
  });

Rx.Observable.fromPromise(promise)
  .subscribe(
  function (value) {
    console.log(value);
  },
  function (err) {
    console.log('エラー: %s', err);
  },
  function () {
    console.log('完了');
  });

Rx.Observable.spawn を使ってジェネレーターによる非同期処理をやってみよう。

var Rx = require('rx');
var request = require('request');

var get = Rx.Observable.fromNodeCallback(request);

Rx.Observable.spawn(function* () {
  var data;

  try {
    data = yield get('http://httpbin.org/get').timeout(5000 /*ms*/);
  } catch (e) {
    console.log('Error %s', e);
  } 

  console.log(JSON.parse(data[0].body).origin);
}).subscribe();

POST

まずは FormData を使ってみましょう。Content-Type の値は multipart/form-data になります。

var data = new FormData();
data.append('input', 'bar');

var promise = fetch('http://httpbin.org/post', {
  method: 'post',
  body: data
}).then(function(response) {
    return response.json()
  })
  .then(function(json) {
    return json.form;
  })
  .catch(function(ex) {
    return ex;
});

Rx.Observable.fromPromise(promise)
  .subscribe(
  function (value) {
    console.log(value);
  },
  function (err) {
    console.log('エラー: %s', err);
  },
  function () {
    console.log('完了');
  });