Riot.jsでフロントエンドの複雑さに反乱するときがやってきた

  • 727
    いいね
  • 0
    コメント

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

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

私は実際に開発を続けながら Riot で正しかったと確信しました。

それは Riot( 暴動 )の名の通り、今までの「複雑さ」に対して「反旗を翻す」ほどのインパクトです。

TL;DR

Riot はこれまでの仮想 DOM を利用したライブラリと比べて以下の点で異なります。

  • 標準に近い文法
  • 必要最小限の機能
  • Web Components ライク

公式に React のコードと比較されているので、それを引用します。

まずは React の文法で ToDo アプリを。

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

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

    render() {
        const {items, text} = this.state;
        return (
            <div>
            <h3>TODO</h3>
            <ul>
            <li>{items.map((item, i)=> <li key={i}>{item}</li>)}</li>
            </ul>
            <form onSubmit={this._onSubmit}>
            <input onChange={this._onChange} value={text}/>
            <button>Add #{items.length + 1}</button>
            </form>
            </div>
        );
    }

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

    _onSubmit(e) {
        e.preventDefault();
        const {items, text} = this.state;
        this.setState({
            items: items.concat(text),
            text: ''
        });
    }
}

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

続いて Riot の文法。

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

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

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

    this.items = []

    handleSubmit(e) {
        var input = e.target[0]
        this.items.push(input.value)
        input.value = ''
    }
</todo>
html
<todo></todo>
<script>riot.mount('todo')</script>

随分と見慣れた形ではないでしょうか。

テンプレート変数を波括弧で囲むこと以外の独自路線は一切ありません。

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

では始めましょう。

複雑さの代償

すべての DOM をサーバーサイドから出力していた時代は今やバックミラーの向こうにあり、強大なパワーを手に入れた JavaScript によって「複雑」にプログラミングされた仮想 DOM がスクリーンを制する時代になりました。

その複雑さはウェブの急進化や開発者自身の需要に合わせて生まれてきたものであり、多くの開発者にとって必要な複雑さでした。

しかし、ふと不安がよぎります。設計はこれでいいのか?もっと標準的なやりかたはないか?プロジェクトに新しい開発者が来たら、テストコードを見るだけで仕様を理解できるか?悩みは尽きません。

複雑さの代償です。

Riot は不要な複雑さにまみれた現在の潮流に「反旗」を翻すプロジェクトです。1

すべてはタグの集合になる

OS のテキストエディタで書いた HTML でホームページを作っていた時代を覚えていますか?そのころ、HTML には <table><img> といった「タグ」と、わずかなスクリプトしかありませんでした。

Riot はそのようなシンプルで馴染み深い HTML に非常に似通った文法で、モダンな仮想 DOMによる UI 開発を実現 2 します。

Riot を使った開発は「タグ」を作ることに始まり、「タグ」を作ることに終わるといっても過言ではありません。すべての画面は独自のタグや標準のタグの集合として定義されます。

Riot は独自のタグのことをカスタムタグと呼んでいます。

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

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

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

weather.tag
<weather>

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

    <script>

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

    </script>

    <style scoped>

        :scope {
            display: block
        }

    </style>

</weather>

表示はこうなります。

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

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

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

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

カスタムタグを実現するために必要なものすべてが、ひとつのファイルに存在します。しかもその文法は標準に忠実な、じつに直感的なものになっています。

このうち必須なのは実際に表示を行うタグだけであって、スクリプトやスタイルを書くのは、それが必要なときだけです。

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

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

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

Web Components に似ている

すでに気づいた方も多いと思いますが、Riot のカスタムタグは HTML の新しい標準仕様として策定が進んでいる Web Components ( 厳密にはその中の Custom Elements の HTML Templates )に似ています。

Web Components はタグそのものを自由に開発できるようにすることで、さまざまな問題を解決します。厄介な依存関係や複雑なオプションから開発者を解放してくれます。必要なスクリプトやスタイルは Web Components の中で完結しているため、タグを使う側は基本的に要素を追加するだけです。

この考え方はまさに Riot のカスタムタグと同じですし、実際に文法も似ています。Riot は標準化される仕様に似た、標準に近い文法です。

そもそも Riot 自体が、仮想 DOM を利用せずに最終的に Web Components に辿り着くべきであるという方針です。Web Components が広範にサポートされるその日が来たら、Riot は Web Components をより簡単に使えるようにしてくれるかも知れません。

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

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

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

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

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 です。それはずっと昔から標準として存在していた仕様です。

古臭く見えますか?そんな美学よりも、スクリプトと HTML を同じモジュールにおくことの方がより重要です。3

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

ほかにもあります。

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

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

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

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

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

<script>
    this.weather = [
        { 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 を変更したり、変更を検知してコールバックすることができます。URL の変更は pushState もハッシュも使えます。( とは言えとくに事情がない限り pushState を使うべきだと思います )

// ベースパスをデフォルトの ”#” から変えて pushState を使う
riot.route.base( '/' )

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

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

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

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

オブザーバブル

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

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

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

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

// EVENT -> , { item : 1 }

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

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

これらの API さえあれば URL の変更によってイベントをトリガーして、カスタムタグをコントロールすることもできます。

好きなように使える

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

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

いま必要なもの

ある大きなライブラリで大規模なものを開発しても、数年後にはまた新しくて合理的なライブラリが注目されているかも知れません。

とくにフロントエンドは変化が速く、標準がアップデートされる限りその流れが落ち着くとも思えません。

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

このように数年後どうなるか分からない状況で、大きなライブラリにコミットするのは危険だと考えています。そこで求めているのは小さくて標準に近いライブラリです。

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

複雑さにつばをかけろ

シンプルに保つことを開発者は求め続けているはずです。

フロントエンド開発者は大きなライブラリがのちに疲弊してくることを、jQuery で経験しています。

だから私の今の選択は Riot です。

開発者コミュニティ

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

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

Riot 公式フォーラム


余談

私はどう使っているか

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

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

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

これらの詳細はまたの機会にどこかで書こうと思ってます。

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

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


  1. 公式ドキュメントより > Riotは、ボイラープレート(=雛形、決まり文句)と、不要な複雑さにまみれた現在の潮流に「反旗」を翻すプロジェクトです。クライアントサイドのライブラリでは、小さくパワフルなAPIと分かりやすい文法が、何にも増して重要だと考えています。 

  2. Riot は厳密には仮想 DOM ではありませんが、仮想 DOM に期待される データの差分だけを更新 する仕様を独自に実装しています。 

  3. やはりこれも公式ドキュメントより 

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