在 React 系列的第一篇文章中 React API 01 : 前端使用 React,後端使用 ASP.NET Core 的測試標準專案 ,雖然沒有特別設計新的 API,而是採用 ASP.NET Core Web API 專案範本內建的天氣控制器,這裡新建立的 React 專案,將會呼叫這個 API,並且要取得這個天氣預報的 JSON 資料,然後渲染到網頁上,不過,這裡遇到了一個問題, [CORS] ,透過在 [Promgram.cs] 檔案內,加入對 CORS 的支援,並且設定允許來自 React 應用程式所在的端口的請求,就可以解決這個問題,讓前端的 React 應用程式能夠成功呼叫後端的 API 端點,並且將回傳的資料渲染到網頁上。
這是關於 React 系列的第二篇文章,這裡想要讓使用者可以輸入一個地點與日期,然後呼叫後端的 API 端點,使用 [GET] 方法,將這兩個值作為查詢字串參數傳送給後端,後端會根據這些參數來產生對應的天氣預報資料,並將這些資料回傳給前端,最後前端會將這些資料渲染到網頁上,以表格的形式顯示出來。
接下來就來嘗試看看這樣的需求如何做到吧!
- 開啟 Visual Studio 2026
- 選擇「建立新專案」
- 在 [建立新專案] 視窗中,在右方清單內,找到並選擇「ASP.NET Core Web API」 項目
此專案範本可用於使用 ASP.NET Core 控制器或最小 API 建立 RESTful Web API,並可選擇地支援 OpenAPI和驗證
- 然後點擊右下方「下一步」按鈕
- 此時將會看到 [設定新的專案] 對話窗
- 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 [WebApiDemo]
- 然後點擊右下方「下一步」按鈕
- 接著會看到 [其他資訊] 對話窗
- 在這個對話窗內,確認使用底下的選項
- 架構:.NET 10.0 (或更新版本)
- 驗證類型:無
- 勾選 針對 HTTPS 進行設定
- 啟用 OpenAPI 支援
- 勾選 不要使用最上層陳述式 (這是我的個人習慣)
- 使用控制器
- 不要勾選 在 .NET Aspire 協調流程中登錄
- 然後點擊右下方「建立」按鈕
- 現在,已經完成了這個 ASP.NET Core Web API 專案的建立
- 在方案總管內,找到並展開 [Controllers] 資料夾
- 找到並打開 [WeatherForecastController.cs] 檔案
- 將該檔案內的程式碼全部刪除,然後將底下的程式碼貼上到該檔案內
using Microsoft.AspNetCore.Mvc;
namespace WebApiDemo.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet("GetWeatherByLocationAndDate")]
public ActionResult<IEnumerable<WeatherForecast>> GetWeatherByLocationAndDate([FromQuery] string location, [FromQuery] DateOnly date)
{
return Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = date.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)],
Location = location
}).ToArray());
}
}這裡定義了一個新的 API 端點 [GetWeatherByLocationAndDate],這個端點使用了 [HttpGet] 的屬性來指定它是一個 GET 方法的 API,並且在路由中指定了 [GetWeatherByLocationAndDate] 的路徑。這個 API 端點接受兩個查詢字串參數,分別是 [location] 與 [date],並且使用 [FromQuery] 的屬性來告訴 ASP.NET Core 從查詢字串中取得這些參數的值。
- 滑鼠右擊解決方案 [WebApiDemo],選擇「加入」>「新增專案」
- 在 [加入新專案] 視窗中,在右方清單內,找到並選擇「React 個應用程式」 項目
請注意選擇具有底下的說明項目的專案範本
TypeScript React 專案範本,透過執行 npx 的全域安裝來進行啟動載入
- 然後點擊右下方「下一步」按鈕
- 此時將會看到 [設定新的專案] 對話窗
- 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 [reactdemo]
- 然後點擊右下方「下一步」按鈕
- 然後點擊右下方「建立」按鈕
- 現在,已經完成了這個 React 個應用程式 專案的建立
- 在方案總管內,將會看到有兩個專案建立起來
- 在方案總管內,找到並展開 [reactdemo] 專案
- 找到並打開 [src] 資料夾內的 [App.tsx] 檔案
- 將該檔案內的程式碼全部刪除,然後將底下的程式碼貼上到該檔案內
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
// 定義 WeatherForecast 型別,對應後端的資料結構
interface WeatherForecast {
date: string;
temperatureC: number;
temperatureF: number;
summary: string;
location: string;
}
function App() {
const [count, setCount] = useState(0)
const [forecasts, setForecasts] = useState<WeatherForecast[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
// 使用者輸入地點與日期
const [location, setLocation] = useState('Kaohsiung')
const [date, setDate] = useState(() => new Date().toISOString().split('T')[0]) // YYYY-MM-DD
const apiBase = 'https://localhost:7074'
// 呼叫後端 GetWeatherByLocationAndDate API
const fetchWeatherByLocationAndDate = async () => {
setLoading(true)
setError('')
setForecasts([])
try {
if (!location.trim()) {
throw new Error('地點不可為空')
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
throw new Error('日期格式需為 YYYY-MM-DD')
}
const query = new URLSearchParams({
location: location.trim(),
date: date
}).toString()
const response = await fetch(`${apiBase}/weatherforecast/GetWeatherByLocationAndDate?${query}`)
if (!response.ok) {
throw new Error(`API 請求失敗: ${response.status}`)
}
const data = await response.json() as WeatherForecast[]
setForecasts(data)
} catch (err) {
setError(err instanceof Error ? err.message : '取得資料時發生錯誤')
console.error('GetWeatherByLocationAndDate 呼叫失敗:', err)
} finally {
setLoading(false)
}
}
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
{/* 自訂查詢天氣預報 */}
<div className="card" style={{ marginTop: '20px', width: '100%', maxWidth: 800 }}>
<h2>查詢指定地點與日期的天氣預報</h2>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '12px' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label htmlFor="location">地點</label>
<input
id="location"
type="text"
value={location}
onChange={e => setLocation(e.target.value)}
placeholder="輸入地點"
style={{ padding: '6px 8px' }}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<label htmlFor="date">日期 (YYYY-MM-DD)</label>
<input
id="date"
type="date"
value={date}
onChange={e => setDate(e.target.value)}
style={{ padding: '6px 8px' }}
/>
</div>
<div style={{ alignSelf: 'flex-end' }}>
<button
onClick={fetchWeatherByLocationAndDate}
disabled={loading || !location.trim() || !/^\d{4}-\d{2}-\d{2}$/.test(date)}
style={{ padding: '8px 14px' }}
>
{loading ? '查詢中...' : '取得天氣'}
</button>
</div>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
{forecasts.length > 0 && (
<div style={{ marginTop: '12px', overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 600 }}>
<thead>
<tr>
<th style={{ border: '1px solid #ddd', padding: '8px' }}>日期</th>
<th style={{ border: '1px solid #ddd', padding: '8px' }}>地點</th>
<th style={{ border: '1px solid #ddd', padding: '8px' }}>溫度 (C)</th>
<th style={{ border: '1px solid #ddd', padding: '8px' }}>溫度 (F)</th>
<th style={{ border: '1px solid #ddd', padding: '8px' }}>概況</th>
</tr>
</thead>
<tbody>
{forecasts.map((forecast, index) => (
<tr key={index}>
<td style={{ border: '1px solid #ddd', padding: '8px' }}>{forecast.date}</td>
<td style={{ border: '1px solid #ddd', padding: '8px' }}>{forecast.location}</td>
<td style={{ border: '1px solid #ddd', padding: '8px' }}>{forecast.temperatureC}</td>
<td style={{ border: '1px solid #ddd', padding: '8px' }}>{forecast.temperatureF}</td>
<td style={{ border: '1px solid #ddd', padding: '8px' }}>{forecast.summary}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{forecasts.length === 0 && !loading && !error && (
<p style={{ marginTop: '8px', color: '#666' }}>請輸入地點與日期後點擊「取得天氣」。</p>
)}
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App在這個 [App.tsx] 的程式碼中,定義了一個 WeatherForecast 的 TypeScript 介面,來對應後端 API 回傳的資料結構。接著,使用 React 的 useState Hook 來管理天氣預報資料、載入狀態和錯誤訊息。
這裡設定 [apiBase] 變數為後端 API 的基本 URL,這樣在呼叫 API 的時候就可以使用這個變數來組合完整的 API 端點 URL。另外,使用了 await fetch(${apiBase}/weatherforecast/GetWeatherByLocationAndDate?${query}) 的方式來呼叫後端的 API 端點,並且將地點與日期作為查詢字串參數傳送給後端。
另外,定義了一個 fetchWeatherForecast 的非同步函數,來呼叫後端的 API 端點,並將回傳的資料存到 forecasts 的狀態中。如果在呼叫 API 的過程中發生錯誤,會將錯誤訊息存到 error 的狀態中。在 JSX 的部分,建立了一個按鈕,當使用者點擊時會呼叫 fetchWeatherForecast 函數來獲取天氣預報資料。當資料成功獲取後,會將資料以表格的形式顯示在畫面上。如果在獲取資料的過程中發生錯誤,我們會在畫面上顯示錯誤訊息。
另外,對於使用者要輸入的欄位,這裡使用了兩個 useState 來分別管理地點與日期的值,並且在按鈕被點擊時,將這些值作為查詢字串參數傳送給後端的 API 端點。接著,也做了輸入防呆檢查,確保地點不可為空,日期的格式必須為 YYYY-MM-DD,只有在這些條件都滿足的情況下,按鈕才會被啟用。
- 在這個方案內,擁有兩個專案,分別是 [WebApiDemo] 與 [reactdemo]
- 前者是 ASP.NET Core Web API 專案,後者是 React 的前端專案
- 因此,我們需要設定 Visual Studio 來同時啟動這兩個專案,才能在開發過程中同時測試前後端的功能
- 在方案總管內,右擊方案 [WebApiDemo],選擇 [設定啟動專案]
- 在 [方案 'ReactWebApi' 屬性頁] 對話窗內,選擇 [多個啟動專案] 的選項
- 在下方的專案列表內,找到 [WebApiDemo] 與 [reactdemo] 這兩個專案
- 將這兩個專案的 [動作] 欄位都設定為 [啟動]
- 然後點擊右下方的 [確定] 按鈕,來儲存這個設定

- 這個錯誤訊息表示了,因為 CORS 的政策限制,導致前端的 React 應用程式無法成功呼叫後端的 API 端點
- 為了修正這個問題,我們需要在後端的 ASP.NET Core Web API 專案中,加入對 CORS 的支援
- 打開 [Program.cs] 檔案
- 在該檔案內,找到
builder.Services.AddOpenApi();這一行 - 在這一行的下方,加入底下的程式碼,來設定 CORS 的政策,允許來自
http://localhost:49158的請求
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowReactApp", policy =>
{
policy.WithOrigins("http://localhost:49158") // React 應用運行在此端口
.AllowAnyHeader()
.AllowAnyMethod();
});
});- 這裡使用了
AddCors方法來加入 CORS 的服務,並定義了一個名為 "AllowReactApp" 的政策,該政策允許來自http://localhost:49158的請求,並且允許任何標頭和方法(這裡使用這些方法 [AllowAnyHeader()] & [AllowAnyMethod()] 是為了簡化測試,實際上在生產環境中,建議根據需求來限制允許的標頭和方法,以增強安全性) - 接著找到
app.UseHttpsRedirection();這一行 - 在這一行的下方,加入底下的程式碼,來啟用 CORS 的中介軟體,並指定使用剛剛設定的 CORS 政策
#region 使用 CORS 中介軟體 - 必須放在管道的早期位置
app.UseCors("AllowReactApp");
#endregion- 這裡使用了
UseCors方法來啟用 CORS 的中介軟體,並指定使用 "AllowReactApp" 這個政策 - 儲存 [Program.cs] 的修改
- 按下 F5 鍵或點擊「開始」按鈕來執行程式
- 此時,會出現 React 設計的網頁,如下圖所示

- 在網頁最下方,可以看到兩個要輸入的欄位
- 地點與日期,請隨意輸入任何值到這兩個欄位中
- 例如,地點輸入「Taipei」,日期輸入「2024-06-01」
- 然後點擊 [取得天氣] 的按鈕
- 此時,React 的前端應用程式會呼叫後端的 API 端點,並將地點與日期作為查詢字串參數傳送給後端
- 後端的 API 端點會根據接收到的地點與日期來產生對應的天氣預報資料,並將這些資料以 JSON 格式回傳給前端
- 前端接收到這些資料後,會將它們渲染到網頁上,以表格的形式顯示出來

沒有留言:
張貼留言