Mithril 1.1.0

シンプルなアプリケーション

シングルページアプリケーションを構成する主要な要素をカバーするシンプルなアプリケーションを開発してみましょう。

まずはアプリケーションのエントリーポイントを作成してみましょう。index.htmlを作成します。

<!doctype html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Mithrilのアプリケーション</title>
    </head>
    <body>
        <script src="bin/app.js"></script>
    </body>
</html>

<!doctype html>行はHTML5のドキュメントであることを示しています。最初のcharsetメタタグはドキュメントのエンコーディングを指定します。viewportメタタグはモバイルブラウザがどのようにページを拡大して表示するかを指定します。titleタグはブラウザのタブに表示されるアプリケーション名のテキストを含みます。scriptタグはアプリケーションを動かすJavaScriptファイルのパスを指定します。

1つのJavaScriptファイルアプリケーション全体を作成することもできますが、将来、コードベースの中を探索するのが難しくなるでしょう。コードを複数のモジュールに分割し、それらのモジュールからbin/app.jsという名前のバンドル を作成するようにしましょう。

バンドラーツールのセットアップ方法はたくさんありますが、一番使われている方法はnpmでしょう。Mithrilも含めて、現代的なJavaScriptのライブラリとツールはnpm経由で配布されています。npmはNode.jsはNode.js Package Managerの省略形です。npmをインストールする場合は、 Node.jsをインストールしましょう。npmはNode.jsと一緒にインストールされます。Node.jsとnpmがインストールされたら、コマンドラインを開き、次のコマンドを入力します:

npm init -y

npmが正しくインストールされていれば、package.jsonが作成されます。このファイルは、プロジェクトのメタ説明の雛形です。このファイルを編集して、プロジェクト情報と作者名を自由に変更してください。


Mithrilをインストールするには、インストールページの解説に従ってください。Mithrilがインストールされたプロジェクトのスケルトンができると、アプリケーションを作成する準備が整いました。

状態を保存するモジュールを作ってみましょう。src/models/User.jsという名前のファイルを作ってみましょう。

// src/models/User.js
var User = {
    list: []
}

module.exports = User

サーバーから何らかのデータをロードしてくるコードを追加しましょう。サーバーとコミュニケーションするには、MithrilのXHRユーティリティである m.requestが使用できます。まず、Mithrilをインクルードして取り込みましょう。

// src/models/User.js
var m = require("mithril")

var User = {
    list: []
}

module.exports = User

次に、XHR呼び出しを行う関数を作ります。loadListという名前にしましょう。

// src/models/User.js
var m = require("mithril")

var User = {
    list: [],
    loadList: function() {
        // TODO: XHR呼び出しをする
    }
}

module.exports = User

それではXHRリクエストを行うm.request呼び出しを行いましょう。このチュートリアルでは、REM APIへの呼び出しを行います。これは高速なプロトタイピングのためにデザインされたのモックのREST APIです。このAPIを使ってGET https://rem-rest-api.herokuapp.com/api/usersエンドポイントにアクセスすると、ユーザーのリストが返ってきます。m.requestを使ってXHRのリクエストを行い、そのエンドポイントのレスポンスを取得しましょう。

// src/models/User.js
var m = require("mithril")

var User = {
    list: [],
    loadList: function() {
        return m.request({
            method: "GET",
            url: "https://rem-rest-api.herokuapp.com/api/users",
            withCredentials: true,
        })
        .then(function(result) {
            User.list = result.data
        })
    },
}

module.exports = User

methodオプションにはHTTPメソッドを指定します。サーバーに副作用をあたえずにデータを取得するので、GETメソッドを使う必要があります。urlはAPIのエンドポイントのアドレスです。withCredentials: trueはREM APIで必要となる、クッキーの送受信を有効化する設定です。

m.requestを呼び出すとPromiseが返されます。このPromiseが解決すると、エンドポイントから返ってきたデータが渡されます。デフォルトではMithrilはHTTPレスポンスボディがJSONフォーマットであるとみなして、自動でJavaScriptのオブジェクトか配列に変換しようとします。.thenコールバックは、XHRリクエストが完了すると呼び出されます。この場合、このコールバックはresult.dataの配列をUser.listに代入します。

