BlazorでSPAするぞ!(6) - Routing -正式版対応済み

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

今回はルーティングについて。
URLに対するページ(コンポーネント)のルーティング(マッチング)ですね。

1. ルーティング(Routing)

サンプルプロジェクトからルーティングを学んでいきたいと思います。
まず、dotnet new blazorwasm コマンドでプロジェクトを作成します。

dotnet new blazorwasm -o route_blazor

自動生成コードには、ルーティング実装も ある程度組み込まれています。

dotnet run で実行し、http://localhost:5000 にアクセスします。
これは「/」(ルート)へのアクセスになります。
「/」に対するルーティング設定は、「Pages/Index.razor」となっています。

f:id:daigo-knowlbo:20190502144641p:plain

サイドメニューから「Counter」をクリックします。
ブラウザのURLが http://localhost:5000/counter になりました。
「/counter」へのアクセス、そして「/counter」へのルーティング設定は「Pages/Counter.razor」となります。

f:id:daigo-knowlbo:20190502144739p:plain

同様にサイドメニューから「Fetch data」をクリックします。
ブラウザのURLが http://localhost:5000/fetchdata になりました。
「/fetchdata」へのアクセス、そして「/fetchdata」へのルーティング設定は「Pages/FetchData.razor」となります。

f:id:daigo-knowlbo:20190502144720p:plain

1.1. @Page

「/」「/counter」「/fetchdata」のような各URLに対する .razor ファイルのルーティングは、「@Page」ディレクティブで設定されます。
自動生成された3つのコンポーネント(Pages配下の.razorファイル)の定義は、それぞれ以下の通りです。

# Pages/Index.razor #

@page "/"
...省略
# Pages/Counter.razor #

@page "/counter"
...省略
# Pages/FetchData.razor #

@page "/fetchdata"
...省略

1.2. ユーザ一覧画面を実装する場合

では、新規に「ユーザ一覧画面」を追加する想定の実装例を見てみましょう。
ルーティング設定するURLは「/users/list」とします。
.razorコンポーネントは「Pages/UserList.razor」ファイルで定義します。
以下がその定義になります。

# Pages/UserList.razor #

@page "/users/list"

ユーザ一覧画面です!!!
...省略...

1.3. URLが変わるけどSPA

自動生成されたプロジェクトを動作させるだけでもわかりますが、ページ(コンポーネント)を切り替えると、ブラウザのURLが切り替わります。
ただし、Blazorはクライアントサイドで動作しているので、対象のページ(コンポーネント)をサーバにHttpRequestするような事はせずSPAとして動作しています。
ChromeであればDevToolを表示し「Network」の監視を行えば確認することができます。

以下が実際に route_blazor を起動した後(ルートを表示した後)、「Counterメニュークリック」→「Fetch dataメニュークリック」という操作を行った際のNetwork状態です。

f:id:daigo-knowlbo:20190502145049p:plain

f:id:daigo-knowlbo:20190502145102p:plain

Counterメニュー クリック時:「favicon.ico」リクエストのみ
Fetch dataメニュー クリック時:「favicon.ico」リクエストおよび「weather.json」リクエストのみ。weather.jsonはアプリロジック的にサーバからjsonデータを明示的にリクエストしているため。

2. <Router>の初期化

@Pageディレクティブ定義によるルーティング設定方法を先に説明しましたが、アプリケーションでは<Router>を初期化しておく必要があります。
ルーティングを有効化する初期化処理は「/App.razor」で行います。
自動生成されたプロジェクトでも、以下のように既に実装が行われています。

# /App.razor #

<Router AppAssembly="typeof(Program).Assembly" />

<Router>を定義し、AppAssembly属性に「typeof(Program).Assembly」を設定します。
Routerクラスは、「Microsoft.AspNetCore.Components.Routing」名前空間で定義されています。

2.1. 無効ルーティング時の表示

定義されたルーティングにマッチしなかった場合(フォールバックした場合)に表示するコンテンツを、「Router - NotFound属性」で指定することができます。

# /App.razor #

...
<NotFound>
  <LayoutView Layout="@typeof(MainLayout)">
    <p>Sorry, there's nothing at this address.</p>
  </LayoutView>
</NotFound>
...

「<p>Sorry, there's nothing at this address.</p>」部分は任意のコンポーネントに置き換えることも可能です。
以下のように「Shared/FallbackPage.razor」ページを用意して、App.razorのNotFountコンテンツとして指定します。

# Shared/FallbackPage.razor #

@DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")
<p>無効なパスが要求されました;;</p>
# App.razor #

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <FallbackPage />
        </LayoutView>
    </NotFound>
</Router>

dotnet runで実行し、http://localhost:5000/invalid にアクセスしてみます。「/invalid」はどのコンポーネントにもルーティングされていません。
以下のように、フォールバックページ(Shared/FallbackPage.razor)が表示されました。

f:id:daigo-knowlbo:20200524160025p:plain

RouterのFallbackComponent属性が未設定の場合、無効なURLがアクセスされると、以下のように「Loading...」画面で止まってしまいます。

f:id:daigo-knowlbo:20200524160041p:plain

Chromeの「DevTools - Console」を確認すると、WASMのエラーが多数出力された状態で止まっていることが確認できます。

f:id:daigo-knowlbo:20200524160110p:plain

2.2. 複数のルート設定

1つのコンポーネント(ページ)には、複数のルーティング定義を行うことができます。
つまり @Page を複数設定可能という事です。
先程の「UserList.razor」に対して、2つのURLルーティング設定を行った例が以下です。

