はじめてのAurelia

http://aurelia.io

Aureliaとは次世代UIフレームワーク。AngularJS、Reactに比べて影が薄い。どうなっていくんだろうか?チュートリアルをやってみた感想としては、HTTPリクエストとルーティング周りがクリーンで素敵。

以下、チュートリアルの適当な和訳。

Getting Set Up

キットをここからダウンロードします。

  • NodeJS - キットのフォルダー内で簡単なサーバーを立ち上げるには、http-servernpm install -g http-serverでインストールします。インストールが終了したら、http-server -o -c-1コマンドでサーバーを立ち上げます。

The Index.html Page

index.htmlは、Aureliaベースのアプリケーションのテンプレートとなるファイルです。

index.html

<!doctype html>
<html>
  <head>
    <title>Aurelia</title>
    <link rel="stylesheet" href="styles/styles.css" />
    <meta name="viewpoint" content="width=device-width, initial-scalse=1"/>
  </head>
  <body aurelia-app>
    <script src="jspm_packages/system.js"></script>
    <script src="config.js"></script>
    <script>
      SystemJS.import('aurelia-boostrapper');
    </script>
  </body>
</html>

ヘッダーはきわめてシンプルで、スタイルシートといくつかのメタデータを指定しています。

スクリプトタグを見てみます。モジュールローダーであるsystem.jsは、Aureliaのライブラリを読み込んでくれます。次に、config.jsファイルはローダーの設定を記述しています。ツールを使ってAureliaのパッケージをインストールすれば、自動的に生成されます。

モジュールローダーとコンフィグを設定した後に、SystemJS.importを使って、aurelia-boostrapperモジュールを読み込んでいます。

このブートストラッパーが読み込まれると、HTMLドキュメントのaurelia-app要素を探します。この要素によってブーストラッパーはアプリケーションのビューモデルとビューを読み込みます。

Creating Your First Screen

Aureliaでは、UIコンポーネントはビューとビューモデルの2つの部分にわかれます。ビューはHTMLで記述されて、そのDOMに描画します。ビューモデルはES 2016で記述されて、データとその振る舞いをビューに渡します。Aureliaの強力なデータバインディングによって、これら2つの部分はリンクし、データの変更が互いに反映されます。この分離のアイデアは、デベロッパー/デザイナーの協業、メンテのしやすさ、アーキテクチャ上の柔軟性、ソースコントロールにおいても、有用です。

srcフォルダーにapp.htmlapp.jsファイルがありますが、これらがビューとビューモデルのコンポーネントです。ビューモデルをfirstNameとlastNameを持つ、シンプルなクラスに置き換えましょう。fullNameプロパティと、"submit"メソッドも追加します。

app.js

export class App {
  heading = 'Welcome to Aurelia';
  firstName = 'John';
  lastName = 'Doe';

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  submit() {
    alert(`Welcome, ${this.fullName}!`);
  }
}

基本的なデータと振る舞いを記述するビューモデルを作成しました。次にHTMLを使って、ビューを作成しましょう。

app.html

<template>
  <section>
    <h2>${heading}</h2>

    <form submit.trigger="submit()">
      <div>
        <label>First Name</label>
        <input type="text" value.bind="firstName"/>
      </div>
      <div>
        <label>Full Name</label>
        <p>${fullName}</p>
      </div>
      <button type="submit">Submit</button>
    </form>
  </section>
</template>

まず、すべてのビューはtemplateタグにネストされています。ビューは基本的な入力フォームです。入力コントロールを見てみましょう。value.bind="firstName"に気づいたでしょうか?これによって、入力した値とビューモデルのfirstNameプロパティのデータバインディングを行います。ビューモデルのプロパティが変化した時に、入力値は新しい値に更新されます。入力コントロールの値を変更した時、Aureliaはビューモデルに新しい値を入れます。簡単なことですね。

この例では、もっと面白いことが起きています。最後のフォームグループのHTML中に、${fullName}があります。これは文字列の挿入です。ビューモデルからビューへの一方的なデータバインディングで、自動的に文字列へと変換され、ドキュメントに挿入されます。最後に、フォーム要素を見てみましょう。submit.trigger="submit()"に気づくでしょう。これはイベントバインディングで、フォームのsubmitイベントが発火した時に、ビューモデルのsubmitメソッドが呼び出されます。

ブラウザを更新して、確かめてみましょう。

Adding Naviagation

ひとつのページのアプリだと面白くないですよね。スクリーンを幾つか追加して、クライアントサイドのルーティングを設定しましょう。app.jsapp.htmlをそれぞれwelcome.jswelcome.htmlにリネームしましょう。マルチスクリーンアプリの最初のページになります。"layout"、"master page"、"root component"となるapp.jsapp.htmlを新しく作成します。ビューにはナビゲーションUIと現在のスクリーンのコンテントプレースホルダーを、ビューモデルにはルーターのインスタンスを設定しましょう。

