Mithril 1.1.0

route(root, defaultRoute, routes)


説明

アプリケーション内の「ページ」間のナビゲーションを行います。

var Home = {
    view: function() {
        return "ようこそ"
    }
}

m.route(document.body, "/home", {
    "/home": Home, // `http://localhost/#!/home` の定義
})

アプリケーションごとに1回だけm.route呼び出しが行えます。


シグニチャ

m.route(root, defaultRoute, routes)

引数 必須 説明
root Element Yes サブツリーの親ノードとなるDOM要素
defaultRoute String Yes 現在のURLが、定義されたどのラウトにもマッチしなかった時にリダイレクトされる先のラウト
routes Object Yes キーがラウトのURL、値がコンポーネントかラウトリゾルバーが格納されているオブジェクト
返り値 undefinedを返す

シグニチャの読み方

静的メンバー

m.route.set

マッチするラウト、もしくはマッチするラウトがなければデフォルトのラウトにリダイレクトします。

m.route.set(path, data, options)

引数 必須 説明
path String Yes 移行先のラウト。ただしプリフィックスは含まない。パスにはラウとのパラメータを含めることができます。
data Object No ラウトのパラメータもしpathはラウとのパラメータスロットを持っている時は、このオブジェクトのプロパティがパス文字列のパラメータ部として利用されます。
options.replace Boolean No 新しい履歴のエントリーを追加するか、今のエントリーを置き換えるか。デフォルトはfalseです。
options.state Object No stateオブジェクトは、内部で呼び出されるhistory.pushState / history.replaceStateに渡されます。このstateオブジェクトはhistory.stateプロパティを通じてアクセスできます。プロパティはラウトパラメータオブジェクトにマージされます。このオプションはpushState APIを使っているときにのみ利用できます。pushState APIが利用できないときは、ラウターはハッシュ変更モードにフォールバックしますがこの時は無視されます。
options.title String No title文字列は、内部で呼び出されるhistory.pushState / history.replaceStateに渡されます。
返り値 undefinedを返す
m.route.get

最後に解決されたラウトのパスをプリフィックスなしで返します。非同期で解決が遅延されている時は、ブラウザのロケーションバーに表示されているパスと異なることがあります。

path = m.route.get()

引数 必須 説明
返り値 String 最後に解決されたラウトのパス
m.route.prefix

ラウターのプリフィックスを定義します。ラウタープリフィックはラウターが使用する戦略を決定する文字列です。

m.route.prefix(prefix)

引数 必須 説明
prefix String Yes プリフィックスはMithrilが内部で使用するラウト戦略を制御します。
返り値 undefinedを返す

この関数はおそらくm("a") vnodeのoncreate(とonupdate)フック内で使用されます:

m("a[href=/]", {oncreate: m.route.link})`.

m.route.linkoncreateフックとして使用すると、ラウター用のリンクとして動作するようになります。hrefはラウトに特化したナビゲーションリンクなります。現在のページから他のページへジャンプしてブラウザの内容がリセットされるのではなく、hrefで指定されたラウトへの内部ジャンプになります。

href属性が静的なものでなければ、onupdateフックも設定しなければなりません。

m("a", {href: someVariable, oncreate: m.route.link, onupdate: m.route.link})`

m.route.link(vnode)

引数 必須 説明
vnode Vnode Yes このメソッドは<a>タグとvnodeoncreateonupdateフックを結びつけるときに使います。
返り値 undefinedを返す
m.route.param

ラウトパラメータの取得ラウとパラメータはキー・バリューのペアです。ラウトパラメータはいくつかの場所から収集されます:

value = m.route.param(key)

引数 必須 説明
key String No ラウトパラメータ名(例えば、/users/:idというラウトであればid/users/1?page=3というパスならpage、あるいはhistory.stateのキー)
返り値 String|Object 指定されたキーの値を返します。もしキーが指定されなければ、すべての挿入されたキーを含むオブジェクトを返します。

ラウトリゾルバ

