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

注意

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

重要

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

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

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 應用程式的舊版範本。 如需這些舊版範本的說明文件,請參閱 ASP.NET Core 7.0 版的 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 命令。

  • 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 架構版本,讓範本保持支援且安全的狀態。

其他資源