📖

.NET Core Blazor + MicroCMSでブログサイト作成②

2021/08/24に公開

今回は、プロジェクト作成時点のソースコードをデバッグ実行してみて、コードの役割を確認します。

ディレクトリ

テンプレートのファイル一覧

Next.jsと違い、サーバー側コードとクライアント側コードが分割されています。
(Reactの3目並べを作ろうとしていたので、プロジェクト名がSanmokuNarabeになってます...)

.Client

wwwroot
 cssやfaviconが入っています。
Pages
 ページのrazorテンプレートファイルが入っています。
Shared
 ページ共通のレイアウトやナビゲーションが入っています。
 Next.jsでいうcomponentsにあたるフォルダ(?)です。
App.razor
 コンテンツ部分としてレンダリングする大元のコンポーネントです。
 CreateReactAppにも同じ役割のApp.jsがあります。

.Server

Controllers
 クライアントからサーバーへの要求を受け取るクラスが入っています。
 MVCのContollersと同じ役割を持ちます。
Pages
 サーバーから返されるビューが入っています。
 拡張子が.cshtmlで同名の.cshtml.csファイルが付属されているので、Razor Pagesと同じ役割を持っています。
appsetting.json
 データベースに接続するときの接続文字列などが記入されます。
 Razor PagesやMVCと同様です。

デバッグ実行

「Debug」「Any CPU」「BlazorBlog.Server」「IIS Express」にして、再生ボタンをクリックします。

Home

Home

BlazorBlog.Client/Pages/Index.razor
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

「@page "/"」は"/"にアクセスしたときに表示させるページということを表しています。

「Hello World!」と「Welcome to your new app.」はそのまま表示されています。
次の「How is Blazor~」はSurvey Promptという別のコンポーネントを呼び出しています。これがどこにあるかというと、BlazorBlog.Client/Shared/SurveyPrompt.razorです。

BlazorBlog.Client/Shared/SurveyPrompt.razor
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" href="https://go.microsoft.com/fwlink/?linkid=2137916">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    // Demonstrates how a parent component can supply parameters
    [Parameter]
    public string Title { get; set; }
}

Index.razor内でSurveyPromptコンポーネントを呼び出す時、Titleというものを与えています。Reactでいうpropsに相当するものです。
Blazorではparameterなのかな?公式チュートリアルの文中にpropertyと書いてあったけどどうなんだろう。

Reactではpropsを、関数コンポーネントの引数としてobject形式で受け取って使う書き方をしましたが、Blazorでは@codeディレクティブ内に[Parameter]アノテーションをつけて受け取りを記述します。
Parameter指定した変数は必ずpublicである必要があるようです。
異なるrazorテンプレート間で値を渡すから、publicじゃないとダメです。

ParameterはPublic

Index.razorから渡されたTitle="How is Blazor working for you?"が、SurveyAttempt.razorの3行目で使われています。

Counter

Click meをクリックするたびにカウンターが1つずつ上がります。

BlazorBlog.Client/pages/Counter.razor
@page "/counter"

<h1>Counter</h1>

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

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

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

Reactではページ内での状態遷移を記述するとき、stateを使っています。
Blazorでは@code内にprivateな変数とprivateな更新用メソッドを定義します。ReactのsetStateと同じですね。
parameterと同様に@変数名で使えます。

Fetch data

一瞬Loading...と表示されて、すぐにデータテーブルが表示されます。


    👇

BlazorBlog.Client/pages/FetchData.razor
@page "/fetchdata"
@using BlazorBlog.Shared
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }
}

今後の実装に一番関わってくる部分なので、順を追って説明します。

アクセス時(Fetch dataクリック時)

まず@code内が実行されます。protected override async Task OnInitializedAsync()の行にブレークポイントをおいてデバッグ実行します。
するとFetch dataをクリックしてすぐブレークポイントにひっかかります。


     👇

     👇

このときブラウザを見ると、まだLoading...になっていません。
つまりDOMがマウントされる前に行われる処理OnInitializedAsyncに書きます。
Next.jsでいうgetServerSidePropsにあたります。素のReactのcomponentWillMountにも相当するのかな。
getStaticPropsにあたるものはBlazorにあるのか...?)

OnInitializedAsync内

Http.GetFromJsonAsyncは、クライアントからサーバーへHTTPリクエストを送り結果をJSON形式から変換した任意の型で受け取る非同期メソッドです。
受け取り型はWeatherForecast[]となっていますが、WeatherForecastクラスはどこに定義されているかというと、BlazorBlog.Shared/WeatherForecast.csです。
クライアント、サーバー両方で使用するクラスであるため、.Sharedフォルダに配置されています。

