11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

create-react-app で掲示板アプリを開発して ASP.NET Core 上で動かす

Last updated at Posted at 2018-08-21

はじめに

ASP.NET Core で シングルページアプリケーションを開発する方法をググると、既存の ASP.NET Core の SPA プロジェクトテンプレートを使う方法と、 SPA と Web API を別々のドメインで提供して CORS するサンプルは出てきますが、create-react-appで開発したアプリを ASP.NET Core 上で同一ドメインで動かすサンプルが見つからなかったので自分でやってみることにしました。

create-react-app

React アプリ開発をビルド環境構築なしに始められる facebook 公式のツールです。
これを使って掲示板の SPA を開発します。

ASP.NET Core

クロスプラットフォームに対応した .NET の Web アプリケーションフレームワークです。
create-react-appで開発した掲示板アプリのクライアントへの配信と Web API の受付を行います。

開発した掲示板アプリ

image.png

サーバーサイドはモックなしで、投稿を保存するデータベース、投稿一覧取得と新規投稿用の Web API まで作りました。
クライアントサイドは**いまどきのJSプログラマーのための Node.jsとReactアプリケーション開発テクニック**を参考に開発しました。

開発環境構築

サーバーサイド

ソリューションとプロジェクト名を別々にしたいので、Visual Studio でなく、 dotnet コマンドでプロジェクトを作成します。

# .NET Core のバージョン
PS > dotnet --version
2.1.400
# 作業フォルダ
PS > mkdir BbsApp
PS > cd BbsApp
# ソリューションファイルの作成
PS > dotnet new sln
# API サーバーのプロジェクトを作成
PS > dotnet new webapi -o ./BbsServer
# プロジェクトをソリューションに追加
PS > dotnet sln add ./BbsServer

Startup.csの編集

HTML/CSS や JavaScript をクライアントサイドに提供できるようにします。
静的ファイルはデフォルトではプロジェクト直下のwwwrootフォルダに配置する規約があります。

wwwrootからクライアントへの静的ファイルを提供するにはBbsServer/Startup.csを以下のように編集します。

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
    if (env.IsDevelopment()) {
        app.UseDeveloperExceptionPage();
    } else {
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseMvc();
+   app.UseStaticFiles();
}

launchsettings.jsonの編集

デバッグでブラウザを立ち上げた際にデフォルトでアクセスする URL を設定します。
launchsettings.jsonlaunchUrlindex.htmlに変更します。

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    略
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
-     "launchUrl": "api/values",
+     "launchUrl": "index.html",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "BbsServer": {
      略
    }
  }
}

クライアントサイド

create-react-app で react アプリケーションを作成

ソリューションファイル(BbsApp.sln)と同じディレクトリに React のプロジェクトを作ります。
(作る場所はどこでもいいのですが、BbsServerプロジェクトと同じフォルダ(BbsServer.csprojのあるf階層)に作成すると、BbsServer をVisual Studio でビルドする際にクライアントサイドもビルドしようとしてエラーを吐くので注意が必要です。)

PS > cd BbsApp
# TypeScript が好きなので、--scripts-version=react-scripts-ts を指定しました
PS > create-react-app bbs-client --scripts-version=react-scripts-ts

ビルドした静的ファイルの配置先を変更

React アプリを ASP.NET Core で動かしたいので、ビルドした静的ファイルの配置先をBbsApp/BbsServer/wwwrootに変更します。

PS > cd bbs-client
# アプリの設定を create-react-app の管理下から外す
PS > yarn eject

ejectconfig/Path.jsが作成されるので、appBuildの値を書き換えます。
BbsServer/wwwrootを相対パスで指定します。

// config after eject: we're in ./config/
module.exports = {
  dotenv: resolveApp(".env"),
- appBuild: resolveApp("build"),
+ appBuild: resolveApp("../BbsServer/wwwroot"),
  /* 略 */
};

React アプリのビルドと確認

React アプリをビルドしてwwwrootに配置できているかを確認します。

PS > yarn build
yarn run v1.7.0
$ node scripts/build.js
Creating an optimized production build...
Starting type checking and linting service...
Using 1 worker with 2048MB memory limit
ts-loader: Using typescript@3.0.1 and C:\Users\sa500\Desktop\qiita-article\BbsApp\bbs-client\tsconfig.prod.json
Compiled successfully.

File sizes after gzip:

  36.07 KB  wwwroot\static\js\main.70f62b72.js
  300 B     wwwroot\static\css\main.29266132.css