ラウとリゾルバはonmatchメソッドとrenderメソッドの両方、あるいはどちらかを持つオブジェクトです。メソッドはどちらもオプションですが、どちらか一方は必要です。ラウトリゾルバはコンポーネントではありません。そのため、ライフサイクルメソッドも持ちません。一般的な使い方は、ラウトリゾルバはm.route呼び出しと同じファイルに入れ、コンポーネントはそれぞれ別のファイルに入れる方法です。

routeResolver = {onmatch, render}

routeResolver.onmatch

onmatchフックはレンダリングするコンポーネントを探すときに呼ばれます。これはラウターのパスが一回変更されるたびに呼ばれます。ただし、同じパスに対して再帰的に子要素が呼び出されて行われる再描画では呼びされません。これはコンポーネントの初期化前位にロジックを実行することができます。例えば認証ロジックはデータのプリロード、リダイレクト分析とかです。

このメソッドを使うと、どのコンポーネントをレンダリングすべきかを非同期に決めることができます。コードを分割して非同期のモジュールのロードを行うときに使えます。コンポーネントのレンダリングを非同期に行う時は、解決時にコンポーネントが格納されるPromiseを返します。

onmatchの詳細は、応用的なコンポーネントの解決のセクションを参照してください。

routeResolver.onmatch(args, requestedPath)

引数 説明
args Object ラウトパラメータ
requestedPath String 最後にラウト操作を行った時のラウターパスです。ラウターパラメータ値は入りますが、プリフィックスは含まれません。onmatchが呼ばれると、パスの解決は延期され、m.route.get()が返すパスも解決されて変更される前のままとなります。
返り値 Component|Promise|undefined コンポーネントまたは、解決時にコンポーネントが格納されるPromiseを返します。

もしonmatchがコンポーネント、あるいはコンポーネントを解決するPromiseを返すと、このコンポーネントはラウトリゾルバーの render メソッドの最初の引数のvnode.tagとして渡されます。返さない時はvnode.tagには"div"が設定されます。同様に、onmatchメソッドが省略されると、vnode.tag"div"になります。

onmatchがPromiseを返し、それがrジジェクとされたら、ラウターのラウトはdefaultRouteに戻ります。この動作を回避するには、Promiseのチェーンを返す前に.catchを呼び出して動作を変更してください。

routeResolver.render

renderはラウトがマッチしたときの再描画時に毎回呼ばれます。これはコンポーネントのviewメソッドに似ていますが、コンポーネント合成をシンプルにおこなうために存在しています。

vnode = routeResolve.render(vnode)

引数 説明
vnode Object vnodeですが、このオブジェクトの属性は、ラウトパラメータを含んでいます。もしonmatchがコンポーネントも、コンポーネントを返すPromiseも返さなかった時は、tag フィールドは"div"となります。
vnode.attrs Object URLパラメータ値のマップ
返り値 Array<Vnode>|Vnode レンダリング対象のvnode

どのように動作するのか

ラウティング(Routing = アメリカ英語読みはルーティングよりもラウティングが近い)は、シングルページアプリケーション(SPA)を作るための仕組みです。他のページに行く時に、フルにブラウザをリフレッシュしなくても済むアプリケーションを実現することができます。

この機能を使うと、各ページをブックマークしたり、ブラウザの履歴の機能はそのままに、シームレスなナビゲーションが可能になります。

ページリフレッシュをしないラウティングの一部はhistory.pushState()APIによって実現されています。このAPIを使うと、ページがロードされた後に、プログラムによって新たなリロードを行わずに、ブラウザに表示されるURLを変更できます。しかし、プログラマはそのURLがそのままコピーされて、新しいタブなどの新しい状態で使用されたときにも、適切なマークアップが表示されることを保証しなければなりません。

ラウト戦略

ラウと戦略は、ライブラリがどのようにラウトを取り扱うのかを決定します。SPAのラウティングシステムを実装するのに使える戦略は、一般的に3種類あります。それぞれ違った特性を持っています。

ハッシュ戦略を使うと、history.pushStateをサポートしていないブラウザ(名前を挙げるとならInternet Explorer 9)でもonhashchangeにフォールバックするため使用することができます。もしIE9をサポートしたいのであればこの戦略を使用してください。