loadListの中に、return構文があることに気づかれたでしょうか?これはPromiseを使うときによく使われる良い習慣です。これにより、XHRのリクエストが完了した後に呼ばれるコールバックを追加で登録できるようになります。

このシンプルなモデルは2つのメンバーを公開しています。User.list(ユーザーオブジェクトの配列)と、User.loadList(サーバーのデータを使ってUser.listを初期化する)です。


それでは今作成したユーザーモデルモジュールのデータを表示できるように、ビューモジュールを作成しましょう。

src/views/UserList.jsというファイルを作成しましょう。まず、これからすぐに必要になるMithrilとモデルをインクルードします。

// src/views/UserList.js
var m = require("mithril")
var User = require("../models/User")

次にMithrilコンポーネントを作りましょう。今回作成するコンポーネントはview関数を持つシンプルなオブジェクトです:

// src/views/UserList.js
var m = require("mithril")
var User = require("../models/User")

module.exports = {
    view: function() {
        // TODO: ここにコードを追加する
    }
}

デフォルトでは、Mithrilのビューはhyperscriptを使って定義されています。Hyperscriptは、複雑なタグのHTMLよりも自然に字下げできる簡潔な構文を提供します。また、その構文は単純にJavascriptなので、Javascriptツールのエコシステムを多く活用することができます。例えばBabel、JSX(インラインHTML構文拡張 )、eslint(linting)、uglifyjs(minification)、istanbul(コードカバレッジ)、flow(静的型分析)など。 Hyperscriptを使うと、複雑なタグを持つHTMLよりも自然にインデントできる、完結な構文でテンプレートが記述できます。それに加えて、Babel, JSX (インラインのHTML文法拡張), eslint (構文チェック), uglifyjs (コードサイズ縮小化), istanbul (コードカバレッジ), flow (静的な型解析) などのさまざまなJavaScriptのツールを活用することができます。

それでは、MithrilのHyperscriptを使って要素のリストを作成してみましょう。HyperscriptはMithrilのビューを作成するもっとも一般的な手法ですが、基本的な書き方に慣れてきた後に使う代替手法として人気のあるJSXにも挑戦してみてください。

var m = require("mithril")
var User = require("../models/User")

module.exports = {
    view: function() {
        return m(".user-list")
    }
}

".user-list"文字列は、見た目で期待される通り、CSSセレクターです。.user-listはクラスを表しています。タグが指定されないとdivがデフォルトで使用されます。このビューは<div class="user-list"></div>と等価です。

それでは、以前作成したモデル (User.list) のユーザーのリストを参照し、各要素に対してループを回してみましょう:

// src/views/UserList.js
var m = require("mithril")
var User = require("../models/User")

module.exports = {
    view: function() {
        return m(".user-list", User.list.map(function(user) {
            return m(".user-list-item", user.firstName + " " + user.lastName)
        }))
    }
}

User.listはJavaScriptの配列ですし、hyperscriptのビューも単なるJavaScriptです。そのため、.mapメソッドを使って配列をループすることができます。このコードはユーザー名を含むdivのリストを表す、vnodeの配列を作成します。

このコードには問題があります。それはUser.loadList関数を呼んでいないことです。そのため、User.listは空の配列のままです。ビューをレンダリングしても何も表示されません。コンポーネントを描画するためにUser.loadListを呼ぶ必要がありますが、それにはコンポーネントのライフサイクルメソッドの力を借ります:

// src/views/UserList.js
var m = require("mithril")
var User = require("../models/User")

module.exports = {
    oninit: User.loadList,
    view: function() {
        return m(".user-list", User.list.map(function(user) {
            return m(".user-list-item", user.firstName + " " + user.lastName)
        }))
    }
}

コンポーネントにoninitを追加しました。これにはUser.loadListの参照を持たせています。このコードは、コンポーネントの初期化時に、XHRのリクエストを行うUser.loadListが呼ばれることを意味しています。サーバーがレスポンスを返すと、User.listに値が設定されます。