The project was built assuming it is hosted at the server root.
You can control this with the homepage field in your package.json.
For example, add this to build it for GitHub Pages:

  "homepage" : "http://myname.github.io/myapp",

The ..\BbsServer\wwwroot folder is ready to be deployed.
You may serve it with a static server:

  yarn global add serve
  serve -s ..\BbsServer\wwwroot

Find out more about deployment here:

  http://bit.ly/2vY88Kr

Done in 7.47s.

Visual Studio でBbsApp.slnを開くと、ソリューションエクスプローラーでwwwrootフォルダにビルドした出力が配置されていることが確認できます。

Visual Studio でデバッグするとhttps://localhost:<port#>/index.htmlにアクセスされ、wwwroot/index.htmlが配信されるのでブラウザに以下の画面が表示されます。

image.png

サーバーサイドの開発

エンティティの作成

投稿内容を表すエンティティを作成します。

BbsServerプロジェクトにModelsフォルダを追加します。
ModelsフォルダにPostItem.csを追加します。
モデルは DB のレコード、各プロパティはカラムのイメージです。
Idは「設定より規約」で自動的に主キーになります。
[Required]属性をつけると必須(DBではnot null)になります。

using System;
using System.ComponentModel.DataAnnotations;

namespace BbsServer.Models {
    public class PostItem {
        // プライマリーキー
        public int Id { get; set; }
        // 投稿者名
        [Required]
        public string Name { get; set; }
        // 本文
        [Required]
        public string Body { get; set; }
        // 投稿日時
        [Required]
        public DateTime PostDateTime { get; set; }
    }
}

データベースコンテキストの作成

アプリからデータベースへのアクセスはデータベースコンテキストを使って行います。
ModelsフォルダにBbsContext.csを作成します。
データベースコンテキストはMicrosoft.EntityFrameworkCore.DbContextを継承します。
イメージとしてはBbsContextがスキーマ、DbSet<PostItem> PostItemsがテーブルです。

using Microsoft.EntityFrameworkCore;

namespace BbsServer.Models {
    public class BbsContext : DbContext {
        public BbsContext(DbContextOptions<BbsContext> options)
            : base(options) { }

        public DbSet<PostItem> PostItems { get; set; }
    }
}

DI コンテナーに データベースコンテキストを登録する。

ASP.NET Core には標準で DI コンテナーが組み込まれています。
DI コンテナーにBbsContextを登録すると、コンストラクタの引数にBbsContextを持つクラスに自動的にBbsContextのインスタンスが渡されます。

また、データベースはSQL Server localdb(ローカルで動く簡易データベース)を使うことにします。
IConfiguration.GetConnectionStringappsetting.josnから接続文字列を取得するように設定します。
接続名はコンテキスト名と同じBbsContextとします。

Startup.csを以下のように編集します。

+   using BbsServer.Models;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc;
+   using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;

    namespace BbsServer {
        public class Startup {
            /* 略 */
            public void ConfigureServices(IServiceCollection services) {
                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
+               services.AddDbContext<BbsContext>(options => {
+                   options.UseSqlServer(Configuration.GetConnectionString(nameof(BbsContext)));
+               });
            }
            /* 略 */
        }
    }

データベース接続文字列の設定

appsettings.jsonから接続文字列を読み取るようにしたので、接続文字列を追加します。
Startup.csで設定した通り、SQL Server localdbを使用、接続名はBbsContext、スキーマもBbsContextとします。