クエリー文字列戦略も技術的にはIE9で動作しますが、ページのリロードが発生してしまいます。この戦略はアンカーを使ったリンクを使用したいが、サーバー側の都合でパス名戦略が使用できないときに使います。

パス名戦略はもっともクリーンなURLを生成しますが、IE9をサポートしません。なおかつアプリケーションのラウターで取りうるすべてのURLでシングルページアプリケーションのコードを返すようにサーバーを設定する必要があります。この戦略はIE9のサポートが必要でなく、なおかつURLの見た目をきれいにしたいときに使います。

ハッシュ戦略を使うシングルページアプリケーションは、ハッシュの後に感嘆符(!)を付けてアンカーへのリンクではなく、ハッシュをラウティングメカニズムとして使用することを明示することがよくあります。#!文字列はハッシュバングとして知られています。

デフォルトの戦略はハッシュバングです。


一般的な使用法

通常は、ラウトにマッピングするコンポーネントをいくつか作成します:

var Home = {
    view: function() {
        return [
            m(Menu),
            m("h1", "ホーム")
        ]
    }
}

var Page1 = {
    view: function() {
        return [
            m(Menu),
            m("h1", "ページ 1")
        ]
    }
}

このサンプルには、HomePage1の2つのコンポーネントが含まれています。それぞれのコンポーネントはメニューとテキストを持っています。コードの重複を避けるためにメニューそのものもコンポーネントとなっています:

var Menu = {
    view: function() {
        return m("nav", [
            m("a[href=/]", {oncreate: m.route.link}, "ホーム"),
            m("a[href=/page1]", {oncreate: m.route.link}, "ページ 1"),
        ])
    }
}

コンポーネントがそろったら、コンポーネントとラウトを対応付けます:

m.route(document.body, "/", {
    "/": Home,
    "/page1": Page1,
})

このコードでは2つのラウト、//page1を定義しています。ユーザーがそれぞれのURLを訪れたら、それぞれ適切なコンポーネントをレンダリングします。デフォルトでは、SPAのラウターは#!をプリフィックスとして使用します。


上記のサンプルではMenuコンポーネントは2つリンクを持っていました。hrefにはラウターURLを指定できます。{oncreate: m.route.link}を付与すると、通常のリンクではなく、現在のページから指定ページに遷移するナビゲーションになります。

これ以外の方法では、m.route.set(route)を使うとソースコードを使ってページの遷移を行わせられます。m.route.set("/page1")のように使用します。

ラウトにナビゲートする時はラウタープリフィックスを指定する必要はありません。むしろ、m.route.linkやリダイレクトを行う時には、#!をラウトパスの前に追加してはいけません。


ラウトのパラメータ

ラウトの中に、ID変数などのデータ表現を追加したいことがあります。もちろん、可能性のあるIDをすべて個別のラウトとして明示したくはありません。この機能を実現するために、Mithrilはパラメータ化したラウトをサポートしています。

var Edit = {
    view: function(vnode) {
        return [
            m(Menu),
            m("h1", vnode.attrs.id + "を編集中")
        ]
    }
}
m.route(document.body, "/edit/1", {
    "/edit/:id": Edit,
})

このサンプルでは/edit/:idというラウトを定義しています。この記法を使うと、/edit/から始まり、何らかのデータがその後に続く動的なラウトを定義できます。例えば、/edit/1, edit/234といったパスにマッチします。id値は、vnode.attrs.idのように、コンポーネントのvnodeコンポーネントの属性にマップされます。

/edit/:projectID/:userIDのように、複数の引数をラウトに持たせることができます。これらの引数も、コンポーネントのvnode属性オブジェクトのprojectIDuserIDプロパティになります。

Keyパラメーター

もし、ユーザーがパラメータ付きのラウトから、同じラウト定義(/page/:id)のパラメータ違いのラウト(/page/1から/page/2)に遷移しようとした時は、1.0からは同じコンポーネントでラウトの解決が行われるため、コンポーネントの破棄と再作成は行われません。フルスクラッチではなく、その場のdiffによる高速な仮想DOMの更新が行われます。これの副作用として、このケースではoninit/oncreateのフックは呼ばれず、onupdateフックだけが呼び出されます。しかし、0.2.Xのように、ラウト変更イベントに対して、同期的なコンポーネントの再生成を行って欲しいという需要も、開発者の中では比較的一般的です。