このコードではoninit: User.loadList() (末尾にかっこ) とはしませんでした。これらの違いは、oninit: User.loadList()の場合はその場で実行されますが、oninit: User.loadListコンポーネントのレンダリング時にのみ呼び出されます。これは重大な違いであり、JavaScript初心者が陥りやすい落とし穴です。その場で関数を呼び出すと、コンポーネントがレンダリングされるかどうかに関わらず、ソースコードの評価後にすぐにXHRのリクエストが行われてしまいます。この場合、アプリケーション上で前後に移動した場合などにコンポーネントが再作成されたときには、期待に反して呼び出しが行われません。


それでは最初に作成したエントリーポイントのファイルsrc/index.jsからビューのレンダリングを行いましょう:

// src/index.js
var m = require("mithril")

var UserList = require("./views/UserList")

m.mount(document.body, UserList)

m.mountを呼び出すと、指定されたコンポーネント (UserList) をDOM要素 (document.body) 内にレンダリングされます。DOMに今まであった要素は削除されます。HTMLファイルをブラウザで開くと、人名のリストが表示されるはずです。


この状態だと何もスタイルを設定されていません。

現在は、アプリケーションのスタイルを整えるのに利用できる規約やライブラリがたくさんあります。Bootstrapのように、HTMLの構造と、意味のあるクラス名の両方を指定する仕組みもあります。これはクラス名と意味が近いという利点がありますが、カスタマイズが難しいという欠点があります。一方で、Tachyonsのように、たくさんの自己記述型のアトミックではあるものの、意味を持たないクラス名にするというコストを支払っています。"CSS-in-JS"は最近人気を伸ばしている他のCSSシステムです。基本的にトランスパイルによってスコープを実現します。CSS-in-JSライブラリを使うと、問題となる領域を狭くすることができるため、メンテナンス性が向上しますが、複雑性のコストを払う必要があります。

どのようなCSSの表記法やライブラリを使ったとしても、CSSのカスケーディング機能を避けることが大切です。チュートリアルでは単純に済ませるために、大げさなぐらい明示的なクラス名を使っています。クラス名自身がTachyonsが提供するようなアトミック性を持っているため、クラス名の衝突は起きないでしょう。プレーンなCSSでも複雑さの低いプロジェクト(例えば最初の実装工数が3-6人月でフェーズが少ない)では十分です。

スタイルを追加したら、styles.cssを作成し、index.htmlにリンクを追加しましょう。

<!doctype html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Mithrilのアプリケーション</title>
        <link href="styles.css" rel="stylesheet" />
    </head>
    <body>
        <script src="bin/app.js"></script>
    </body>
</html>

これでUserListコンポーネントにスタイルを追加することができるようになりました:

.user-list {list-style:none;margin:0 0 10px;padding:0;}
.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
.user-list-item:hover {text-decoration:underline;}

上記のCSSは、ルールのすべてのスタイルをアルファベット順に1行にまとめるという規約を使って書かれています。この表記法は、スクリーンサイズの横幅を最大限に活かすルールになっています。CSSセレクターを探す時は、論理的なグループで整列されて並んでいますし、予測可能で均一化されているため、簡単に要素を探すことができます。

もちろん、スペースとインデントを使うルールが好みであればそれを使うこともできます。このサンプルが紹介しているルールはあまり広まっていはいませんが、強い理由付けがあるルールになっています。より広く使うルールもあります。

ブラウザウィンドウをリロードすると、スタイルが設定された要素が表示されます。


ラウティングをアプリケーションに追加しましょう。

ラウティングはユニークなURLと画面の組を設定します。これにより、複数の「ページ」の間の移動ができるようになります。Mithrilはシングルページアプリケーションを念頭に設計されています。この「ページ」は古い価値観ではそれぞれ個別のHTMLファイルですが、シングルページアプリケーションではページごとにファイルを分ける必要はありません。シングルページアプリケーションのラウティングを使うと、同じHTMLファイルがブラウザを閉じられるまで継続して使われますが、JavaScriptを使ってアプリケーションの状態が変更されます。クライアント側でラウティングを行うと、ページ遷移中に画面がクリアされてブランクなページが見えてしまうのを避けることができます。また、ウェブサービス指向アーキテクチャ(サーバー側で生成されたHTMLを返すのではなく、JSONでデータをダウンロードさせるウェブアプリケーション)と組み合わせると、サーバーとの通信量を削減できます。