{
  "Logging": {
    
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "BbsContext": "Server=(localdb)\\mssqllocaldb;Database=BbsContext;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

データベースのマイグレーション

まず、マイグレーションファイルを作成します。
マイグレーションファイルは データベースの設計図です。
マイグレーションファイルを実行すると、その内容でデータベースを生成したり、変更を適用してくれます。

PMC(パッケージマネージャーコンソール)に以下のコマンドを入力します。
最初なのでマイグレーション名はInitialCreateとしました。

PM> Add-Migration InitialCreate
Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 2.1.1-rtm-30846 initialized 'BbsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
To undo this action, use Remove-Migration.

プロジェクトにMigrationsフォルダが追加され、3つのファイルが作成されます。
ファイルの内容については 移行 - EF Core | Microsoft Docs に詳しく載っています。

実際にデータベースを作成します。
Update-Databaseコマンドを入力します。

PM> Update-Database
# 略
Applying migration '20180818034025_InitialCreate'.
Microsoft.EntityFrameworkCore.Migrations[20402]
      Applying migration '20180818034025_InitialCreate'.
Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [PostItems] (
          [Id] int NOT NULL IDENTITY,
          [Name] nvarchar(max) NOT NULL,
          [Body] nvarchar(max) NOT NULL,
          [PostDateTime] datetime2 NOT NULL,
          CONSTRAINT [PK_PostItems] PRIMARY KEY ([Id])
      );
# 略
Done.

Create tableが実行されデータベースが作成されました。

表示 > SQL Server オブジェクトエクスプローラーから確認すると実際に DB が作成されています。
dbo.postitems.PNG

コントローラーの作成

Http リクエストを受け付けるためコントローラーを作成します。

ContorollersフォルダのvaluesController.csは不要なので削除します。
ContorollersフォルダにBbsController.csを作成します。
コントローラーはMicrosoft.AspNetCore.Mvc.ControllerBaseを継承します。
※普通にファイルを追加するのと変わりませんがスキャフォールディングすることもできます。
Contorollersを右クリック > 追加 > コントローラー > API コントローラー -空を選択します。

[Route("api/[controller]")]属性で WebAPI の URL を指定します。
[controller]は自動的にコントローラー名が割り当てられます。
今回の場合、https://localhost:<port#>/api/bbsでアクセスできます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace BbsServer.Controllers {
    [Route("api/[controller]")]
    [ApiController]
    public class BbsController : ControllerBase {
    }
}

データベースにアクセスするために、コンストラクタを作成し、引数にBbsContextを追加します。
DI コンテナーからBbsContextのインスタンスが自動で渡されます。

using BbsServer.Models;
using Microsoft.AspNetCore.Mvc;

namespace BbsServer.Controllers {
    [Route("api/[controller]")]
    [ApiController]
    public class BbsController : ControllerBase
    {
        // コンストラクタを通じて受け取った BbsContext を保持
        BbsContext BbsContext { get; }

        public BbsController(BbsContext bbsContext) {
            BbsContext = bbsContext;
        }
        /*
         * コントローラーから直接 DB にアクセスするのは良くない設計なので、
         * 本来はサービスクラスを作成してそれを呼んだ方が良い。
         * 大規模なものならリポジトリークラスを作成しデータベースへのアクセスは
         * Controller -> Service -> Repository -> DB のように行う
         */
    }
}

投稿一覧を取得するGetメソッドと新しく投稿するPostメソッドを追加します。
[HttpGet] [HttpPost]属性をつけることで、受け付ける Http リクエストメソッドを指定できます。

投稿一覧取得Getは先に投稿されたものから順に(投稿日時の昇順)で取得できるようにします。

新規投稿Postはサーバー側の投稿日時を設定し DB に保存します。
その際、Idは自動で採番(インクリメント)されます。

using BbsServer.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

namespace BbsServer.Controllers {
    [Route("api/[controller]")]
    [ApiController]
    public class BbsController : ControllerBase {
        BbsContext BbsContext { get; }

        public BbsController(BbsContext bbsContext) {
            BbsContext = bbsContext;
        }

        // GET api/bbs
        [HttpGet]
        public ActionResult<IEnumerable<PostItem>> Get() {
            var postItems = BbsContext.PostItems
                .OrderBy(postItem => postItem.PostDateTime)
                .ToList(); // ToList でクエリが実行されるため ToList は必要
            return postItems;
        }

        // POST api/bbs
        [HttpPost]
        public ActionResult<PostItem> Post([FromBody] PostItem postItem) {
            postItem.PostDateTime = DateTime.Now;
            BbsContext.PostItems.Add(postItem);
            BbsContext.SaveChanges();
            return postItem;
        }
    }
}

Web API を実際に使ってみます。Visual Stduio でデバック実行しサーバーを立ち上げます。
クライアントサイドはまだできていないので Postman で Http リクエストを送ってみます。

・POSTMAN の設定と入力内容
Http メソッド : POST
URL : https://localhost:<port#>/api/bbs
Body : { "name": "nossa", "body": "はじめての投稿!" }

first_post.PNG

投稿内容がレスポンスとして帰ってきているのが確認できます。

2回目の投稿でIdがインクリメントされているのが確認できます。
second_post.PNG

投稿一覧も取得できました。投稿日時の昇順になっています。
get.PNG

クライアントサイドの開発

React の開発は VSCode で行いました。

ユーザー定義型の宣言

先にこのアプリで使うユーザー定義型を宣言しておきます。
src直下にtypes.tsを作成します。

src/types.ts
// 投稿内容
export interface IPostItem {
  id: number;
  name: string;
  body: string;
}
// 書き込みフォームの State
export interface IBbsFormState {
  name: IPostItem["name"];
  body: IPostItem["body"];
}
// 書き込みフォームの Props
export interface IBbsFormProps {
  onPost: () => void;
}
// アプリの state
export interface IBbsAppState {
  items: IPostItem[];
}

スタイルの定義

スタイルも先に定義しておきます。
textAlignの値はcenter rightなどに固定されているので、TypeScript で 指定する場合、キャストが必要です。

src/styles.ts
const styles = {
  form: {
    backgroundColor: "#F0F0F0",
    border: "1px solid silver",
    padding: 12
  },
  h1: {
    backgroundColor: "blue",
    color: "white",
    fontSize: 60,
    padding: 12
  },
  right: {
    // textAlign の値の文字列はキャストしておかないと tsx のstyle に渡せずコンパイルが通りません。
    textAlign: "right" as "right"
  }
};
export default styles;

書き込みフォームの作成

書き込みフォームを作成します。
src直下にBbsForm.tsxを作成します。

import * as React from "react";
import styles from "./styles";
import { IBbsFormProps, IBbsFormState } from "./types";

class BbsForm extends React.Component<IBbsFormProps, IBbsFormState> {
  constructor(props: IBbsFormProps) {
    super(props);
    this.state = { body: "", name: "" };
    this.nameChanged = this.nameChanged.bind(this);
    this.bodyChanged = this.bodyChanged.bind(this);
    this.post = this.post.bind(this);
  }

  // 名前が変更された時、state に反映する
  public nameChanged(e: React.FormEvent<HTMLInputElement>) {
    this.setState({ name: (e.target as HTMLInputElement).value });
  }

  // 本文が変更された時、state に反映する
  public bodyChanged(e: React.FormEvent<HTMLInputElement>) {
    this.setState({ body: (e.target as HTMLInputElement).value });
  }

  // 投稿処理。投稿後、コールバック this.props.onPost を実行する。
  public async post() {
    const headers = {
      Accept: "application/json",
      "Content-Type": "application/json"
    };
    const body = JSON.stringify(this.state);
    await fetch("api/bbs", {
      method: "POST",
      headers,
      body,
    });
    this.props.onPost();
  }

  public render() {
    return (
      <div style={styles.form}>
        名前: <br />
        <input type="text" value={this.state.name} onChange={this.nameChanged} />
        <br />
        本文: <br />
        <input type="text" value={this.state.body} size={60} onChange={this.bodyChanged} />
        <button onClick={this.post}>発言</button>
      </div>
    );
  }
}
export default BbsForm;

アプリの本体の作成

アプリの本体を作成します。

src/BbsApp.tsx
import * as React from "react";
import BbsForm from "./BbsForm";
import styles from "./styles";
import { IBbsAppState, IPostItem } from "./types";

class BbsApp extends React.Component<{}, IBbsAppState> {
  constructor(props: {}) {
    super(props);
    this.state = {
      items: []
    };
    this.GetPostItems = this.GetPostItems.bind(this);
  }

  // コンポーネント描画後、投稿内容一覧を取得
  public async componentDidMount() {
    await this.GetPostItems();
  }

  // 投稿内容の一覧を取得し、state に反映
  public async GetPostItems() {
    const response: Response = await fetch("api/bbs", {
      method: "GET"
    });
    if (response.status === 200) {
      const postItems: IPostItem[] = await response.json();
      this.setState({ items: postItems });
    }
  }

  public render() {
    // 投稿内容一覧の tsx を作成
    const items = this.state.items.map(item => (
      <li key={item.id}>
        {item.name} - {item.body}
      </li>
    ));
    return (
      <div>
        <h1 style={styles.h1}>掲示板</h1>
        {/* コールバックとして投稿内容一覧取得処理を渡す */}
        <BbsForm onPost={this.GetPostItems} />
        <p style={styles.right}>
          <button onClick={this.GetPostItems}>再読込</button>
        </p>
        <ul>{items}</ul>
      </div>
    );
  }
}
export default BbsApp;

続いて、src/index.tsxReactDom.Renderする tsx を<App>から<BbsApp>に変更します。

掲示板アプリを使ってみる

まず、React アプリをwwwrootに配置します。
環境構築でビルドした出力先をwwwrootに変更したので以下のコマンドを入力すれば OK です。

PS > yarn build

Visual Studio を立ち上げてデバッグ実行します。
名前と本文を入れて発言ボタンを押すと投稿されます。
投稿後は画面の投稿一覧が再表示されます。
一覧表示から再表示まで他の人の投稿があった場合、それも反映されます。

最後にもう一度スクリーンショットを張っておきます。

image.png

11
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?