これらの両方のニーズを満たすために、ラウトのパラメータ化と仮想DOMのkey称号機能を使います。

m.route(document.body, "/edit/1", {
    "/edit/:key": Edit,
})

このコードは、ルートのコンポーネントが、ラウトパラメータオブジェクトとしてkey属性を持つことを意味します。ラウとパラメータはvnodeのattrsプロパティとなります。この時、あるページから他のページに遷移すると、keyが変更されます。この属性の違いから、仮想DOMエンジンは、既存のコンポーネントと新しいコンポーネントの内容がまったく異なると判断し、既存のコンポーネントを破棄して新コンポーネントを再作成します。

この機能を使うと、リロード時に完全に再作成されるコンポーネントを作ることができます。

m.route.set(m.route.get(), {key: Date.now()})

あるいは、ヒストリー状態の機能を使うと、URLを変更せずにリロード可能なコンポーネントを作ることができます。

m.route.set(m.route.get(), null, {state: {key: Date.now()}})

可変個引数のラウト

可変個引数をもったラウトを作ることもできます。例えば、スラッシュを含むURLのパス名を引数に取るラウトなどを実現することができます:

m.route(document.body, "/edit/pictures/image.jpg", {
    "/files/:file...": Edit,
})

ヒストリー状態

ユーザのナビゲーションの使いやすさを向上させるために、裏で動作しているhistory.pushState APIのすべての機能を使いこなせるようになっています。例えば、ユーザーがナビゲーションに従って別のページに遷移した時に、巨大な入力フォームの状態をアプリケーションに覚えさせておくこともできます。この機能を使うと、ユーザーがブラウザの「戻る」ボタンを押したときにフォームの状態を入力時の状態に維持しておくことができます。

次のようなフォームを作ることができます:

var state = {
    term: "",
    search: function() {
        // 現在のラウトの状態を保持しておく
        // これは`history.replaceState({term: state.term}, null, location.href)`と同じ
        m.route.set(m.route.get(), null, {replace: true, state: {term: state.term}})

        // 別のページに遷移
        location.href = "https://google.com/?q=" + state.term
    }
}

var Form = {
    oninit: function(vnode) {
        state.term = vnode.attrs.term || "" // ユーザーが戻るボタンを操作したときは`history.state`プロパティを復元
    },
    view: function() {
        return m("form", [
            m("input[placeholder='Search']", {oninput: m.withAttr("value", function(v) {state.term = v}), value: state.term}),
            m("button", {onclick: state.search}, "検索")
        ])
    }
}

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

この方法を使うと、ユーザーが検索したあとに戻るボタンを押してアプリケーションに戻ってきたときに検索用語が表示されたままになります。このテクニックはユーザー体験を大きく改善することができます。ユーザーが入力した内容を消去してしまうアプリケーションはユーザーに面倒さを与えてしまいます。


ラウタープリフィックスの変更

ラウタープリフィックはラウターが使用する戦略を決定する文字列です。

// パス名戦略を使用
m.route.prefix("")

// クエリー文字列戦略を使用
m.route.prefix("?")

// 感嘆符なしのハッシュを使用
m.route.prefix("#")

// URLのルートで稼働しないパス名戦略を使うアプリケーションの設定
// 他のページがhttp://localhostにあり、アプリケーションがhttp://localhost/my-app以下にある場合
m.route.prefix("/my-app")

高度なコンポーネントの解決

コンポーネントをラウトにマッピングする代わりに、ラウトリゾルバーオブジェクトを使うことができます。ラウトリゾルバーオブジェクトにははonmatchメソッドとrenderメソッドの両方、あるいはどちらかが含まれます。メソッドはどちらもオプションですが、どちらか一方は必要です。

m.route(document.body, "/", {
    "/": {
        onmatch: function(args, requestedPath) {
            return Home
        },
        render: function(vnode) {
            return vnode // m(Home)と同じ
        },
    }
})

