ASP.NET Core 中的單頁應用程式 (SPA) 概觀
注意
這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本。
Visual Studio 提供的專案範本可根據 Angular、React 和 Vue 等具有 ASP.NET Core 後端的 JavaScript 架構來建立單頁應用程式 (SPA)。 這些範本會:
- 建立一個包含前端專案和後端專案的 Visual Studio 解決方案。
- 使用 Visual Studio 的 JavaScript 和 TypeScript 專案類型 (.esproj) 來開發前端專案。
- 使用 ASP.NET Core 專案來開發後端專案。
使用 Visual Studio 範本建立的專案可在 Windows、Linux 和 macOS 上透過命令列執行。 若要執行應用程式,請使用 dotnet run --launch-profile https
執行伺服器專案。 執行伺服器專案會自動啟動前端 JavaScript 開發伺服器。 目前需要 https
啟動設定檔。
Visual Studio 教學課程
若要開始使用,請遵循 Visual Studio 文件中的其中一個教學課程:
如需詳細資訊,請參閱 Visual Studio 中的 JavaScript 和 TypeScript
ASP.NET Core SPA 範本
Visual Studio 包含使用 JavaScript 或 TypeScript 前端建置 ASP.NET Core 應用程式的範本。 這些範本可在已安裝 ASP.NET 和 Web 開發工作負載的 Visual Studio 2022 17.8 版或更新版本中取得。
使用 JavaScript 或 TypeScript 前端建置 ASP.NET Core 應用程式的 Visual Studio 範本提供下列優點:
- 清除前端和後端的專案區隔。
- 隨時掌握最新的前端架構版本。
- 整合最新的前端架構命令列工具,例如 Vite。
- JavaScript 和 TypeScript 的範本 (僅限 TypeScript for Angular)。
- 豐富的 JavaScript 和 TypeScript 程式碼編輯體驗。
- 整合 JavaScript 建置工具與 .NET 組建。
- npm 相依性管理 UI。
- 與 Visual Studio Code 偵錯和啟動組態相容。
- 使用 JavaScript 測試架構,在測試總管中執行前端單元測試。
舊版 ASP.NET Core SPA 範本
舊版 .NET SDK 包含使用 ASP.NET Core 建置 SPA 應用程式的舊版範本。 如需這些舊版範本的說明文件,請參閱 ASP.NET Core 7.0 版的 SPA 概觀 以及 Angular 和 React 文章。
單頁應用程式範本的架構
Angular 和 React 的單頁應用程式 (SPA) 範本可讓您開發裝載在 .NET 後端伺服器內的 Angular 和 React 應用程式。
在發佈時,Angular 和 React 應用程式的檔案會複製到 wwwroot
資料夾,並透過靜態檔案中介軟體提供。
系統不會傳回 HTTP 404 (找不到),而是由後援路由處理後端的未知要求,並為 SPA 提供 index.html
。
在開發期間,應用程式會設定為使用前端 Proxy。 React 和 Angular 會採用相同的前端 Proxy。
當應用程式啟動時,index.html
頁面會在瀏覽器中開啟。 僅在開發中啟用的特殊中介軟體:
- 攔截傳入要求。
- 檢查 Proxy 是否正在執行。
- 如果 Proxy 正在執行則重新導向至其 URL,或是啟動新的 Proxy 執行個體。
- 將頁面傳回瀏覽器,每隔幾秒鐘自動重新整理一次,直到 Proxy 啟動並重新導向瀏覽器為止。
ASP.NET Core SPA 範本的主要優點包括:
- 若 Proxy 未執行則啟動 Proxy。
- 設定 HTTPS。
- 將某些要求設定為透過 Proxy 傳送到後端 ASP.NET Core 伺服器。
當瀏覽器傳送後端端點的要求時,例如範本中的 /weatherforecast
。 SPA Proxy 會接收要求,並以透明方式將其傳回伺服器。 伺服器會進行回應,而 SPA Proxy 則將要求傳回瀏覽器:
已發佈的單頁應用程式
應用程式發佈後,SPA 會變成 wwwroot
資料夾中的檔案集合。
提供應用程式不需要執行階段元件:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html");
app.Run();
在上述範本產生的 Program.cs
檔案中:
app.
UseStaticFiles 支援提供檔案。app.
MapFallbackToFile("index.html")
支援針對伺服器接收的任何未知要求提供預設文件。
使用 dotnet publish 發佈應用程式時,csproj
檔案中的下列工作可確保 npm restore
執行,並執行適當的 npm 指令碼來產生實際執行成品:
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)build\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>
開發單頁應用程式
專案檔會定義一些屬性,用於在開發期間控制應用程式的行為:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>
<SpaRoot>ClientApp\</SpaRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
<SpaProxyServerUrl>https://localhost:44414</SpaProxyServerUrl>
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.1" />
</ItemGroup>
<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list -->
<Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)build\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>
SpaProxyServerUrl
:控制預期執行 SPA Proxy 的伺服器 URL。 此 URL 用於:- 在啟動 Proxy 之後,伺服器會對其進行 Ping 以確認是否就緒。
- 成功回應之後,瀏覽器重新導向的位置。
SpaProxyLaunchCommand
:伺服器偵測到 Proxy 未執行時,用來啟動 SPA Proxy 的命令。
套件 Microsoft.AspNetCore.SpaProxy
負責處理上述邏輯以偵測 Proxy 並重新導向瀏覽器。
Properties/launchSettings.json
中定義的裝載啟動元件會在開發期間自動新增所需元件,用於偵測 Proxy 是否正在執行,若未執行則將其啟動:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51783",
"sslPort": 44329
}
},
"profiles": {
"MyReact": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:7145;http://localhost:5273",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
}
}
}
}
用戶端應用程式的設定
此設定專門用於應用程式所使用的前端架構,不過設定的許多層面都類似。
Angular 設定
範本產生的 ClientApp/package.json
檔案:
{
"name": "myangular",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"prestart": "node aspnetcore-https",
"start": "run-script-os",
"start:windows": "ng serve --port 44483 --ssl --ssl-cert \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem\" --ssl-key \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.key\"",
"start:default": "ng serve --port 44483 --ssl --ssl-cert \"$HOME/.aspnet/https/${npm_package_name}.pem\" --ssl-key \"$HOME/.aspnet/https/${npm_package_name}.key\"",
"build": "ng build",
"build:ssr": "ng run MyAngular:server:dev",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^14.1.3",
"@angular/common": "^14.1.3",
"@angular/compiler": "^14.1.3",
"@angular/core": "^14.1.3",
"@angular/forms": "^14.1.3",
"@angular/platform-browser": "^14.1.3",
"@angular/platform-browser-dynamic": "^14.1.3",
"@angular/platform-server": "^14.1.3",
"@angular/router": "^14.1.3",
"bootstrap": "^5.2.0",
"jquery": "^3.6.0",
"oidc-client": "^1.11.5",
"popper.js": "^1.16.0",
"run-script-os": "^1.1.6",
"rxjs": "~7.5.6",
"tslib": "^2.4.0",
"zone.js": "~0.11.8"
},
"devDependencies": {
"@angular-devkit/build-angular": "^14.1.3",
"@angular/cli": "^14.1.3",
"@angular/compiler-cli": "^14.1.3",
"@types/jasmine": "~4.3.0",
"@types/jasminewd2": "~2.0.10",
"@types/node": "^18.7.11",
"jasmine-core": "~4.3.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.1",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"typescript": "~4.7.4"
},
"overrides": {
"autoprefixer": "10.4.5"
},
"optionalDependencies": {}
}
包含啟動 Angular 開發伺服器的指令碼:
prestart
指令碼會叫用ClientApp/aspnetcore-https.js
,後者負責確保開發伺服器 HTTPS 憑證可供 SPA Proxy 伺服器使用。start:windows
和start:default
將會:- 透過
ng serve
啟動 Angular 開發伺服器。 - 提供連接埠、使用 HTTPS 的選項,以及憑證和關聯金鑰的路徑。 提供的連接埠號碼會符合
.csproj
檔案中指定的連接埠號碼。
- 透過
範本產生的 ClientApp/angular.json
檔案包含:
serve
命令。一項
development
設定中的proxyconfig
元素,用以指出proxy.conf.js
應用來設定前端 Proxy,如下列醒目提示的 JSON 所示:{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "MyAngular": { "projectType": "application", "schematics": { "@schematics/angular:application": { "strict": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "progress": false, "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "allowedCommonJsDependencies": [ "oidc-client" ], "assets": [ "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.css" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "MyAngular:build:production" }, "development": { "browserTarget": "MyAngular:build:development", "proxyConfig": "proxy.conf.js" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "MyAngular:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "assets": [ "src/assets" ], "styles": [ "src/styles.css" ], "scripts": [] } }, "server": { "builder": "@angular-devkit/build-angular:server", "options": { "outputPath": "dist-server", "main": "src/main.ts", "tsConfig": "tsconfig.server.json" }, "configurations": { "dev": { "optimization": true, "outputHashing": "all", "sourceMap": false, "namedChunks": false, "extractLicenses": true, "vendorChunk": true }, "production": { "optimization": true, "outputHashing": "all", "sourceMap": false, "namedChunks": false, "extractLicenses": true, "vendorChunk": false } } } } } }, "defaultProject": "MyAngular" }
ClientApp/proxy.conf.js
會定義需要透過 Proxy 回到伺服器後端的路由。 一般選項組定義於 react 和 angular 的 http-proxy-middleware 中,因為兩者都使用相同的 Proxy。
下列 ClientApp/proxy.conf.js
醒目提示的程式碼會根據開發期間設定的環境變數使用邏輯,判斷後端正在執行的連接埠:
const { env } = require('process');
const target = env.ASPNETCORE_HTTPS_PORTS ? `https://localhost:${env.ASPNETCORE_HTTPS_PORTS}` :
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:51951';
const PROXY_CONFIG = [
{
context: [
"/weatherforecast",
],
target: target,
secure: false,
headers: {
Connection: 'Keep-Alive'
}
}
]
module.exports = PROXY_CONFIG;
React 設定
package.json
指令碼區段包含下列指令碼,用於在開發期間啟動 react 應用程式,如下列醒目提示的程式碼所示:{ "name": "myreact", "version": "0.1.0", "private": true, "dependencies": { "bootstrap": "^5.2.0", "http-proxy-middleware": "^2.0.6", "jquery": "^3.6.0", "merge": "^2.1.1", "oidc-client": "^1.11.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-bootstrap": "^0.26.2", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "reactstrap": "^9.1.3", "rimraf": "^3.0.2", "web-vitals": "^2.1.4", "workbox-background-sync": "^6.5.4", "workbox-broadcast-update": "^6.5.4", "workbox-cacheable-response": "^6.5.4", "workbox-core": "^6.5.4", "workbox-expiration": "^6.5.4", "workbox-google-analytics": "^6.5.4", "workbox-navigation-preload": "^6.5.4", "workbox-precaching": "^6.5.4", "workbox-range-requests": "^6.5.4", "workbox-routing": "^6.5.4", "workbox-strategies": "^6.5.4", "workbox-streams": "^6.5.4" }, "devDependencies": { "ajv": "^8.11.0", "cross-env": "^7.0.3", "eslint": "^8.22.0", "eslint-config-react-app": "^7.0.1", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.30.1", "nan": "^2.16.0", "typescript": "^4.7.4" }, "overrides": { "autoprefixer": "10.4.5" }, "resolutions": { "css-what": "^5.0.1", "nth-check": "^3.0.1" }, "scripts": { "prestart": "node aspnetcore-https && node aspnetcore-react", "start": "rimraf ./build && react-scripts start", "build": "react-scripts build", "test": "cross-env CI=true react-scripts test --env=jsdom", "eject": "react-scripts eject", "lint": "eslint ./src/" }, "eslintConfig": { "extends": [ "react-app" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
prestart
指令碼會叫用下列項目:aspnetcore-https.js
,負責確保開發伺服器 HTTPS 憑證可供 SPA Proxy 伺服器使用。- 叫用
aspnetcore-react.js
以設定適當的.env.development.local
檔案以使用 HTTPS 本機開發憑證。aspnetcore-react.js
會透過將SSL_CRT_FILE=<certificate-path>
和SSL_KEY_FILE=<key-path>
新增至檔案來設定 HTTPS 本機開發憑證。
.env.development
檔案會定義開發伺服器的連接埠,並指定 HTTPS。
src/setupProxy.js
會將 SPA Proxy 設定為將要求轉送至後端。 一般選項組定義於 http-proxy-middleware 中。
下列 ClientApp/src/setupProxy.js
醒目提示的程式碼會根據開發期間設定的環境變數使用邏輯,判斷後端正在執行的連接埠:
const { createProxyMiddleware } = require('http-proxy-middleware');
const { env } = require('process');
const target = env.ASPNETCORE_HTTPS_PORTS ? `https://localhost:${env.ASPNETCORE_HTTPS_PORTS}` :
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:51783';
const context = [
"/weatherforecast",
];
const onError = (err, req, resp, target) => {
console.error(`${err.message}`);
}
module.exports = function (app) {
const appProxy = createProxyMiddleware(context, {
target: target,
// Handle errors to prevent the proxy middleware from crashing when
// the ASP NET Core webserver is unavailable
onError: onError,
secure: false,
// Uncomment this line to add support for proxying websockets
//ws: true,
headers: {
Connection: 'Keep-Alive'
}
});
app.use(appProxy);
};
ASP.NET Core SPA 範本中支援的 SPA 架構版本
每個 ASP.NET Core 版本隨附的 SPA 專案範本會參考適當 SPA 架構的最新版本。
SPA 架構的發行週期通常比 .NET 短。 由於兩者的發行週期不同,支援的 SPA 架構和 .NET 版本可能會不同步:主要 SPA 架構版本 (即 .NET 主要版本的相依版本) 可能會停止支援,而 .NET 版本隨附的 SPA 架構仍會受到支援。
ASP.NET Core SPA 範本可透過修補程式版本更新為新的 SPA 架構版本,讓範本保持支援且安全的狀態。