共用方式為


ASP.NET Core 中的單頁應用程式 (SPA) 概觀

注意

這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前的版本,請參閱 本文的 .NET 9 版本。

Visual Studio 提供的專案範本可根據 AngularReactVue 等具有 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 應用程式的舊版範本。 如需這些舊版範本的檔,請參閱 .NET 7 版的 SPA 概觀AngularReact 文章。

單頁應用程式範本的架構

AngularReact 的單頁應用程式 (SPA) 範本可讓您開發裝載在 .NET 後端伺服器內的 Angular 和 React 應用程式。

在發佈時,Angular 和 React 應用程式的檔案會複製到 wwwroot 資料夾,並透過靜態檔案中介軟體提供。

系統不會傳回 HTTP 404 (找不到),而是由後援路由處理後端的未知要求,並為 SPA 提供 index.html

在開發期間,應用程式會設定為使用前端 Proxy。 React 和 Angular 會採用相同的前端 Proxy。

當應用程式啟動時,index.html 頁面會在瀏覽器中開啟。 僅在開發中啟用的特殊中介軟體:

  • 攔截傳入要求。
  • 檢查 Proxy 是否正在執行。
  • 如果 Proxy 正在執行則重新導向至其 URL,或是啟動新的 Proxy 執行個體。
  • 將頁面傳回瀏覽器,每隔幾秒鐘自動重新整理一次,直到 Proxy 啟動並重新導向瀏覽器為止。

瀏覽器 Proxy 伺服器圖表

ASP.NET Core SPA 範本的主要優點包括:

  • 若 Proxy 未執行則啟動 Proxy。
  • 設定 HTTPS。
  • 將某些要求設定為透過 Proxy 傳送到後端 ASP.NET Core 伺服器。

當瀏覽器傳送後端端點的要求時,例如範本中的 /weatherforecast。 SPA Proxy 會接收要求,並以透明方式將其傳回伺服器。 伺服器會進行回應,而 SPA Proxy 則將要求傳回瀏覽器:

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 檔案中:

使用 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:windowsstart:default 將會:

    • 透過 ng serve 啟動 Angular 開發伺服器。
    • 提供連接埠、使用 HTTPS 的選項,以及憑證和關聯金鑰的路徑。 提供的連接埠號碼會符合 .csproj 檔案中指定的連接埠號碼。

範本產生的 ClientApp/angular.json 檔案包含:

  • serve 命令。

  • 一項 proxyconfig 設定中的 development 元素,用以指出 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 架構版本,讓範本保持支援且安全的狀態。

其他資源