また、このクラスを使うためにテンプレート冒頭で@using BlazorBlog.Sharedをつけています。
同様にHttpモジュールを使用するために@inject HttpClient Httpをつけています。ECMAScriptだとimportだけどここでは違うので要注意。おそらくDependency Injectionを指していると思われます。

そのままステップ実行すると、データを取得できているのがわかります。

データ取得詳細

サーバーがリクエストを受信したとき、Controllerが処理を受け付けます。
BlazorBlog.Server/ControllersWeatherForecastController.csというクラスがあるので見てみましょう。

BlazorBlog.Server/Controllers/WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SanmokuNarabe.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SanmokuNarabe.Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        // 気候を表す一言
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        // データ取得ロガー(DB使用時はSQLがログ出力される)
        private readonly ILogger<WeatherForecastController> _logger;

        // コンストラクタ
        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        // GETリクエストを受け付ける
        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            // 乱数生成クラス
            var rng = new Random();
            // index:1,2,3,4,5 に対して、WeatherForecastクラスをランダムに生成して配列で返す
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

MVCとまったく同じ書き方ですね。
MVC経験者であれば特に疑問はないと思われますが、念の為に。

[ApiController]は、指定することでControllerをAPIのエンドポイントとして定義する機能を持ちます。

[Route("[controller]")]トークン置換という便利機能を使っていて、勝手に[Route("/WeatherForecast")]と同等とみなしてくれます。(👇公式サイト参照)
Token replacement in route templates [controller], [action], [area]

[HttpGet]はHTTP GETリクエストを受けたときの処理を記述するアノテーションです。public IEnumerable<WeatherForecast> Get()内でOnInitializedAsyncに返すデータを作成しています。

Loading...が表示される仕組み

前述の通り、OnInitializedAsyncは非同期メソッドです。そのためデータ取得が完了する前にOnInitializedAsyncが完了し、DOMツリーのマウントに移行します。
その結果、データが未取得の状態、すなわちforecasts == nullの状態でDOMツリーを生成することになります。
するとテンプレートのうち👇の部分が表示されます。

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}

その後、追いかけるようにデータ取得が完了するとforecastsにはデータが入りnullではなくなるので、👇の部分が表示されます。

else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

Reactで同じことをやる場合、useEffectを使うことになるかと思いますが、やっぱりHooksは説明を読んでサクっと使いこなすにはハードルが高いです。使い方を頭で分かっていても、いざ実装してみると想定通りにならず頭を抱えることがあるんじゃないかと思います。
QiitaにHooksの説明記事がたくさんあるところを見ると、多くの人が理解に力が必要だったことの裏返しに感じます。
その点Razorは口で説明するように書けるところがスバラシイと思います。

最終結果

データ取得詳細で見たデータが画面に出てきます。
乱数で生成しているので、8/18の気温と気候がミスマッチですね。火星の住人からしたらWarmかもしれませんが。

image.png

初回ロードのLoading...は何?

デバッグ開始すると、まずLoading...が表示されます。

これを表示させているのは何かというと、BlazorBlog.Client/wwwroot/index.htmlです。

BlazorBlog.Client/wwwroot/index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorBlog</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="SanmokuNarabe.Client.styles.css" rel="stylesheet" />
</head>

<body>
    <!-- これ! -->
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

CreateReactApp同様、App.razorのDOMが完成した段階で、<div id="app">Loading...</div>の中身を更新する処理が走ります。
そのため最初はLoading...ですが、少し待てばトップページが表示されます。
もし何かしらのエラー(DB接続がタイムアウト、環境変数の読み込み失敗、etc.)があれば👇の部分が表示されます(試してはないけど)。

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

このファイルは.Client側のソースコードであるため、VisualStudioのブレークポイントでは止まりません。

サンプルを触ってみて思ったこと

さすがはMicrosoft、非常にわかりやすいです。
というのも、この記事を書くにあたってBlazorのチュートリアル等をほぼ読んでいません(裏とり程度で読むことはありましたが)。
ソースコードだけを見て何がどうなっているのか一目瞭然でした。
これだけ簡単に、システマティックにSPAが作れるのであれば、むりにReact/Next.jsを使わなくてもよかったです。

次回

MicroCMSの設定、APIコールの実装をします。

Discussion