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 は認証不要で使いやすいです。
index.html
まだ Obseriot は関係ありません。
ただの index.html
です。
Roit のカスタムタグをマウントするための app
という要素と、Riot や Obseriot をひとまとめにした bundle.js
を読み込むようになっています。
app
は今回のサンプルでそうしているだけで、名前は整合性さえとれていればなんでも構いません。bundle.js
はまだ作っていないので、この 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
に以下ファイルを追加します。
export default {}
export default {}
Obseriot ではアクションもストアもオブジェクトなのでこれだけです。
アクションを拡張
先ほど作ったアクションにすべてのアクションを書いてもいいのですが、1 アクション毎にファイルが分かれていると見通しがよくなり共同作業もしやすいので、今回はそうします。
action.fetch.weather
というアクションを発行すると、store.wether
というストアが更新される設計にしたいと思います。
では /src/js/action
にファイルを追加していきます。
まずは /src/lib/action.js
までのパスを書くのが大変なのでショートカットするためのファイルから。
import action from '../../lib/action'
export default action
action.fetch
をつくります。呼ばれることはないので空です。もしも今後 action.fetch
を呼びたくなったらこのファイルを編集すればいいですね。
import action from './action'
action.fetch = {}
export default action.fetch
action.fetch.weather
をつくります。ここで Yahoo Weather API を呼び出すので少し長くなります。
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
しています。
import actionFetchWeather from './action.fetch.weather'
actionFetchWeather.done = {
handler: {
name: 'action_fetch_weather_done',
action: json => json.query.results
}
}
次に action.fetch.weather.error
です。
import actionFetchWeather from './action.fetch.weather'
actionFetchWeather.error = {
handler: {
name: 'action_fetch_weather_error',
action: error => error
}
}
エラーをそのまま返すだけです。
これでアクションが完成しました。
ファイルを小分けしているので難しそうに見えるかもしれませんが、実際にやっていることは action と名付けた空オブジェクト {}
の拡張 です。
ストアを拡張
続いてストアですが、ストアは store.weather
に天気情報を保存したいだけなのでアクションより少ない数で足ります。
まずは /src/lib/store.js
までのパスを書くのが大変なのでショートカットするためのファイルから。
import store from '../../lib/store'
export default store
では store.weather
です。
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>
にマウントされて、アプリケーションに必要なカスタムタグをマウントします。
<app>
<input-city></input-city>
<results-weather></results-weather>
</app>
input-city
に天気情報を知りたい都市名を入力すると、results-weather
に天気情報が表示される感じです。
では入力を担う input-city
を書きます。
<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
です。
<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
するだけで終わりです。
import riot from 'riot'
riot.mount( 'app' )
ビルド
ビルドについては詳しく触れませんが Rollup をつかってビルドします。
少し変わったところといえば、ビルド用のエントリーポイントを設けずに rollup-plugin-multi-entry
を使ってビルド対象を拾ってくるところです。アクションやストアの数が多くなりがちなので採用しました。
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 を git 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 さんによるテストの話です!