Help us understand the problem. What is going on with this article?

Riot + Obseriot で優しく Flux する

More than 1 year has passed since last update.

Riot Advent Calendar の 17 日目です。

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

TL;DR

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

詳しくは こちらの記事 にまとめています。

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

チュートリアルで作成するプロジェクトは GitHub で公開しています。

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

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

書いていきます

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

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

Yahoo Weather API

index.html

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

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

app は今回のサンプルでそうしているだけで、名前は整合性さえとれていればなんでも構いません。bundle.js はまだ作っていないので、この 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 に以下ファイルを追加します。

/src/lib/action.js
export default {}
/src/lib/store.js
export default {}

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

アクションを拡張

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

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

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

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

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

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

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

action.fetch = {}

export default action.fetch

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

/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 しています。

/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 です。

/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 までのパスを書くのが大変なのでショートカットするためのファイルから。

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

では store.weather です。

/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> にマウントされて、アプリケーションに必要なカスタムタグをマウントします。

src/tag/app.tag
<app>

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

</app>

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

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

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.weathernotify しています。

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

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 を書き換えています。

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

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

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

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

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

riot.mount( 'app' )

ビルド

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

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

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 の説明を省略します )

完成

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

GitHubgit clone して試してみてください。

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

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 でデプロイするならこうです。

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

Now については こちらの記事 にまとめているので、興味のある方はご覧ください。

Riot と Obseriot と Flux

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

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

そこで Flux に特化した別のライブラリ Businessman を開発しています。

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

明日の Riot Advent Calendar@clown0082 さんによるテストの話です!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away