ラウトリゾルバーは、さまざまな高度なルーティングのユースケースを実装するのに便利です。


レイアウトコンポーネントのラッピング

ラウトで選択されるコンポーネントであっても、レイアウトと呼ばれる再利用可能な枠組みでラップして使われることがよくあります。まずさまざまなコンポーネントをラップする、共通のマークアップを含むコンポーネントを作成します。

var Layout = {
    view: function(vnode) {
        return m(".layout", vnode.children)
    }
}

上記の例ではレイアウトの構成要素は、引数で渡された子供のコンポーネントをラップする<div class="layout">しかありませんが、現実世界ではより複雑なコードになります。

レイアウトでラップする方法の1つは、ラウトの定義のマップの中で無名コンポーネントを定義する方法です:

// 例 1
m.route(document.body, "/", {
    "/": {
        view: function() {
            return m(Layout, m(Home))
        },
    },
    "/form": {
        view: function() {
            return m(Layout, m(Form))
        },
    }
})

しかし、トップレベルコンポーネントが無名コンポーネントだと、/から/formなどのラウトに遷移した時に、無名コンポーネント一度破棄されて、ゼロから再生成されます。もしレイアウトコンポーネントがライフサイクルメソッドを持っていたとすると、ラウトの変更のたびにoninitoncreateフックが呼び出されます。アプリケーションによりますが、この動作が望ましくないこともあるでしょう。

Layoutコンポーネントをゼロから再作成するのではなく、そのままの状態で差分更新を有効にしたい場合は、代わりにラウトリゾルバーをルートオブジェクトとして使用します:

// 例 2
m.route(document.body, "/", {
    "/": {
        render: function() {
            return m(Layout, m(Home))
        },
    },
    "/form": {
        render: function() {
            return m(Layout, m(Form))
        },
    }
})

この場合、すべてのラウトで同じレイアウトが使われていると判断されるため、レイアウトコンポーネントのoninitoncreateのライフサイクルメソッドは最初のラウトの遷移では呼ばれなくなります。

これらのサンプルの違いを明確にするために、例 1と同じコードを作成してみます。

//例 1と機能的に同じ
var Anon1 = {
    view: function() {
        return m(Layout, m(Home))
    },
}
var Anon2 = {
    view: function() {
        return m(Layout, m(Form))
    },
}

m.route(document.body, "/", {
    "/": {
        render: function() {
            return m(Anon1)
        }
    },
    "/form": {
        render: function() {
            return m(Anon2)
        }
    },
})

Anon1Anon2は異なるコンポーネントなので、Layoutを含むこれらのサブツリーはすべて、移行時に全部破棄されてから再作成されます。これは、ラウトリゾルバーを使わず、コンポーネントを直接使っていても起きます。

例2では、どちらのラウトでもLayoutがトップレベルコンポーネントでした。そのため、Layoutコンポーネントでは完全再作成ではなく、差分検知が行われ、変更がない部分はそのまま残り、DOMの子要素のHomeからFormへの再作成だけが行われます。


認証

ラウトリゾルバーのonmatchフックはラウトのトップレベルコンポーネントが初期化される前になんらかのロジックを実行するときに使えます。次のサンプルは、ログインスクリーンを作成し、ログインしていないユーザーに/secretページを見せないようにするサンプルです。

var isLoggedIn = false

var Login = {
    view: function() {
        return m("form", [
            m("button[type=button]", {
                onclick: function() {
                    isLoggedIn = true
                    m.route.set("/secret")
                }
            }, "ログイン")
        ])
    }
}

m.route(document.body, "/secret", {
    "/secret": {
        onmatch: function() {
            if (!isLoggedIn) m.route.set("/login")
            else return Home
        }
    },
    "/login": Login
})

アプリケーションがロードされると、onmatchが呼ばれ、isLoggedInがfalseになり、/loginにリダイレクトされます。ユーザーがログインすると、isLoggedInがtrueに設定され、アプリケーションは/secretにリダイレクトします。onmatchが再度実行されたときにisLoggedInがtrueであれば、Homeコンポーネントがレンダリングされます。

