Riot + Obseriot で優しく Flux する

[Riot Advent Calendar](http://qiita.com/advent-calendar/2016/riotjs) の 17 日目です。

Riot はとっても優しい UI ライブラリです。Riot には [riot-observable](https://www.npmjs.com/package/riot-observable) というライブラリが同梱( v3.0.4 現在 )されていて、それを使えば Flux などのライブラリを使わなくても Flux のようなことができます。しかしアプリケーションの規模が大きくなるにつれてそのままの `riot-observable` だとコードが大変なことになりやすいので、[Obseriot](https://www.npmjs.com/package/obseriot) というライブラリをつくりました。

# TL;DR

Obseriot はビューに依存しないオブザーバーパターンライブラリです。もともと Flux 実装のために作ったのではありませんが、オブジェクトを `listen` したり `notify` する特性を使って Flux のように使うこともできます。

詳しくは [こちらの記事](http://qiita.com/aggre/items/dfad1c7cf2b70fb2a2fe) にまとめています。

今回はチュートリアル形式でざっと進めていきたいと思います。

チュートリアルで作成するプロジェクトは [GitHub](https://github.com/aggre/try-riot-with-obseriot) で公開しています。

また、プロジェクトは Now でも公開しました。=> https://try-riot-with-obseriot-goqrgqnhcn.now.sh

( チュートリアルのプロジェクトを `git clone` して `now` するだけです。Now 本当に便利だなぁ... )

# 書いていきます

今回目指すアプリケーションは、**Yahoo.com の Weather API を利用して、知りたい都市の天気を表示する** というものです。

Yahoo.com の Weather API は認証不要で使いやすいです。

> [Yahoo Weather API](https://developer.yahoo.com/weather/)

## index.html

まだ Obseriot は関係ありません。
ただの `index.html` です。

Roit のカスタムタグをマウントするための `app` という要素と、Riot や Obseriot をひとまとめにした `bundle.js` を読み込むようになっています。

`app` は今回のサンプルでそうしているだけで、名前は整合性さえとれていればなんでも構いません。`bundle.js` はまだ作っていないので、この HTML をブラウザで開いても何も表示されません。追って作っていきましょう。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Try Riot with Obseriot</title>
  </head>
  <body>

    <app></app>
    <script src="dist/bundle.js" charset="utf-8"></script>

  </body>
</html>
```

> `index.html` はルートに置いて、`bundle.js` を構成することになる各ソースは `src` というディレクトリを作ってそこに置いていきます。

## アクションとストア

カスタムタグで `import` するためのアクションとストアを作ります。

今回は `/src/lib` に以下ファイルを追加します。

```js:/src/lib/action.js
export default {}
```

```js:/src/lib/store.js
export default {}
```

Obseriot ではアクションもストアもオブジェクトなのでこれだけです。

## アクションを拡張

先ほど作ったアクションにすべてのアクションを書いてもいいのですが、1 アクション毎にファイルが分かれていると見通しがよくなり共同作業もしやすいので、今回はそうします。

**`action.fetch.weather`** というアクションを発行すると、**`store.wether`** というストアが更新される設計にしたいと思います。

では `/src/js/action` にファイルを追加していきます。

まずは `/src/lib/action.js` までのパスを書くのが大変なのでショートカットするためのファイルから。

```js:/src/js/action/action.js
import action from '../../lib/action'
export default action
```

`action.fetch` をつくります。呼ばれることはないので空です。もしも今後 `action.fetch` を呼びたくなったらこのファイルを編集すればいいですね。

```js:/src/js/action/action.fetch.js
import action from './action'

action.fetch = {}

export default action.fetch
```

`action.fetch.weather` をつくります。ここで Yahoo Weather API を呼び出すので少し長くなります。

```js:/src/js/action/action.fetch.weather.js
import actionFetch from './action.fetch'
import obseriot from 'obseriot'
import qs from 'qs'

const endpoint = 'https://query.yahooapis.com/v1/public/yql'

actionFetch.weather = {
  handler: {
    name: 'action_fetch_weather',
    action: ( text = '' ) => {

      let yql = `select * from weather.forecast where woeid in (select woeid from geo.places(1) where text="${ text }")`,
          query = {
            q: yql,
            format: 'json',
            env: 'store://datatables.org/alltableswithkeys'
          },
          queryString = qs.stringify( query )

      fetch( `${ endpoint }?${ queryString }` )
      .then( res => res.json() )
      .then( json => obseriot.notify( actionFetch.weather.done, json ) )
      .catch( e => obseriot.notify( actionFetch.weather.error, e ) )

      return query
    }
  }
}

export default actionFetch.weather
```

`handler.action` の中で `fetch` を使って Yahoo Weather API を呼び出しています。

`fetch` が終わると今度は `actionFetch.weather.done` というアクションを発行しています。`actionFetch.weather` は自分自身なので、対外的には `action.fetch.weather.done` というアクションを発行していることになります。

`action.fetch.weather.done` には Yahoo Weather API の結果をまるごと渡します。

`fetch` でエラーがあったら `actionFetch.weather.error` というアクションを発行しています。対外的には `action.fetch.weather.error` というアクションを発行していることになります。

ではまず `action.fetch.weather.done` を書きます。

Yahoo Weather API のデータのうち、実際に使いたい箇所だけを `return` しています。

```js:/src/js/action/action.fetch.weather.done.js
import actionFetchWeather from './action.fetch.weather'

actionFetchWeather.done = {
  handler: {
    name: 'action_fetch_weather_done',
    action: json => json.query.results
  }
}
```

次に `action.fetch.weather.error` です。

```js:/src/js/action/action.fetch.weather.error.js
import actionFetchWeather from './action.fetch.weather'

actionFetchWeather.error = {
  handler: {
    name: 'action_fetch_weather_error',
    action: error => error
  }
}
```

エラーをそのまま返すだけです。

これでアクションが完成しました。

ファイルを小分けしているので難しそうに見えるかもしれませんが、実際にやっていることは **action と名付けた空オブジェクト `{}` の拡張** です。

## ストアを拡張

続いてストアですが、ストアは `store.weather` に天気情報を保存したいだけなのでアクションより少ない数で足ります。

まずは `/src/lib/store.js` までのパスを書くのが大変なのでショートカットするためのファイルから。

```js:/src/js/store/store.js
import store from '../../lib/store'
export default store
```

では `store.weather` です。

```js:/src/js/store/store.weather.js
import store from './store'
import action from '../../lib/action'
import obseriot from 'obseriot'

store.weather = {
  state: {},
  handler: {
    name: 'store_weather',
    action: () => store.weather.state
  }
}

obseriot.listen( action.fetch.weather.done, results => {
  store.weather.state = results.channel
  obseriot.notify( store.weather )
} )
```

先ほど作った `action.fetch.weather.done` をストアが `listen` します。Yahoo Weather API の結果が `results`  として渡ってくるので、そのなかで天気情報として必要な箇所だけを `store.weather.state` に格納します。

`obseriot.notify( store.weather )` を実行することで、`obseriot.listen( store.weather )` しているカスタムタグが最新の天気情報を取得できるようになります。

もちろん `store.weather.state` に格納された時点で、`store.weather.state` を直接参照すれば天気情報を見ることもできます。

## カスタムタグ

アクションとストアが完成したので、アクションを `notify` したり、ストアを `listen` するカスタムタグをつくっていきます。

カスタムタグは `/src/tag` に追加していきます。

まずは `app.tag` です。`index.html` に書かれた `<app></app>` にマウントされて、アプリケーションに必要なカスタムタグをマウントします。

```html:src/tag/app.tag
<app>

  <input-city></input-city>
  <results-weather></results-weather>

</app>
```

`input-city` に天気情報を知りたい都市名を入力すると、`results-weather` に天気情報が表示される感じです。

では入力を担う `input-city` を書きます。

```html:src/tag/input-city.tag
<input-city>

  <input type="text" onchange="{ search }" placeholder="city...">

  <script>
    import action from '../lib/action'
    import obseriot from 'obseriot'

    search ( e ) {
      let city = e.target.value
      obseriot.notify( action.fetch.weather, city )
    }
  </script>

  <style>
    input {
      width: 100%;
      padding: 0.5rem;
      font-size: 1rem;
      box-sizing: border-box;
    }
  </style>

</input-city>
````

`input` 要素の `change` イベントから `action.fetch.weather` を `notify` しています。

次に結果を表示する `results-weather` です。

```html:src/tag/results-weather.tag
<results-weather>

  <div if="{ weather }">
    <h1>{ weather.location.city }</h1>
    <ul>
      <li each="{ weather.item.forecast }">{ date } ( { day } ): { text }</li>
    </ul>
  </div>

  <script>
    import store from '../lib/store'
    import obseriot from 'obseriot'

    const weatherListener = state => this.update( { weather: state } )

    this.weather = false

    obseriot.listen( store.weather, weatherListener )

    this.on( 'unmount', () => obseriot.remove( store.weather, weatherListener ) )
  </script>

</results-weather>
```

Yahoo Weather API が返ってきて `store.weather` が更新されたことを `listen` して、`this.update( { weather: state } )` で UI を書き換えています。

カスタムタグがアンマウントされてもリスナーは動き続けているので、アンマウントされたらリスナーを止めています。`unmount` で `obseriot.remove` しているところですね。

## アプリケーションのエントリーポイント

アクション、ストア、カスタムタグも完成しました。

あとはアプリケーションを HTML に表示するだけです。Riot のカスタムタグを `mount` するだけで終わりです。

```js:/src/js/main.js
import riot from 'riot'

riot.mount( 'app' )
```

## ビルド

ビルドについては詳しく触れませんが Rollup をつかってビルドします。

少し変わったところといえば、ビルド用のエントリーポイントを設けずに `rollup-plugin-multi-entry` を使ってビルド対象を拾ってくるところです。アクションやストアの数が多くなりがちなので採用しました。

```js:config/rollup.config.js
import riot from 'rollup-plugin-riot'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import buble from 'rollup-plugin-buble'
import multiEntry from 'rollup-plugin-multi-entry'

export default {
  entry: [ 'src/js/**/*.js', 'src/tag/**/*.tag', 'src/main.js' ],
  dest: 'dist/bundle.js',
  format: 'iife',
  plugins: [
    riot(),
    nodeResolve( { jsnext: true } ),
    multiEntry( { exports: false } ),
    commonjs(),
    buble()
  ]
}
```

`rollup -c config/rollup.config.js` を CLI で実行すればビルドされます。( プロジェクトでは `npm run build` で同様にビルドしますが、今回は `package.json` の説明を省略します )

# 完成

以上、すごくざっくりな説明だったので分かりづらかったかもしれません。

[GitHub](https://github.com/aggre/try-riot-with-obseriot) を `git clone` して試してみてください。

以下のようにすると、`localhost:9000` で見られるようになります。

```bash
git clone git@github.com:aggre/try-riot-with-obseriot.git
cd try-riot-with-obseriot
npm install
npm run build
npm start
```

もしくは、Now を使ってデプロイしてもいいと思います。Now でデプロイするならこうです。

```bash
sudo npm install -g now
git clone git@github.com:aggre/try-riot-with-obseriot.git
cd try-riot-with-obseriot
now
```

Now については [こちらの記事](http://qiita.com/aggre/items/f0cb9f8b8e8c54768e50) にまとめているので、興味のある方はご覧ください。

# Riot と Obseriot と Flux

Riot はそのシンプルさが魅力です。

Obseriot は Riot の魅力を損ねないようにシンプルにしたつもりですが、Flux を実装するとなるとまだ冗長なところがあります。

そこで Flux に特化した別のライブラリ [Businessman](https://www.npmjs.com/package/businessman) を開発しています。

2017/04/23 追記: もともと Obseriot の Flux 特化版として始めたプロジェクトでしたが、Worker API を使った Flux 実装にピボットしました。

明日の [Riot Advent Calendar](http://qiita.com/advent-calendar/2016/riotjs) は [@clown0082](http://qiita.com/clown0082) さんによるテストの話です！