app.js

export class App {
  configureRouter(config, router) {
    config.title = 'Aurelia';
    config.map([
      { route: ['', 'welcome'], name: 'welcome', moduleId: './welcome', nav: true, title: 'Welcome' }
    ]);

    this.router = router;
  }
}

ルーターを使うために、configureRouterコールバックを実行するAppクラスをエクスポートします。このコールバックは設定オブジェクトとともに呼びだされます。設定オブジェクトを使用すれば、ドキュメントのタイトルを生成する際に、これを使用し、ルートのマッピングを行います。それぞれのルートは次のプロパティを持ちます:

  • route: パターンに一致したら、このルートにナビゲートします。静的なものだけでなく、customer/:idのようにパラメーターを使用することも出来ます。ワイルドカードパターンとクエリ文字列もサポートしています。routeは文字列パターンやパターンの配列でなければなりません。
  • name: URLを生成するコードで使用する名前です。
  • moduleId: このルートで描画するコンポーネントのパスです。
  • title: ドキュメントのタイトルを任意で指定できます。
  • nav: このルートがナビゲーションモジュールに含まれる場合、trueに設定します。

app.html

<template>
  <require from="bootstrap/css/bootstrap.css"></require>
  <require from="font-awesome/css/font-awesome.css"></require>

  <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
        <span class="sr-only">Toggle Navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">
        <i class="fa fa-home"></i>
        <span>${router.title}</span>
      </a>
    </div>

    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}">
          <a href.bind="row.href">${row.title}</a>
        </li>
      </ul>

      <ul class="nav navbar-nav navbar-right">
        <li class="loader" if.bind="router.isNavigating">
          <i class="fa fa-spinner fa-spin fa-2x"></i>
        </li>
      </ul>
    </div>
  </nav>

  <div class="page-host">
    <router-view></router-view>
  </div>
</template>

Appクラスは上記のapp.htmlのビューにデータバインドされます。このマークアップの大部分はメインのナビゲーション構造を扱います。ビューの上部にrequireエレメントがありますね。ES2015/2016のimportと同じく、AureliaはHTMLのrequireエレメントの使用を可能にします。このエレメントによって、この場合ではCSSを読み込むことが出来ます。これで、ナビゲーション構造のレイアウトにブートストラップを使用することが出来ます。

基本的なデータバインディングと文字列の挿入はすでに見たので、新しいことに注目しましょう。ulエレメントのnavbar-navを見て下さい。lirepeat.for="row of rotuer.navigation"によって、どのようにリピーターを使うかを示しています。これによって、router.navigation配列のそれぞれのアイテムにliが作られます。ローカル変数はrowで、子エレメントで見ることが出来ます。

liでは、クラスを追加/削除するために、どのように文字列挿入を使用しているか示しています。下に行くと、ふたつ目のulがあります。子エレメントのliのバインディングを見て下さい。if.bind="router.isNavigating"これは、条件のもとで、バインディングされた値をベースとしたliを追加/削除します。ルーターはisNavigatingプロパティを更新します。

ブラウザを更新して、確かめてみましょう。"welcome"ルートのために選択されたタブが確認できます。このwelcomeビューはメインコンテントエリアで表示されます。ブラウザのデバグツールを開いて、DOMを見てみましょう。router-viewの中でwelcomeビューコンテントが表示されているのがわかるでしょう。

Adding a Second Page

さて、ナビゲーションアプリケーションができましたが、スクリーンがひとつだと面白く無いので、ふたつ目のスクリーンを追加しましょう。

Githubからユーザー情報を取得して表示しましょう。まずは、仮のスクリーンへのルートを設定します。

app.js

export class App {
  configureRouter(config, router){
    config.title = 'Aurelia';
    config.map([
      { route: ['','welcome'],  name: 'welcome',  moduleId: './welcome',  nav: true, title:'Welcome' },
      { route: 'users',         name: 'users',    moduleId: './users',    nav: true, title:'Github Users' }
    ]);

    this.router = router;
  }
}

users.jsusers.htmlファイルを作成する必要があるとわかるでしょう。これがそのソースです。

users.js

import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import 'fetch';

@inject(HttpClient)
export class Users {
  heading = 'Github Users';
  users = [];

  constructor(http) {
    http.configure(config => {
      config
        .useStandardConfiguration()
        .withBaseUrl('https://api.github.com/');
    });

    this.http = http;
  }

  activate() {
    return this.http.fetch('users')
      .then(response => response.json())
      .then(users => this.users = users);
  }
}

