Riot はミニマルで Web Components のような UI ライブラリ

  • 757
    Like
  • 0
    Comment

古典的な構成のサービスを AWS Lambda + S3 で動作するサーバーレスアーキテクチャで再構築し、そのフロントエンドに Riot を採用しました。

プロジェクトは WWD JAPAN.com として公開しています。

React や Angular などに代表される JavaScript の UI ライブラリのうち、Riot はミニマルな API と HTML 標準に近い文法を採用しているのが特徴です。

Riot はコンポーネントベースの UI 開発から複雑さを取り除き、楽しさを与えます。

TL;DR

Riot はこれまでの UI ライブラリと比べて以下の点で異なります。

  • 必要最小限の API
  • 少ないボイラープレート
  • Web Components ( HTML Template ) に似た文法

React のコードと比較してみます。

ToDo アプリケーションを React で書くとこのようにできます。

import React from 'react';
import ReactDOM from 'react-dom';

class TodoApp extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = {items: [], text: ''};
  }

  render() {
    return (
      <div>
        <h3>TODO</h3>
        <TodoList items={this.state.items} />
        <form onSubmit={this.handleSubmit}>
          <input onChange={this.handleChange} value={this.state.text} />
          <button>{'Add #' + (this.state.items.length + 1)}</button>
        </form>
      </div>
    );
  }

  handleChange(e) {
    this.setState({text: e.target.value});
  }

  handleSubmit(e) {
    e.preventDefault();
    var newItem = {
      text: this.state.text,
      id: Date.now()
    };
    this.setState((prevState) => ({
      items: prevState.items.concat(newItem),
      text: ''
    }));
  }
}

class TodoList extends React.Component {
  render() {
    return (
      <ul>
        {this.props.items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    );
  }
}

ReactDOM.render(<TodoApp />, mountNode);

続いて Riot の文法。

tag
<todo>
  <h3>TODO</h3>

  <ul>
    <li each={ item, i in items }>{ item }</li>
  </ul>

  <form onsubmit={ handleSubmit }>
    <input ref="input">
    <button>Add #{ items.length + 1 }</button>
  </form>

  <script>
    this.items = []

    handleSubmit(e) {
      e.preventDefault()
      var input = this.refs.input
      this.items.push(input.value)
      input.value = ''
    }
  </script>
</todo>
html
<todo></todo>

<script>riot.mount('todo')</script>

React より少ないボイラープレート、少ないメソッドで同じことを実現しています。

Polymer のようにも見えますが、Riot はそれよりも少ないコードです。

すっきりしたシンプルなコードで、テンプレートとロジックを分けることができました。

すべてはカスタムタグの集合

Riot は独自のタグのことをカスタムタグと呼んでいます。Web Components におけるカスタムエレメントに似ています。

例えば、指定した日の天気を表示する <weather> というカスタムタグを作って、それを使いたいとします。HTML には以下のように記述しておきます。

<weather date="2016/01/01" state="晴れ"></weather>

カスタムタグを定義するファイルの中身を以下のようにします。

weather.html
<weather>

    <p>{ date }の天気は{ weather }です。</p>

    <script>

        this.date = opts.date
        this.weather = opts.state

    </script>

    <style>

        :scope {
            display: block
        }

