-
Notifications
You must be signed in to change notification settings - Fork 0
MS_ASPNETCoreReactTemplate
- 戻る(ASP.NET Core SPAテンプレート)
- ASP.NET Core Angularテンプレート
- ASP.NET Core React.jsテンプレート
- ASP.NET Core React+Reduxテンプレート
https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/
※ JavaScript Services も流行ってないので削除しました。
IDE に標準のテンプレートとして表示されなくなっている(dotnet new コマンドには残っている)。
補足(サンプルは削除されたが、解説は今も読む価値がある): サンプル
コード自体は削除されているが、
本ページの「詳細」節は React の構造を丁寧に追っており、
教材として今も有効である。【今も通用する部分】 ・エントリ ポイント → ルーティング → コンポーネント という階層 ・state と再描画の関係 ・API 呼び出しと loading 状態の扱い 【古くなった部分】 ・webpack 前提(現在は Vite) ・クラス コンポーネント(現在は関数コンポーネント + Hooks)★ ・react-router v4 の書き方(現在は v6) ・isomorphic-fetch(現在はブラウザ標準の fetch) ・ASP.NET Core の Razor View が土台(現在は index.html)各節で、現在の書き方を併記する。
ReactTemplate
├ ClientApp
| ├ components
| | ├ Counter.tsx
| | ├ FetchData.tsx
| | ├ Home.tsx
| | ├ Layout.tsx
| | └ NavMenu.tsx
| ├ css
| | └ site.css
| ├ boot.tsx
| └ routes.tsx
├ Controllers
| ├ HomeController.cs
| └ SampleDataController.cs
├ Views
| ├ Home
| | └ Index.cshtml
| ├ Shared
| | ├ _Layout.cshtml
| | └ Error.cshtml
| ├ _ViewImports.cshtml
| └ _ViewStart.cshtml
├ wwwroot
| └ favicon.ico
├ .gitignore
├ appsettings.Development.json
├ appsettings.json
├ npm-shrinkwrap.json
├ package.json
├ Program.cs
├ ReactTemplate.csproj
├ Startup.cs
├ tsconfig.json
├ webpack.config.js
└ webpack.config.vendor.js
補足(この構成が示す設計と、現在の構成): 1 つのプロジェクトに
.NET と React が同居している——これが当時のテンプレートの特徴である。【当時(このテンプレート)】 ReactTemplate.csproj ← 1 つのプロジェクト ├ Controllers / Views … .NET 側 ├ ClientApp … React 側 └ package.json … npm も同じ階層 ・dotnet build すると webpack も走る(MSBuild のターゲットで連携) ・【1 つのプロジェクトで完結する】= 当時の売り 【現在(.NET 8 以降のテンプレート)】 MyApp.sln ├ MyApp.Server/ … ASP.NET Core(Web API のみ) └ myapp.client/ … 【React(.esproj)】★ ├ package.json ├ vite.config.ts └ src/ ・【2 つのプロジェクトに分離された】 ・[.esproj(JavaScript・TypeScriptプロジェクトシステム)] 参照この変化が、ASP.NET Core SPAテンプレート の
原文が主張していた「フロントとバックを分離せよ」の実現である。
npm-shrinkwrap.jsonについて:
現在はpackage-lock.jsonが標準である
(npm 5 以降。shrinkwrapは明示的に発行する場合のみ)。
どちらも依存の版を固定するためのファイルで、
必ずソース管理に含める
(NuGetパッケージの依存関係の肥大化 と同じ問題意識)。
Node.js、React、TypeScript、webpack からなる。
- Node.js(
DNET_NodeJs.md) - React(
DNET_React.md) - TypeScript
- webpack(
DNET_Webpack.md)
補足(各要素の役割と現況):
要素 役割 現況 Node.js ビルド ツールの実行環境(本番では不要) 現役。LTS 版を使う React UI ライブラリ 現役。ただし書き方が変わった(Hooks) TypeScript 静的型付け 現役。むしろ標準化した webpack モジュール バンドラ Vite に置き換わりつつある ★ 【webpack → Vite への移行の理由】 ・開発サーバの起動が【桁違いに速い】 webpack: 全部バンドルしてから起動(大規模で数十秒) Vite: ESM をそのまま配信、変更分だけ変換(1 秒未満) ・設定が単純(webpack.config.js の複雑さが有名だった) ・本番ビルドは Rollup(実績のあるバンドラ)原文の構成に
webpack.config.jsとwebpack.config.vendor.jsの
2 つがあるのは、ベンダー ライブラリ(React 本体など)と
アプリ コードを別々にバンドルするためである。【当時の工夫】 vendor(React, react-dom 等)… ほぼ変わらない → 長期キャッシュ app(自分のコード) … 頻繁に変わる → 分けることで、アプリ更新時に vendor を再取得させない 【現在】 Vite / webpack 5 が【自動でコード分割する】ため、 手で分ける必要はほぼない
-
-
ヘッダ
@{ ViewData["Title"] = "Home Page"; }
-
フッタ
@section scripts { <script src="~/dist/main.js" asp-append-version="true"></script> } -
_Layout.cshtml
https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/Views/Shared/_Layout.cshtml
-
-
React 部位
<div id="react-app">Loading...</div>
補足(Razor View を土台にする方式の意味): 現在の SPA は
静的なindex.htmlを土台にするのが普通だが、
当時は Razor View(.cshtml)を土台にしていた。【Razor を土台にする利点(当時)】 ・サーバ側の値を初期状態として埋め込める <div id="react-app" data-user="@User.Identity.Name"> ・認証・認可を .NET 側で掛けられる ・asp-append-version でキャッシュ バスティングが効く ★ 【欠点】 ・フロントを独立して配信できない(CDN に置けない) ・.NET を起動しないとフロントが見られない ・フロント担当者が .NET の知識を要求される
asp-append-version="true"は
ASP.NET の BundleConfig で述べた
キャッシュ バスティングである。<script src="~/dist/main.js" asp-append-version="true"></script> <!-- → /dist/main.js?v=Ynv7Ur0kEz…(内容のハッシュ) -->現在は Vite がファイル名にハッシュを埋め込むため、
この機能は不要になった。dist/assets/index-4f8a2b1c.js ← ファイル名自体にハッシュ
<div id="react-app">Loading...</div>のLoading...は、
JS が読み込まれるまでの間に表示される文字である。
現在も同じ手法が使われる(あるいはスケルトン UI やスピナー)。
「Index.cshtml(土台)」の「<div id="react-app">」の部分に、
routes のコンテンツをロードする。
Node.js のモジュールのインポート
-
CSS
import './css/site.css';
-
ホットロード
AppContainer タグは、React 拡張プロダクトの独自タグ。{ AppContainer } from 'react-hot-loader';
-
URL と DOM を仮想的に紐付け(HTML5 History を内部で使用)。
BrowserRouter タグは、React 拡張プロダクトの独自タグ。{ BrowserRouter } from 'react-router-dom';
- ReactDOM.render メソッドを使用して、document.getElementById('react-app') で
Index.cshtml の <div id="react-app"> 要素に AppContainer と、
BrowserRouter を適用して、描画する。
function renderApp() {
// This code starts up the React app when it runs in a browser. It sets up the routing
// configuration and injects the app into a DOM element.
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href')!;
ReactDOM.render(
<AppContainer>
<BrowserRouter children={ routes } basename={ baseUrl } />
</AppContainer>,
document.getElementById('react-app')
);
}補足(現在の書き方): この部分はAPI が変わったので、
対応を示しておく。// 現在(React 18 以降 + react-router v6 + Vite) import React from 'react'; import { createRoot } from 'react-dom/client'; // ← ReactDOM.render は非推奨 import { BrowserRouter } from 'react-router-dom'; import App from './App'; import './css/site.css'; const container = document.getElementById('react-app')!; createRoot(container).render( <React.StrictMode> <BrowserRouter> <App /> </BrowserRouter> </React.StrictMode> );
当時 現在 ReactDOM.render(...)createRoot(container).render(...)(React 18)react-hot-loaderのAppContainer不要(Vite の HMR が標準で効く) <BrowserRouter children={routes} /><BrowserRouter><App /></BrowserRouter>basenameを<base>タグから取得basenameを直接指定(または不要)
<base href>からベース URL を取るという当時の工夫は、
アプリが仮想ディレクトリ配下に配置される場合への対応である
(/myapp/に置かれてもルーティングが壊れないようにする)。
現在は Vite のbase設定で同じことを行う。// vite.config.ts export default defineConfig({ base: '/myapp/' });
document.getElementsByTagName('base')[0]の!(非 null アサーション)
は TypeScript の構文である。
「null ではないと保証する」という宣言であり、
実際に null なら実行時に落ちる。
多用すると型チェックの意味が薄れるため、
現在はオプショナル チェーンや早期リターンを使う方が良い。
-
各種コンポーネントをインポート
-
ReactRouter の定義
export const routes = <Layout>
<Route exact path='/' component={ Home } />
<Route path='/counter' component={ Counter } />
<Route path='/fetchdata' component={ FetchData } />
</Layout>;-
初期表示時の描画
- <Layout></Layout> → Layout.tsx
-
以降の描画(URL に依存)
- <Route exact path='/' component={ Home } />(Home.tsx、exact は初期表示)
- <Route path='/counter' component={ Counter } />(Counter.tsx)
- <Route path='/fetchdata' component={ FetchData } />(FetchData.tsx)
移行メモ(
exactの意味): 原文は「exact は初期表示」と
説明しているが、正確には「パスの完全一致」を要求する指定である。【exact なし】 path='/' は【前方一致】する / → 一致 ○ /counter → 一致 ○("/" で始まるため)★ 意図しない → Home と Counter が【両方表示されてしまう】 【exact あり】 完全一致のみ / → 一致 ○ /counter → 一致 ✗結果として「初期表示(ルート)のときだけ Home が出る」という
挙動になるため、原文の説明も的は外していないが、
**仕組みは「完全一致の指定」**である。現在(react-router v6)では
exactは廃止された。
既定で完全一致になり、前方一致したい場合に/*を付ける。// react-router v6 <Routes> <Route path="/" element={<Layout />}> <Route index element={<Home />} /> {/* exact 相当 */} <Route path="counter" element={<Counter />} /> <Route path="fetchdata" element={<FetchData />} /> </Route> </Routes>
当時(v4) 現在(v6) <Route component={X} /><Route element={<X />} />exact既定で完全一致(不要) <Layout>で囲む入れ子ルート + <Outlet />this.props.children<Outlet />
-
静的なレイアウト用の HTML を返す "public render() {" だけ実装される。
-
初期表示時の描画
- <NavMenu/> → NavMenu.tsx
- { this.props.children } → this.props.children で routes(ReactRouter)に
定義したコンテンツをロードする。
- ナビゲーション用の静的な HTML を返す "public render() {" だけ実装される。
- <Link>, <NavLink> コンポーネントを使用している。
補足(
<Link>を使う理由): これが SPA の要なので補っておく。<!-- ✗ 普通のリンク → ページ全体が再読込される(SPA の意味がない) --> <a href="/counter">Counter</a> <!-- ○ Link → JS がルーティングを処理。再読込しない --> <Link to="/counter">Counter</Link>【Link がやっていること】 ① クリックの既定動作を止める(preventDefault) ② history.pushState で【URL だけ書き換える】(HTML5 History API) ③ Router に通知 → 対応するコンポーネントを描画 → サーバへのリクエストが【発生しない】= 速い → URL は変わるので、戻るボタン・ブックマークが効く ★
<NavLink>は<Link>の拡張で、
現在の URL と一致するときにクラスを付ける
(メニューの「現在地」表示に使う)。これが Spa Services で述べた
MapFallbackToFileが必要になる理由でもある。
URL が/counterに変わった状態で再読込されると、
サーバに/counterが飛ぶためである。
- ホーム画面用の静的な HTML を返す "public render() {" だけ実装される。
カウントアップ処理のテスト画面の HTML を返す。
-
constructor() {
オブザーバーが constructor で初期化した this.state を監視して再描画。 -
public render() {
- onClick イベントは JSX で書いてある。ここで、incrementCounter メソッドを呼び出す。
- incrementCounter メソッドが this.setState を呼び this.state を更新すると、再描画される。
補足(state と再描画:React の中核概念): 原文の説明は
要点を押さえているが、「オブザーバーが監視して」という表現は
少し補足が要る。【正確な仕組み】 React は state を【監視していない】 setState を【呼んだこと】が再描画のきっかけになる ★ ✗ this.state.count = 5; ← 直接書き換えても再描画されない ○ this.setState({ count: 5 }); ← これで再描画される 【流れ】 setState 呼び出し → React が「この コンポーネントは古い」と印を付ける → 【仮想 DOM】を再構築 → 前回の仮想 DOM と差分を取る(Reconciliation) → 【差分だけ】実 DOM に反映する**これが「宣言的 UI」**である。
【命令的(jQuery 的)】 $('#count').text(count + 1); ← DOM を直接操作する 【宣言的(React)】 「state が count のとき、画面はこう見える」と書く → どう更新するかは React が決める → [ASP.NET Web Forms] の ViewState/ポストバックとも 発想が違う(あちらはサーバが HTML を作り直す)現在の書き方(関数コンポーネント + Hooks):
// クラス コンポーネント(当時) export class Counter extends React.Component<{}, { currentCount: number }> { constructor(props) { super(props); this.state = { currentCount: 0 }; } render() { return <button onClick={() => this.incrementCounter()}>Increment</button>; } incrementCounter() { this.setState({ currentCount: this.state.currentCount + 1 }); } } // 関数コンポーネント + useState(現在)★ export function Counter() { const [count, setCount] = useState(0); return ( <> <p>Current count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </> ); }
setCount(c => c + 1)と書く理由:・setState / setCount は【非同期にまとめて処理される】ことがある ・setCount(count + 1) を連続で呼ぶと、古い count を参照して 1 しか増えない場合がある ・【関数形式】なら、常に最新の値を受け取れる ★React 16.8(2019 年)で Hooks が導入されて以降、
新規はすべて関数コンポーネントである。
クラス コンポーネントは今も動くが、
新しい機能(Suspense、並行機能)は Hooks 前提で作られている。
WebAPI からデータを取得しリスト表示する処理のテスト画面の HTML を返す。
-
constructor() {
- constructor で this.state を初期化
- isomorphic-fetch による fetch は WebAPI 呼び出し。
- WebAPI から取得したデータを this.setState
- forecasts: リストに表示するデータ
- loading: loading 中か否か。
-
public render() {
this.state.loading を判定して描画を変えている。-
true:Loading... の表示
<p><em>Loading...</em></p>
-
false:
renderForecastsTable メソッドが返したリストの内容。
-
補足(
isomorphic-fetchとは、そして現在): 名前が特徴的なので
説明しておく。【isomorphic(同型)とは】 「ブラウザでもサーバ(Node.js)でも【同じコードが動く】」という意味 ・ブラウザ … window.fetch が標準で存在する ・Node.js … 当時は fetch がなかった(node-fetch 等が必要) → isomorphic-fetch がその差を吸収する 【なぜ必要だったか】 サーバー サイド レンダリング(SSR)で、 同じコンポーネントを Node.js 上でも実行するため ★ → [ASP.NET Core React+Reduxテンプレート] の boot-server.tsx現在は不要である。
・ブラウザ … fetch は全ブラウザで標準(IE 終了) ・Node.js … 【Node 18 以降で fetch が標準搭載】★ → isomorphic-fetch を入れる理由がなくなった現在の書き方:
export function FetchData() { const [forecasts, setForecasts] = useState<Forecast[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const ac = new AbortController(); // ← 中断できるようにする (async () => { try { const res = await fetch('/api/SampleData/WeatherForecasts', { signal: ac.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); // ★ 重要 setForecasts(await res.json()); } catch (e) { if ((e as Error).name !== 'AbortError') setError(String(e)); } finally { setLoading(false); } })(); return () => ac.abort(); // ← 後始末 }, []); if (loading) return <p><em>Loading...</em></p>; if (error) return <p role="alert">エラー: {error}</p>; return <ForecastTable forecasts={forecasts} />; }原文のサンプルに欠けている 3 点(現在なら必ず入れる):
① 【エラー処理】 fetch は【HTTP 404 / 500 でも例外を投げない】★ → res.ok を必ず確認する(C# の HttpClient と違う点) ② 【中断(クリーンアップ)】 応答が返る前に画面を離れると、 アンマウント済みのコンポーネントに setState する → 警告、あるいはメモリ リーク ③ 【loading / error / データ の 3 状態】 原文は loading の 2 状態しかない → 実務では「失敗した」状態の表示が必ず要るさらに現在は、データ取得を自前で書かずに
ライブラリに任せるのが主流である。
ライブラリ 内容 TanStack Query(React Query) キャッシュ・再取得・リトライ・重複排除を全部やる ★ SWR 同種。より軽量 RTK Query Redux Toolkit に統合されたもの // TanStack Query なら、これだけ const { data, isLoading, error } = useQuery({ queryKey: ['forecasts'], queryFn: () => fetch('/api/SampleData/WeatherForecasts').then(r => r.json()), });
- JavaScript(
DNET_JavaScript.md)- React(
DNET_React.md)
- React(
React のセカンド・ステップ(DNET_ReactSecondStep.md)
移行メモ(
create-react-appは非推奨になった): 参考リンクが挙げる
create-react-app(CRA)は、React 公式から非推奨とされた
(2023 年に公式ドキュメントから外れ、実質的にメンテナンス停止)。【現在の推奨(React 公式)】 ・Vite … 【SPA を作るならこれ】★ ・Next.js … SSR / SSG が要るなら ・Remix … 同上 ・Astro … コンテンツ主体のサイト# 現在の作り方 npm create vite@latest my-app -- --template react-ts
- Visual Studio での React と ASP.NET Core
https://learn.microsoft.com/ja-jp/visualstudio/javascript/tutorial-asp-net-core-with-react - ASP.NET Core での SPA の開発
https://learn.microsoft.com/ja-jp/aspnet/core/client-side/spa/intro
Tags: 移行, .NET開発, .NET Core, ASP.NET, ASP.NET Web API, ASP.NET SPA, JavaScript
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。