Riot.js Advent Calendar 2016 の2日目!
1日目はkuwahara_jsngさんの「今年1年真剣にRiot.jsと付き合ってきたので振り返ってみた」です。
さて、ついにv3がリリースされたRiot.jsですが、実は以前、社内向けに作ったハンズオンがあるので、Riot.js Advent Calendar 2016の場をお借りして紹介させていただきたいと思います。
ただ、バージョンは2.6時点のものですのでご了承下さい。
v3版も、いずれ近いうちに!!!
デモ
http://omiend.github.io/riotjs_todo/
はじめに
Riot.jsとは
関連するHTMLとJavaScriptを結合し、再利用可能なコンポーネントとしてまとめたカスタムタグを利用してアプリケーションを構築することができる、超軽量Webフレームワークです。
カスタム・タグ
Viewと対になるScriptを、一つのタグに定義したものです。
HTML
└── Custom Tag
├── Custom Tag
│ ├── HTML
│ └── Script
├── HTML
└── Script
コンパイラ
インブラウザ・コンパイルとプリコンパイルから選べます。 このハンズオンでは環境構築を簡素化するために、表示時にコンパイルされるインブラウザ・コンパイルを採用しています。
http://riotjs.com/ja/api/compiler/
私は普段、Webpackでプリコンパイル(&トランスパイル)させて、BundleされたJSを読み込むようにしています。
その際に利用する webpack.config.js 例です。
var path = require('path');
var webpack = require('webpack');
module.exports = {
entry: {
index: path.join(__dirname, 'src/app.js')
},
output: {
path: path.join(__dirname, 'public/scripts'),
filename: 'app.js',
library: 'Router'
},
plugins: [
new webpack.ProvidePlugin({
riot: 'riot'
})
],
module: {
loaders: [
{
test: /\.tag$/,
exclude: /node_modules/,
loader: 'riotjs-loader'
},
{
test: /\.json$/,
exclude: /node_module]s/,
loader: 'json-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
}
};
ちなみに、 package.json はこんな感じ。
{
"name": "riot_twitter",
"version": "1.0.0",
"description": "",
"dependencies": {},
"devDependencies": {
"babel": "^6.5.2",
"babel-core": "^6.13.2",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.13.2",
"json-loader": "^0.5.4",
"riot": "^2.5.0",
"riot-router": "^0.8.0",
"riotjs-loader": "^3.0.0",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
},
"scripts": {
"watch": "webpack --progress --color --watch"
},
"author": "omiend"
}
ルーティング
ルーティングにはriot.routeを利用します。
下記アクションでURLが変更されると、与えられたcallback関数を実行します。
- 1.新しいURLが、ブラウザのロケーションバーに入力されたとき
- 2.ブラウザの戻る/進むボタンが押されたとき
- 3.route(to)が呼び出されたとき
- 4.アンカータグがクリックされたとき
当TODOアプリにおいては、URLの変更を検知→URLの状態を受け取り、callback関数にてカスタム・タグをマウントさせています。
// 例えば、‘todo/1000/edit’にアクセスすると // collection=‘todo’; id=‘1000’; action=’edit’がバインドされる riot.route(function(collection, id, action){ riot.mount('route', collection || 'home', {id: id}) })
参考までに、私は普段riot-routerを利用しています。
こちらも気になっています。
ハンズオン
注意点
独学なので、Riot.jsのお作法に則っていない可能性があります(お作法がアレば、の話ですが)
Global変数をそのまま触っており(良くない)、JavaScriptについては割りと乱暴にお手軽に実装しています。
※observableを利用して、fluxを取り入れるべきだなぁとは感じています。
また、ローカル環境においては、カスタムタグを外部ファイルとして読み込むため
<script type="riot/tag" src="tags/home.tag"></script>
Google Chromeで起動すると下記のエラーが発生し、正常に動作しない可能性があります。
その際は、Google Chrome起動時に--allow-file-access-from-filesを引数を指定して起動するか、Firefoxなどの別のブラウザでお試し下さい。
riot+compiler.min.js:2 XMLHttpRequest cannot load file://hoge-hoge/riotjs_todo/tags/home.tag. Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https, chrome-extension-resource.
Python(MacOSならば標準搭載)を利用し、SimpleServerを利用する手もあります。
ローカル環境で動かしましょう
Zipダウンロード → 解凍
git clone git@github.com:omiend/riotjs_todo.git でもどうぞ
- index.htmlをブラウザで開く
構成
.
├── css
│ ├── main.css // ページの整形用に
│ ├── normalize.css // https://necolas.github.io/normalize.css/
│ └── skeleton.css // わたくしomiendが好きな、軽量CSSフレームワークです( http://getskeleton.com/)
├── images
│ └── riotjs.png // riotをイメージした画像
├── index.html // ブラウザが一番初めに読み込む単純なHTMLファイルです
├── scripts
│ └── app.js // ルーティングと、予め登録されたデータを格納しています
└── tags // カスタム・タグを配置しているディレクトリです
├── add.tag // TODOデータの追加を行うカスタム・タグです
├── commands.tag // TODOデータに対して何らかの処理を行うコマンドを集めたカスタム・タグです
├── edit.tag // TODOデータを更新するためのフォームが定義されたカスタム・タグです
├── home.tag // ホーム画面を表示するための簡単なHTMLが定義されたカスタム・タグです
├── navi.tag // ナビバーの単純なHTMLが定義されたカスタム・タグです
└── todo.tag // TODOデータを一覧したりなど、TODOアプリの基盤画面を定義したカスタム・タグです
index.html
ブラウザで一番初めに読み込むファイルです。
内容は単純なHTMLですが、下記の要素が定義されています。
<!-- カスタムタグがマウントされるセレクター 名称はrouteでなくても良いです --> <route></route> <!-- Riot.js本体と、コンパイラをCDNで読み込み --> <script src="https://cdn.jsdelivr.net/riot/2.6/riot+compiler.min.js"></script> <!-- ルーティングなどの読み込み --> <script src="scripts/app.js" defer></script>
tag/navi.tag
こちらは本当に単純なHTMLです。
コメントアウトを解除し、画面にナビバーが表示されることを確認してください。 正しく表示されていれば、naviタグが正しくマウントされているということです。
tag/home.tag
ルーティング設定によって、「/」にアクセスされた時に
同タグ内のScriptにて‘Riot.js’という値を返す関数「echo()」が定義されています。
「
I Love {this.echo()} !
」にて当該関数を実行し、 ‘Riot.js’を受け取ってレンダリングしています。単純な例ですが、カスタム・タグの基本がわかると思います。
tag/todo.tag
TODOアプリケーションの基盤的なカスタム・タグです。
まず、riotが提供するeachによってTODOデータをループし、表示しています。
<div class="row" each={todo in store.todos}>
一行ごとにcommandsタグをマウントさせています。
commandsタグはtodoという名前を引数として、ループ中のtodoを受け取っています。
<commands todo={todo}></commands>
tag/commands.tag
todoタグから受け取ったTODOデータは、optsというriotが提供する特別な変数に格納されます。
optsは、Reactで言う所のpropsのイメージです。
<div if={opts.todo.done}><!-- doneがtrueだったらレンダリング -->
commandsタグは、TODOデータに対してアクションを行うボタンを提供するために実装したカスタム・タグです。
editTagを
一つのカスタム・タグが、各行で各TODOデータのみを利用する動きとなっており、コンポーネントの再利用を意識した作りとしています。
tag/add.tag
addタグがマウントされる
子タグは親タグ(todoタグ)がマウントされた後に初期化されます。
TODO内容を入力し、ADDボタンがクリックされると、addタグに定義されたadd関数が実行され、storeに入力内容を保存します。
実はその後の「 store.trigger('refresh') 」が大切な処理です。
sroreはapp.jsにてobservableのオブジェクトとしており、add実行時にtriggerを実行することによって refresh イベントを発火させています。
riot.observable(store)
storeに対する refresh イベントが発火されると、todoタグに定義された下記のcallback関数が実行され、todoタグが自身を更新し、TODO一覧に追加したTODOデータが表示されるという仕組みになっています。
var self = this // callbackの中でtodoタグを参照するため store.on('refresh', function(){ self.update() // todoタグを更新 })
オブザーバブル(Observable)について http://riotjs.com/ja/api/observable/
tag/edit.tag
これまでの説明以上の目新しいことはしていません。
コメントアウトを解除して、更新画面が表示されることを確認してください。
commandsタグからリクエストパラメータでeditタグを呼び出し・・・
edit() { riot.route(`edit/${opts.todo.id}`) }
riot.route(function(collection, id, action){ // collection = edit // id = commandsタグの${opts.todo.id}で取得されたTODOデータのID riot.mount('route', collection || 'home', {id: id}) })
TODOデータのIDを opts を利用して受け取っています。
opts.form = store.todos.filter(function(s){return s.id == opts.id})[0]