fetch polyfillと同じく、HttpClientをAurelia's Fetchプラグインからインポートします。これによって、HTTPリクエストを簡単に行うことが出来ます。デフォルトではこのプラグインは含まれませんが、スターターキットにはすでに含まれています。

injectデコレーターを見てみましょう。ここでは何を行っているのでしょう?Aureliaはアプリを描画するために必要なUIコンポーネントを作成します。Dependency InjectionコンテナがHttpClientのようなコンストラクタ依存を供給することで可能になります。では、どのようにDIは何を供給すべきかを知るのでしょうか?あなたが行うことといえば、ES2016のinjectデコレータをクラスに追加することだけですが、これで、供給するインスタンスの種類のリストを渡します。それぞれのコンストラクタパラメーターにはひとつの引数を用意します。上記の例では、HttpClientインスタンスが必要なので、injectデコレータにHttpClientタイプを追加し、コンストラクタに対応したパラメーターを渡します。

Aureliaのルーターはルーターの変更時にビューモデルのライフサイクルを実施します。"Screen Activation Lifecycle"や"Navigation Lifecycle"と呼ばれるものです。ルートの内外に対する流れをコントロールするために、ビューモデルは任意でライフサイクルの多様な部分にフックすることが出来ます。ルートがアクティベートする準備ができた時には、ルーターはactivateフックを呼び出します。上記のコードでは、Github APIを呼び出して、ユーザーを取得するためにこのフックを使用しています。activateメソッドで、httpリクエストの結果を返していることに注意して下さい。すべてのHttpClient APIはPromiseを返します。ルーターはPromiseを探知し、それがresolveするまでナビゲーションの完了を待機します。 つまり、この場合では、データが入れられるまで、任意でルーターにページの表示を遅らせることができるのです。

users.html

<template>
  <section>
    <h2>${heading}</h2>
    <div class="row au-stagger">
      <div class="col-sm-6 col-md-3 card-container" repeat.for="user of users">
        <div class="card">
          <canvas class="header-bg" width="250" height="70"></canvas>
          <div class="avatar">
            <img src.bind="user.avatar_url" crossorigin />
          </div>
          <div class="content">
            <p class="name">${user.login}</p>
            <p><a target="_blank" class="btn btn-default" href.bind="user.html_url">Contact</a></p>
          </div>
        </div>
      </div>
    </div>
  </section>
</template>

このスクリーンのビューはきわめてシンプルです。初めて見るものはないでしょう。

要約すると次のようになります。アプリにページを追加するには: 1. app.jsルーターにルートを設定します。 2. ビューモデルを作成します。 3. 同じ名前のビューを作成します。 4. お祭り騒ぎします。

Bonus: Creating a Custom Element

延長戦です。カスタムHTMLエレメントを作成しましょう。"navbar"が適した候補だと思います。たくさんのHTMLがapp.htmlファイルにあります。<nav-bar>エレメントを抽出して、少し叙述的にします。最後には次のように記述することが出来ます。

app.html

<template>
  <require from="bootstrap/css/bootstrap.css"></require>
  <require from="font-awesome/css/font-awesome.css"></require>
  <require from="./nav-bar"></require>

  <navbar router.bind="router"></navbar>

  <div class="page-host">
    <router-view></router-view>
  </div>
</template>

このコードは"nav-bar"のnav-barエレメントを必要とします。ひとたびビューにおいて、このエレメントが利用可能になると、(routerのような)カスタムプロパティへのデータバインディングを含め、その他のエレメントと同じように使用することが出来ます。

まずは、nav-bar.jsnav-bar.htmlを作成しましょう。

nav-bar.js

import {bindable} from 'aurelia-framework';

export class NavBar {
  @bindable router = null;
}

カスタムエレメントを作成するために、クラスを作成し、それをエクスポートします。このクラスはHTMLでエレメントとして使用されるので、どんなプロパティがエレメントの要素として表れるべきかをフレームワークに教える必要があります。そのために、bindableデコレータを使用します。injectと似て、bindableはAureliaにクラスについての情報を提供します。bindableデコレータは、フレームワークに対して、routerプロパティをHTMLの要素として表示して欲しいと伝えます。要素として表示されれば、ビューとバインドすることが出来ます。

nav-bar.html

<template>
  <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
        <span class="sr-only">Toggle Navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">
        <i class="fa fa-home"></i>
        <span>${router.title}</span>
      </a>
    </div>

    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}">
          <a href.bind="row.href">${row.title}</a>
        </li>
      </ul>

      <ul class="nav navbar-nav navbar-right">
        <li class="loader" if.bind="router.isNavigating">
          <i class="fa fa-spinner fa-spin fa-2x"></i>
        </li>
      </ul>
    </div>
  </nav>