m.mount呼び出しをm.routeに変更すると、ラウティングを使うことができます:

// src/index.js
var m = require("mithril")

var UserList = require("./views/UserList")

m.route(document.body, "/list", {
    "/list": UserList
})

m.routeを呼ぶとアプリケーションがdocument.bodyの中にレンダリングされます。"/list"引数はデフォルトのラウトになります。ユーザーがラウト一覧にないURLを開こうとすると、このURLにリダイレクトされます。{"/list": UserList}オブジェクトは存在するラウトと、それぞれのラウトに対してどのコンポーネントが利用されるのかのマップを宣言します。

ブラウザのページをリフレッシュするとURLに#!/listが付与されて、ラウティング機構が稼働していることが分かります。ラウトは同じUserListのレンダリングを行うため、前と同じユーザーのリストが見えます。

#!スニペットはハッシュバングと呼ばれています。これはクライアントサイドのラウティング実装で一般的に使われる文字列です。この文字列はm.route.prefixを使って設定できます。設定に寄ってはサーバー側の変更が必要となるため、このチュートリアル内ではこのまま進めます。


それではユーザーを編集するための別のラウトをアプリケーションに追加しましょう。まず、views/UserForm.jsというモジュールを作成します。

// src/views/UserForm.js

module.exports = {
    view: function() {
        // TODO: ビューを実装する
    }
}

モジュールがでkちあらrequireを使ってsrc/index.jsにこの新しいモジュールを追加します。

// src/index.js
var m = require("mithril")

var UserList = require("./views/UserList")
var UserForm = require("./views/UserForm")

m.route(document.body, "/list", {
    "/list": UserList
})

次に、このモジュールを参照するラウトを追加しましょう:

// src/index.js
var m = require("mithril")

var UserList = require("./views/UserList")
var UserForm = require("./views/UserForm")

m.route(document.body, "/list", {
    "/list": UserList,
    "/edit/:id": UserForm,
})

新しいラウトの中に:idという文字列があります。これはラウトパラメータです。これは一種のワイルドカードでと考えることができます。/edit/1というURLがあると、id"1"が代入され、UserFormが解決されます。/edit/2UserFormを解決しますが、id"2"となります。他にもあります。

それではこれらのラウとパラメータを受け取れるようにUserFormコンポーネントを実装しましょう。

// src/views/UserForm.js
var m = require("mithril")

module.exports = {
    view: function() {
        return m("form", [
            m("label.label", "名前"),
            m("input.input[type=text][placeholder=名前]"),
            m("label.label", "名字"),
            m("input.input[placeholder=名字]"),
            m("button.button[type=button]", "保存"),
        ])
    }
}

新しいコンポーネントで使うスタイルもstyles.cssに追加しましょう:

/* styles.css */
body,.input,.button {font:normal 16px Verdana;margin:0;}

.user-list {list-style:none;margin:0 0 10px;padding:0;}
.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
.user-list-item:hover {text-decoration:underline;}

