Архитектура шаблонов одностраничных приложений
Шаблоны одностраничных приложений (SPA) для Angular и React позволяют разрабатывать приложения Angular и React , размещенные на сервере серверной части .NET.
Во время публикации файлы приложения Angular и React копируются wwwroot
в папку и обслуживаются через ПО промежуточного слоя статических файлов.
Вместо возврата HTTP 404 (Не найдено), резервный маршрут обрабатывает неизвестные запросы к серверной части и служит index.html
для SPA.
Во время разработки приложение настроено на использование внешнего прокси-сервера. React и Angular используют тот же внешний прокси-сервер.
При запуске приложения страница index.html
открывается в браузере. Специальное ПО промежуточного слоя, которое включено только в разработке:
- Перехватывает входящие запросы.
- Проверяет, запущен ли прокси-сервер.
- Перенаправляет URL-адрес прокси-сервера, если он запущен или запускает новый экземпляр прокси-сервера.
- Возвращает страницу в браузер, который автоматически обновляется каждые несколько секунд до тех пор, пока прокси-сервер не будет перенаправляться.

Основное преимущество шаблонов SPA для ASP.NET Core предоставляют следующие преимущества:
- Запускает прокси-сервер, если он еще не запущен.
- Настройка HTTPS.
- Настройка некоторых запросов для прокси-сервера серверной ASP.NET Core.
Когда браузер отправляет запрос на серверную конечную точку, например /weatherforecast
в шаблонах. Прокси-сервер SPA получает запрос и отправляет его обратно на сервер прозрачно. Сервер отвечает, а прокси-сервер SPA отправляет запрос обратно в браузер:

Опубликованные одностраничные приложения
Когда приложение опубликовано, 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
: управляет URL-адресом, в котором сервер ожидает запуска прокси-сервера SPA. Это URL-адрес:
- Сервер проверяет связь после запуска прокси-сервера, чтобы узнать, готов ли он.
- Где он перенаправляет браузер после успешного ответа.
SpaProxyLaunchCommand
: команда, в которой сервер используется для запуска прокси-сервера SPA, когда он обнаруживает, что прокси-сервер не запущен.
Пакет Microsoft.AspNetCore.SpaProxy
отвечает за предыдущую логику для обнаружения прокси-сервера и перенаправления браузера.
Сборка запуска размещения, определенная в Properties/launchSettings.json
, используется для автоматического добавления необходимых компонентов во время разработки, чтобы определить, запущен ли прокси-сервер и запустить его в противном случае:
{
"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"
}
}
}
}
Настройка клиентского приложения
Эта настройка связана с интерфейсной платформой, используемой приложением, однако многие аспекты конфигурации похожи.
Созданный 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.
И start:windows
start:default
:
- Запустите сервер разработки Angular с помощью
ng serve
.
- Укажите порт, параметры для использования HTTPS и путь к сертификату и связанному ключу. Номер порта соответствует номеру порта, указанному
.csproj
в файле.
Созданный шаблон ClientApp/angular.json
содержит:
Команда serve
.
Элемент proxyconfig
в development
конфигурации, указывающий, что proxy.conf.js
следует использовать для настройки внешнего прокси-сервера, как показано в следующем выделенном формате 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
определяет маршруты, которые должны быть проксированы обратно на серверную часть сервера. Общий набор параметров определяется в по промежуточном слоях http-proxy для react и angular, так как они оба используют один и тот же прокси-сервер.
Следующий выделенный код использует 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;
Раздел 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.
aspnetcore-react.js
Вызывается для настройки соответствующего .env.development.local
файла для использования локального сертификата разработки HTTPS. aspnetcore-react.js
настраивает сертификат локальной разработки HTTPS, добавив SSL_CRT_FILE=<certificate-path>
и SSL_KEY_FILE=<key-path>
в файл.
Файл .env.development
определяет порт для сервера разработки и задает ПРОТОКОЛ HTTPS.
Прокси-сервер src/setupProxy.js
SPA настраивает перенаправление запросов на серверную часть. Общий набор параметров определяется в ПО промежуточного слоя http-proxy-.
Следующий выделенный код использует 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);
};
Поддерживаемая версия платформы SPA в шаблонах SPA ASP.NET Core
Шаблоны проектов SPA, которые поставляются с каждым выпуском ASP.NET Core, ссылаются на последнюю версию соответствующей платформы SPA.
Платформы SPA обычно имеют более короткий цикл выпуска, чем .NET. Из-за двух разных циклов выпуска поддерживаемая версия платформы SPA и .NET может выйти из синхронизации: основная версия платформы SPA, от которой зависит основной выпуск .NET, может выйти из поддержки, в то время как версия .NET, отправленная на платформу SPA, по-прежнему поддерживается.
Шаблоны spa ASP.NET Core можно обновить в выпуске исправлений до новой версии spa framework, чтобы шаблоны сохранялись в поддерживаемом и безопасном состоянии.