</template>

これはもともとのapp.htmlファイルのnavbar HTMLとほとんど同じに見えますね。でも、app.jsにバインディングするのではなく、ここではnav-bar.jsにバインドしています。

このカスタムエレメントはとてもシンプルです...が、シングルrouterプロパティを定義するためにES 2016のクラスを必要とするのは、すこし馬鹿げているように見えます。どうにか取り除けないでしょうか?答えは、YESです。振る舞いのない、しかし、一連のプロパティがバインドされるビューを供給する、とてもシンプルなエレメントでは、省略することが出来るのです。

まずは、nav-bar.jsファイルを削除します。次に、nav-bar.htmlファイルをひとつ変更します。templateエレメントでは、次のようにbindableプロパティを宣言することができます。

nav-bar.html

<template bindable="router">
  ...
</template>

最後に、requireエレメントhtmlコンポーネントを指すので、app.htmlファイルを更新する必要があります。

app.html

<template>
  <require from="bootstrap/css/bootstrap.css"></require>
  <require from="font-awesome/css/font-awesome.css"></require>
  <require from='./nav-bar.html'></require>

  <nav-bar router.bind="router"></nav-bar>

  <div class="page-host">
    <router-view></router-view>
  </div>
</template>

Aureliaがどのようにカスタムエレメントの名前を決めているのか不思議に思うでしょう。慣例では、エクスポートされた、小文字かつハイフンで繋がれたクラス名を使用します。HTMLのみの場合では、ファイル名を使用します。

カスタムエレメントの作成に加えて、既存のエレメントに新しい振る舞いを追加するカスタムアトリビュートを作成することも出来ます。repeatifのように、ビューからDOMを追加したり削除することで、直接テンプレートをコントロールするアトリビュートが必要になることがあるでしょう。Aureliaのパワフルな、かつ拡張可能なテンプレートエンジンでは、これが可能になります。これは秘密ですが...いわゆるAureliaの"ビルトイン"の振る舞いには、実際にビルトインされているものはありません。それらは独自のライブラリに存在し、プラグインとしてAureliaに"インストール"されるのです。私たちは、あなたが自身のアプリとプラグインにビルドしなければならないものと同じコアを使用する"ビルトイン"を提供します。

Bonus: Leveraging Child Routers

独自のルータを持つ三番目のページを追加しましょう。これを子ルーターと呼びます。子ルーターはそれぞれ自身のルーター設定を持ち、親ルーターへナビゲートします。

まずは、app.jsを更新しましょう。

app.js

export class App {
  configureRouter(config, router) {
    config.title = 'Aurelia';
    config.map([
      { route: ['','welcome'],  name: 'welcome',      moduleId: './welcome',      nav: true, title:'Welcome' },
      { route: 'users',         name: 'users',        moduleId: './users',        nav: true, title:'Github Users' },
      { route: 'child-router',  name: 'child-router', moduleId: './child-router', nav: true, title:'Child Router' }
    ]);

    this.router = router;
  }
}

何も新しい物はありません。面白いのはchild-router.jsです。

child-router.js

export class ChildRouter {
  heading = 'Child Router';

  configureRouter(config, router){
    config.map([
      { route: ['','welcome'],  name: 'welcome',       moduleId: './welcome',       nav: true, title:'Welcome' },
      { route: 'users',         name: 'users',         moduleId: './users',         nav: true, title:'Github Users' },
      { route: 'child-router',  name: 'child-router',  moduleId: './child-router',  nav: true, title:'Child Router' }
    ]);

    this.router = router;
  }
}

これ、部分的にはAppの設定と同じですよね。これが、再帰的ルーターです。

ビューを作成して、完成させましょう。

child-router.html

<template>
  <section>
    <h2>${heading}</h2>
    <div>
      <div class="col-md-2">
        <ul class="well nav nav-pills nav-stacked">
          <li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}">
            <a href.bind="row.href">${row.title}</a>
          </li>
        </ul>
      </div>
      <div class="col-md-10" style="padding: 0">
        <router-view></router-view>
      </div>
    </div>
  </section>
</template>

アプリを実行して、魔法を目の当たりにしなさい...そして宇宙が爆発しないように祈りなさい。

Conclusion

Aureliaは素晴らしいアプリケーションを作成出来るだけでなく、そのプロセスを楽しむことも出来ます。私たちはシンプルな慣習によってデザインしました。なので、デベロッパーは、たくさんの設定や、頑固で、制限の多いフレームワークを満たすためのボイラープレートコードを記述に、時間をムダにすることがなくなります。Aureliaを使えば、もう障害物にぶつかることがなくなるでしょう。Aureliaはプラグシステムを使用し、カスタムできるように注意深くデザインされています。