.label {display:block;margin:0 0 5px;}
.input {border:1px solid #ddd;border-radius:3px;box-sizing:border-box;display:block;margin:0 0 10px;padding:10px 15px;width:100%;}
.button {background:#eee;border:1px solid #ddd;border-radius:3px;color:#333;display:inline-block;margin:0 0 10px;padding:10px 15px;text-decoration:none;}
.button:hover {background:#e8e8e8;}

この状態では表示はできますが、ユーザーイベントには反応するようになっていません。src/models/User.jsに実装されているUserにいくつかコードを追加しましょう。これが変更前のコードです。

// src/models/User.js
var m = require("mithril")

var User = {
    list: [],
    loadList: function() {
        return m.request({
            method: "GET",
            url: "https://rem-rest-api.herokuapp.com/api/users",
            withCredentials: true,
        })
        .then(function(result) {
            User.list = result.data
        })
    },
}

module.exports = User

1人のユーザをロードできるようにコードを追加します。

// src/models/User.js
var m = require("mithril")

var User = {
    list: [],
    loadList: function() {
        return m.request({
            method: "GET",
            url: "https://rem-rest-api.herokuapp.com/api/users",
            withCredentials: true,
        })
        .then(function(result) {
            User.list = result.data
        })
    },

    current: {},
    load: function(id) {
        return m.request({
            method: "GET",
            url: "https://rem-rest-api.herokuapp.com/api/users/:id",
            data: {id: id},
            withCredentials: true,
        })
        .then(function(result) {
            User.current = result
        })
    }
}

module.exports = User

User.currentプロパティが追加され、User.load(id)メソッドを呼び出すとこのプロパティに格納されます。UserFormビューでこのメソッドを使うようにしましょう。

// src/views/UserForm.js
var m = require("mithril")
var User = require("../models/User")

module.exports = {
    oninit: function(vnode) {User.load(vnode.attrs.id)},
    view: function() {
        return m("form", [
            m("label.label", "名前"),
            m("input.input[type=text][placeholder=名前]", {value: User.current.firstName}),
            m("label.label", "名字"),
            m("input.input[placeholder=名字]", {value: User.current.lastName}),
            m("button.button[type=button]", "保存"),
        ])
    }
}

UserListコンポーネントと同様に、oninitからUser.load()を呼び出します。"/edit/:id": UserFormというラウト定義には:idというパラメータがあったのを覚えているでしょうか?ラウとパラメータはUserFormコンポーネントのvnodeの属性となります。/edit/1というURLでアクセスされると、vnode.attrs.id"1"が代入されます。

UserListビューを修正し、UserFormのページに遷移できるようにしましょう:

// src/views/UserList.js
var m = require("mithril")
var User = require("../models/User")

module.exports = {
    oninit: User.loadList,
    view: function() {
        return m(".user-list", User.list.map(function(user) {
            return m("a.user-list-item", {href: "/edit/" + user.id, oncreate: m.route.link}, user.firstName + " " + user.lastName)
        }))
    }
}

.user-list-itema.user-list-itemに修正しました。クリック時の移行先の参照をhrefに追加します。そしてSPAとして遷移を行うために、oncreate: m.route.linkも追加します。これにより、このリンクは通常のリンクのように動作するのではなく、ラウトの切り替えのリンクとして動作するようになります。このリンクをクリックすると、現在のページをアンロードすることなく、ハッシュバング#!以降のURLだけが書き換わるようになります。

ページをリフレッシュすると、人の行をクリックしてフォームに遷移できるようになります。ブラウザの戻るボタンを押すと人名のリストに戻ります。


このフォーム自体はまだ未完成なので「保存」を押しても保存しません。この機能を完成させましょう:

// src/views/UserForm.js
var m = require("mithril")
var User = require("../models/User")

module.exports = {
    oninit: function(vnode) {User.load(vnode.attrs.id)},
    view: function() {
        return m("form", {
                onsubmit: function(e) {
                    e.preventDefault()
                    User.save()
                }
            }, [
            m("label.label", "名前"),
            m("input.input[type=text][placeholder=名前]", {
                oninput: m.withAttr("value", function(value) {User.current.firstName = value}),
                value: User.current.firstName
            }),
            m("label.label", "名字"),
            m("input.input[placeholder=名字]", {
                oninput: m.withAttr("value", function(value) {User.current.lastName = value}),
                value: User.current.lastName
            }),
            m("button.button[type=button]", "保存"),
        ])
    }
}

両方のinputタグに、ユーザーが入力をするとUser.current.firstNameUser.current.lastNameプロパティを変更するoninput イベントを追加しました

これに加えて、「保存」ボタンが押されたときに呼ばれるUser.saveメソッドを定義する必要があります。このメソッドを実装してみましょう:

// src/models/User.js
var m = require("mithril")

var User = {
    list: [],
    loadList: function() {
        return m.request({
            method: "GET",
            url: "https://rem-rest-api.herokuapp.com/api/users",
            withCredentials: true,
        })
        .then(function(result) {
            User.list = result.data
        })
    },

    current: {},
    load: function(id) {
        return m.request({
            method: "GET",
            url: "https://rem-rest-api.herokuapp.com/api/users/:id",
            data: {id: id},
            withCredentials: true,
        })
        .then(function(result) {
            User.current = result
        })
    },

    save: function() {
        return m.request({
            method: "PUT",
            url: "https://rem-rest-api.herokuapp.com/api/users/:id",
            data: User.current,
            withCredentials: true,
        })
    }
}

module.exports = User

このソースの一番下にあるsaveメソッドの中では、PUT HTTPメソッドを指定して、サーバーにデータの変更を行います。

それでは、できあがったアプリケーションを使ってユーザーの名前を編集してみましょう。変更を保存すると、変更がユーザーリストに反映されるのを見ることができます。


現在は、ブラウザの戻るボタンしか、元のリストに戻る手段がありません。理想的には、何らかのメニュー、あるいはさらに汎用化させて、グローバルなUI要素が配置されたレイアウトがある方が望ましいでしょう。

src/views/Layout.jsというファイルを作成しましょう:

var m = require("mithril")

module.exports = {
    view: function(vnode) {
        return m("main.layout", [
            m("nav.menu", [
                m("a[href='/list']", {oncreate: m.route.link}, "ユーザー一覧")
            ]),
            m("section", vnode.children)
        ])
    }
}

このコンポーネントはきわめてシンプルです。ユーザー一覧へのリンクを持つ<nav>要素を持ちます。/editリンクを作成したのと同じように、このリンクもラウティングの変更が行えるようにm.route.linkを使っています。

<section>エレメントには、子供の要素としてvnode.childrenを渡しています。vnodeLayoutコンポーネントのインスンタンスを表現しているvnodeの参照です。vnodeはm(Layout)という関数呼び出しで返されます。vnode.childrenはvnodeの子供を参照します。

スタイルを追加しましょう:

/* styles.css */
body,.input,.button {font:normal 16px Verdana;margin:0;}

.layout {margin:10px auto;max-width:1000px;}
.menu {margin:0 0 30px;}

.user-list {list-style:none;margin:0 0 10px;padding:0;}
.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
.user-list-item:hover {text-decoration:underline;}

.label {display:block;margin:0 0 5px;}
.input {border:1px solid #ddd;border-radius:3px;box-sizing:border-box;display:block;margin:0 0 10px;padding:10px 15px;width:100%;}
.button {background:#eee;border:1px solid #ddd;border-radius:3px;color:#333;display:inline-block;margin:0 0 10px;padding:10px 15px;text-decoration:none;}
.button:hover {background:#e8e8e8;}

src/index.jsのラウトを変更して、このレイアウトを追加してみましょう:

// src/index.js
var m = require("mithril")

var UserList = require("./views/UserList")
var UserForm = require("./views/UserForm")
var Layout = require("./views/Layout")

m.route(document.body, "/list", {
    "/list": {
        render: function() {
            return m(Layout, m(UserList))
        }
    },
    "/edit/:id": {
        render: function(vnode) {
            return m(Layout, m(UserForm, vnode.attrs))
        }
    },
})

それぞれのコンポーネントはラウトリゾルバに置き換えられました。ラウトリゾルバはrenderメソッドを持つオブジェクトです。renderメソッドは通常のコンポーネントのビュート同じように、ネストされたm()関数呼び出しを使って書くことができます。

m()関数呼び出しの中で、セレクター文字列の代わりにコンポーネントが使われている点が今までと違っていますよね?/listラウトの中では、m(Layout, m(UserList))というコードが呼ばれています。このコードは、vnodeのルートはLayoutのインスタンスとなり、子供としてUserListvnodeを持つという意味になります。

/edit/:idラウトでは、vnode引数を使ってラウトパラメータをUserFormコンポーネントに渡しています。URLがもし/edit/1であれば、vnode.attrs{id: 1}となり、m(UserForm, vnode.attrs)m(UserForm, {id: 1})と等価になります。等価なJSXコードは<UserForm id={vnode.attrs.id} />となります。

このページをリフレッシュすると、アプリケーション内のすべてのページにグローバルナビゲーションが表示されます。


チュートリアルはここで終了です。

このチュートリアルでは、サーバーからユーザーをリストして個別に編集できるシンプルなアプリケーションを順を追って作成してきました。ぜひ、追加のエクササイズとして、ユーザの新規作成と削除の実装にも挑戦してみてください。

もしMithrilコードのサンプルをもっと見たいのであればサンプルページを参照してください。もし何か質問があればMithrilのチャット(英語)で自由に聞いてください。


License: MIT. © Leo Horie.