はじめに
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 の受付を行います。
開発した掲示板アプリ
サーバーサイドはモックなしで、投稿を保存するデータベース、投稿一覧取得と新規投稿用の 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.json
のlaunchUrl
をindex.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
eject
でconfig/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
が配信されるのでブラウザに以下の画面が表示されます。
サーバーサイドの開発
エンティティの作成
投稿内容を表すエンティティを作成します。
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.GetConnectionString
でappsetting.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 が作成されています。
コントローラーの作成
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": "はじめての投稿!" }
投稿内容がレスポンスとして帰ってきているのが確認できます。
2回目の投稿でId
がインクリメントされているのが確認できます。
クライアントサイドの開発
React の開発は VSCode で行いました。
ユーザー定義型の宣言
先にこのアプリで使うユーザー定義型を宣言しておきます。
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 で 指定する場合、キャストが必要です。
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;
アプリの本体の作成
アプリの本体を作成します。
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.tsx
のReactDom.Render
する tsx を<App>
から<BbsApp>
に変更します。
掲示板アプリを使ってみる
まず、React アプリをwwwroot
に配置します。
環境構築でビルドした出力先をwwwroot
に変更したので以下のコマンドを入力すれば OK です。
PS > yarn build
Visual Studio を立ち上げてデバッグ実行します。
名前と本文を入れて発言ボタンを押すと投稿されます。
投稿後は画面の投稿一覧が再表示されます。
一覧表示から再表示まで他の人の投稿があった場合、それも反映されます。
最後にもう一度スクリーンショットを張っておきます。