    </style>

</weather>

表示はこうなります。

2016/01/01の天気は晴れです。

ファイル名を weather.html としていますが、weather.js でも、それ以外の好きな名前+拡張子でも構いません。Riot はカスタムタグとして読み込まれたファイルを自動的にコンパイルしてくれます。それもブラウザ側で。( もちろんプリコンパイルしておくこともできます )

カスタムタグの属性を opts オブジェクトによって受け渡していて、この中身は文字列でもオブジェクトでもなんでも構いません。カスタムタグで利用したい値を自由に渡せます。

カスタムタグの記述は、<weather></weather> のあいだに、実際に表示を行う HTML と、カスタムタグの仕様を実現するためのスクリプトとスタイルを同梱します。

カスタムタグを実現するために必要なものすべてが、ひとつのファイルに存在 します。

実際に表示を行うタグは標準のタグでも良いですし、また他のカスタムタグも使えます。

読みやすくて標準に近い文法 にも関わらず、カスタムタグというコンポーネント同士を組み合わせることで どのようなアプリケーションも実現できる 強力なパターンです。

公式ドキュメント では、より実用的でもっと踏み込んだカスタムタグが例示されています。

Web Components に似ている

Riot のカスタムタグは HTML の新しい標準仕様として策定が進んでいる Web Components ( 厳密にはその中の HTML Templates )に似ています。

Riot は Web Components に似た仕様を Web Components よりも少ないコードで実現できます。

Riot 自体が最終的に Web Components に辿り着くべきであるという方針のため、Web Components が広範にサポートされる世界では、Riot は実際の Web Components をより簡単に開発できるように手助けしてくれるかも知れません。

ちなみに、Web Components では独自のタグの命名を my-component のようにハイフンで区切ることが規定されています。来る日に向けて、さきほど例示した weather タグも my-weather のように名前を変えておきましょう。

標準どおりで覚えることが少ない

ライブラリ独自のやりかたを覚えるために四苦八苦するのは終わりです。

例えば Vue では、要素のクリックイベントに応じてメソッドを実行したいときはこのようにします。

html
<div id="example">
    <button v-on:click="methodName">Click</button>
</div>
js
var vm = new Vue( {
    el: '#example',
    methods: {
        methodName: function ( event ) {
            console.log( event )
        }
    }
} )

一方の Riot ではこうです。

tag
<example>
    <button onclick='{ methodName }'>Click</button>
    <script>
        methodName ( event ) {
            console.log( event )
        }
    </script>
</example>

分かりやすい onclick です。

実際には Riot がカスタムタグをコンパイルする時点で onclick も影響のない形で解釈するので、なにも心配せずに onclick を使用できます。

ほかにもあります。

例えば Vue には transition 属性を使うことで CSS トランジションを使うことができますが、Riot の場合はそもそもこの API はありません。

インタラクションに応じて CSSトランジションするなら、onclick などのイベントに応じて呼び出されるメソッドからクラスや style 属性を変更するという ごく標準的なやりかた で対応できます。

条件属性やループなど標準にはない API も提供していますが、どれもごくシンプルで必要最小限の API だけを提供します。公式ドキュメント に書かれている分かりやすくて数少ない API が、Riot のすべてです。

先ほどの weather タグも、カスタムタグのなかで 1 以下のようにすれば簡単にループできます。

<ul>
    <li each="{ weather in historicalWeather }">
        <weather date="{ weather.date }" state="{ weather.state }"></weather>
    </li>
</ul>

<script>
    this.historicalWeather = [
        { date : '2016/01/01', state : '晴れ' },
        { date : '2016/01/02', state : '曇り' },
        { date : '2016/01/03', state : '雨' }
    ]
</script>

表示はもちろんこうなります。

2016/01/01の天気は晴れです。
2016/01/02の天気は曇りです。
2016/01/03の天気は雨です。

アプリケーションを作るのに十分な機能

カスタムタグだけでもそれなりのものは出来上がりますが、ひとつのアプリケーションとして使う場合に必要なものはまだあります。いずれも Riot はシンプルで分かりやすい API を提供してくれます。

ルーティング

URL を変更したり、変更を検知してコールバックすることができます。

// コールバックを定義
riot.route( ( pri, sec, ter ) => {
    console.log( 'URL -> ', pri, sec, ter )
} )

// URLを変更する
riot.route( '/shop/1/detail' )

// URL -> 'shop', '1', 'detail'

ほかにも、riot.route( '/shop/*', ... ) のようにして特定のパスのときのみ実行したい処理を書くこともできます。詳細は
公式ドキュメント にあります。

オブザーバブル

イベントの監視、トリガーができます。

// イベントインスタンスをつくる
const e = new function () {
    riot.observable( this )
}

// start イベントを監視
e.on( 'start', e => {
    console.log( 'EVENT -> ', e )
} )

// start イベントをトリガーし、リスナーにオブジェクトを渡す
e.trigger( 'start', { item: 1 } )

// EVENT -> , { item: 1 }

もちろん off()one() などの操作もできます。詳細は 公式ドキュメントにあります。

私はこのオブザーバブルを管理しやすくしたライブラリ Obseriot を作りました。使い方によっては簡易 Flux にもなります。

好きなように使える

Riot は標準から乖離した機能や独自路線の機能を提供しないため、他のさまざまなライブラリと同時に使うことができます。

Flux と組み合わせたり、jQuery などの昔からあるライブラリも使えます。

プリプロセッサ

Riot は独自のコンパイラによってカスタムタグを js に変換します。このときカスタムタグの Script, HTML, CSS すべてにおいて好みのプリプロセッサを使用できます。

Riot の気に入った部分だけ使いつつ、Script を好みの言語や仕様にするということもできるのです。

プリプロセッサについては 公式ドキュメント が詳しいです。

いま必要なもの

大きなライブラリには大きなエコシステムが出来上がります。

大きなライブラリとその周辺のエコシステムに従って大規模なものを開発しても、数年後にはまた新しくて合理的なライブラリが注目されているかも知れません。

とくにフロントエンドは変化が速く、標準がアップデートされる限りその流れが落ち着くとも思えません。直近では Web Components が転換になるでしょう。

新しいライブラリを良しとするわけではありませんが、そのときの技術的背景に合わせられてユーザーに最適なサービスを提供できるライブラリが存在するなら、それはやはり魅力的です。

このように数年後どうなるか分からない状況のなか求めているのは、小さくて標準に近いライブラリです。

小さくて標準に近いライブラリはライブラリ自体のメンテナンスも容易になり、変化にも合わせやすくなります。他のエコシステムの一部として使うことも可能です。仮にライブラリを変える決断をしたとしても、大きなライブラリ/エコシステムに依存した開発環境を切り替えるのよりはずっと簡単に済むはずです。

開発者コミュニティ

日本人開発者のためのコミュニティとして、Facebook と Slack が開設されています。

もちろん公式フォーラムもあります。

Riot 公式フォーラム


余談

私はどう使っているか

カスタムタグのほか独自に用意したのは、いくつかの mixin というタグの拡張と、Obseriot を使った Flux 実装です。

カスタムタグは画面自体を担う“スクリーンタグ”から、文字列を表示する程度な単機能を担う“モジュールタグ”まで複数のレベルに分けて設計しています。

すべてのコードは新しい ES で書いて Rollup でビルド+バンドルしています。カスタムタグのプリコンパイルもこのときに走ります。

ちなみにこのプロジェクトに興味ある開発者は...

私と一緒にプロジェクトをやりたいという稀有な方がおられましたら私までメール( プロフィール欄参照 )ください :smile:


  1. ループは Riot の API のため、HTML の中に同じように書くことはできません。なんらかのカスタムタグの中で使うことになります。