サンプルコードを短くするためにユーザーのログイン状態はグローバル変数を使って保持しています。このフラグはユーザーががログインボタンを押すと単純に入れ替わる手抜き実装になっています。実際のアプリケーションでは、ユーザーは適切なログインクレデンシャルを持つ必要があるでしょう。ログインボタンを押すとサーバーにユーザーの認証リクエストを送る実装になるでしょう。

var Auth = {
    username: "",
    password: "",

    setUsername: function(value) {
        Auth.username = value
    },
    setPassword: function(value) {
        Auth.password = value
    },
    login: function() {
        m.request({
            url: "/api/v1/auth",
            data: {username: Auth.username, password: Auth.password}
        }).then(function(data) {
            localStorage.setItem("auth-token": data.token)
            m.route.set("/secret")
        })
    }
}

var Login = {
    view: function() {
        return m("form", [
            m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
            m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
            m("button[type=button]", {onclick: Auth.login, "ログイン")
        ])
    }
}

m.route(document.body, "/secret", {
    "/secret": {
        onmatch: function() {
            if (!localStorage.getItem("auth-token")) m.route.set("/login")
            else return Home
        }
    },
    "/login": Login
})

データのプリロード

通常はコンポーネントの初期化時にデータをロードします。この方法でロードすると、コンポーネントはラウティング時と、リクエスト完了時の二回レンダリングされます。

var state = {
    users: [],
    loadUsers: function() {
        return m.request("/api/v1/users").then(function(users) {
            state.users = users
        })
    }
}

m.route(document.body, "/user/list", {
    "/user/list": {
        oninit: state.loadUsers,
        view: function() {
            return state.users.length > 0 ?state.users.map(function(user) {
                return m("div", user.id)
            }) : "ロード中"
        }
    },
})

上記のサンプルは、最初のレンダリングではstate.users が空配列なので、リクエストが完了するまでは"ロード中"と表示されます。データが利用可能にあると、UIが再描画され、ユーザーIDのリストが表示されます。

ラウトリゾルバーを使って、UIのフリッカーを回避するためにコンポーネントのレンダリング前にデータのプリロードを行い、代わりにロード中のインジケーターを表示することができます。

var state = {
    users: [],
    loadUsers: function() {
        return m.request("/api/v1/users").then(function(users) {
            state.users = users
        })
    }
}

m.route(document.body, "/user/list", {
    "/user/list": {
        onmatch: state.loadUsers,
        render: function() {
            return state.users.map(function(user) {
                return m("div", user.id)
            })
        }
    },
})

上記のrenderはリクエストが完了したときにのみ実行されます。


コード分割

巨大なアプリケーションでは、事前に全部のプログラムをダウンロードするのではなく、ラウトごとにオンデマンドでコードをダウンロードするほうが望ましいことがあります。このようなコードベースの分割は、コード分割、あるいは遅延ダウンロードと呼ばれます。MithrilではonmatchフックでPromiseを返すことで簡単に実現できます。

もっとも基本的な形式は次のようになります:

// Home.js
module.export = {
    view: function() {
        return [
            m(Menu),
            m("h1", "ホーム")
        ]
    }
}
// index.js
function load(file) {
    return m.request({
        method: "GET",
        url: file,
        extract: function(xhr) {
            return new Function("var module = {};" + xhr.responseText + ";return module.exports;")
        }
    })
}

m.route(document.body, "/", {
    "/": {
        onmatch: function() {
            return load("Home.js")
        },
    },
})

しかし、現実的にこの手法をプロダクションレベルで実現するにはHome.jsに必要なモジュールを、サーバーから返されるファイルにすべてバンドルする必要があります。

幸い、遅延ダウンロードを実現するためにモジュールをバンドルする機能を持ったツールはいくつもあります。ここでは、WebPackのコード分割システムを使ってみましょう:

m.route(document.body, "/", {
    "/": {
        onmatch: function() {
            // WebPackの非同期コード分割を使用
            return new Promise(function(resolve) {
                require(['./Home.js'], resolve)
            })
        },
    },
})

License: MIT. © Leo Horie.