# Pages/UserList.razor #

@page "/users/list"
@page "/employees/list"

ユーザ一覧画面です!!!

3. NavLinkコンポーネント

ルーティング設定された各コンポーネント(ページ)の表示切替は <a href="/xxx"> タグで可能です。
<a>タグをラップしたコンポーネントとして「NavLinkコンポーネント(Microsoft.AspNetCore.Components.Routing名前空間)」が用意されています。
サンプルプロジェクトでは、左サイドのメニューを「Shared/NavMenu.razor」で実装しており、ここでNavLinkコンポーネントを利用しています。

# Shared/NavMenu.razor #

<div class="top-row pl-4 navbar navbar-dark">
  <a class="navbar-brand" href="">route_blazor</a>
  <button class="navbar-toggler" onclick="@ToggleNavMenu">
    <span class="navbar-toggler-icon"></span>
  </button>
</div>

<div class="@NavMenuCssClass" onclick="@ToggleNavMenu">
  <ul class="nav flex-column">
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
        <span class="oi oi-home" aria-hidden="true"></span> Home
      </NavLink>
    </li>
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="counter">
        <span class="oi oi-plus" aria-hidden="true"></span> Counter
      </NavLink>
    </li>
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="fetchdata">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
      </NavLink>
    </li>
    <li>
      
    </li>
  </ul>
</div>

@code {
  bool collapseNavMenu = true;

  string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

  void ToggleNavMenu()
  {
    collapseNavMenu = !collapseNavMenu;
  }
}

3.1. href属性

<a>タグと同様に「href属性」でURLを設定します。この「URL」と「.razprで定義された@Page値」によってルーティング先コンポーネントが決定されます。

3.2. 「active」CSSクラス

NavLinkコンポーネントの特徴(機能の1つ)として、「href属性値」と「現在のURL」がマッチした場合、「active」CSSクラスが付与される というものがあります。

以下は「現在のURLが /counter」なので「Counterメニューにactive cssクラス」が付いています。
加えて、NavLinkは<a>タグとしてレンダリングされていることが確認できます。

f:id:daigo-knowlbo:20190502145212p:plain

f:id:daigo-knowlbo:20190502145225p:plain

Match属性

NavLinkコンポーネントには「Match属性(Microsoft.AspNetCore.Components.Routing名前空間のNavLinkMatch列挙型)」というものがあります。
href値とURLのマッチングルールを設定します。
以下の2つの列挙値があります。

  • NavLinkMatch.All
    URLが完全一致した場合に active CSSクラスが適用されます。
    ※ href=/usersに対しては、URL=/users はマッチするが、URL=/users/list はマッチしない
  • NavLinkMatch.Prefix
    URLのプレフィクスが一致した場合に active CSSクラスが適用されます。
    ※ href=/usersに対しては、URL=/users も URL=/users/list もマッチとして扱われる

4. ルートパラメータ

ルーティングのURLにパラメータを含めることができます。
それらのパラメータ値は、ルーティング先のコンポーネントで受け取ることができます。

Pages/Index.razorコンポーネントでルート パラメータを受け取るようにしたいと思います。

4.1. @page ディレクティブにパラメータ定義を追加

ルートパラメータは @Page ディレクティブで設定を行います。
Index.razorは「/」(ルート)にルーティングされています。
「/【Message】」といった形式でパラメータを受け取るように設定します。

# Pages/Index.razor #

@page "/"
@page "/{Message}"

<h1>Hello, world!</h1>

your message is '@Message'
<br />

@code {
  private string _Message = "None";

  [Parameter]
  protected string Message {
    get {
      return this._Message;
    }
    set { 
      this._Message = System.Net.WebUtility.UrlDecode(value);
    }
  }
}

上記のように、ルートパラメータ定義は「{【パラメータ名】} 」の形式をとります。
Messageは、コードブロックで[Parameter]属性を付与したプロパティとして定義します。
ここでは「%20(スペース)」などのURLエンコードされた文字列パラメータを、URLデコードしてMessageプロパティに保持するために、getter / setterを定義し setterではUrlDecode()処理を行っています。

dotnet run コマンドで実行し、以下のURLをブラウザでアクセスします。

http://localhost:5000/Blazor%20is%20new%20SPA%20framework

URLパラメータを受け取り、画面に表示することが出来ました。

f:id:daigo-knowlbo:20190502145250p:plain

4.2. ルートパラメータの型指定

ルートパラメータは厳密な型を指定することができます。
Pages/Counter.razorコンポーネントに、CurrentCountの初期値をint型ルートパラメータとして受け取るような実装を追加したいと思います。
以下のように「{【パラメータ名】:【型名】}」の形式でルーティング設定を記述します。
コードブロック側も、[Parameter]属性を適用するために、CurrentCountを 変数 から setterを持つプロパティ に修正しています。

# Pages/Counter.razor #

@page "/counter"
@page "/counter/{currentCount:int}"

<h1>Counter</h1>

<p>Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
  [Parameter]
  public int CurrentCount {get; set; } = 0;

  void IncrementCount()
  {
    this.CurrentCount++;
  }
}

http://localhost:5000/counter/100」にアクセスした実行画面は以下です。

f:id:daigo-knowlbo:20190502145309p:plain

5. まとめ

ルーティングも従来のASP.NET Core等の仕組みと基本思想は同じくしているので、スムーズに理解することができたました。
次の投稿では、DI(Dependency Injection)を見ていこうと思います。

次は ryuichi111std.hatenablog.com