Introducción a las aplicaciones de página única (SPA) en ASP.NET Core
Nota:
Esta no es la versión más reciente de este artículo. Para la versión actual, consulta la versión .NET 8 de este artículo.
Advertencia
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.
Importante
Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.
Para la versión actual, consulta la versión .NET 8 de este artículo.
Visual Studio proporciona plantillas de proyecto para crear aplicaciones de página única (SPA) basadas en marcos de JavaScript como Angular, React y Vue que tienen un back-end de ASP.NET Core. Estas plantillas:
- Crean una solución de Visual Studio con un proyecto de front-end y un proyecto de back-end.
- Usan el tipo de proyecto de Visual Studio para JavaScript y TypeScript (.esproj) para el front-end.
- Usan un proyecto de ASP.NET Core para el back-end.
Los proyectos creados mediante las plantillas de Visual Studio se pueden ejecutar desde la línea de comandos en Windows, Linux y macOS. Para ejecutar la aplicación, usa dotnet run --launch-profile https
para ejecutar el proyecto de servidor. Al ejecutar el proyecto de servidor, se inicia automáticamente el servidor de desarrollo de JavaScript de front-end. Actualmente se requiere el perfil de inicio https
.
Tutoriales de Visual Studio
Para empezar, siga uno de los tutoriales de la documentación de Visual Studio:
- Creación de una aplicación ASP.NET Core con Angular
- Creación de una aplicación ASP.NET Core con React
- Creación de una aplicación ASP.NET Core con Vue
Para obtener más información, consulte JavaScript y TypeScript en Visual Studio.
Plantillas de SPA de ASP.NET Core
Visual Studio incluye plantillas para compilar aplicaciones ASP.NET Core con un front-end de JavaScript o TypeScript. Estas plantillas están disponibles en la versión 17.8 o posterior de Visual Studio 2022 con la carga de trabajo de desarrollo web y ASP.NET instalada.
Las plantillas de Visual Studio para compilar aplicaciones ASP.NET Core con un front-end de JavaScript o TypeScript ofrecen las siguientes ventajas:
- Eliminan la separación del proyecto para el front-end y el back-end.
- Se mantienen al día con las versiones más recientes del marco de front-end.
- Se integran con las herramientas de línea de comandos del marco de front-end más recientes, como Vite.
- Plantillas para JavaScript y TypeScript (solo TypeScript para Angular).
- Experiencia enriquecida de edición de código de JavaScript y TypeScript.
- Integran las herramientas de compilación de JavaScript con la compilación de .NET.
- Interfaz de usuario de administración de dependencias de npm.
- Compatible con la depuración y la configuración de inicio de Visual Studio Code.
- Ejecuta pruebas unitarias de front-end en el Explorador de pruebas mediante marcos de prueba de JavaScript.
Plantillas de SPA de ASP.NET Core heredadas
Las versiones anteriores del SDK de .NET incluían las plantillas heredadas para compilar aplicaciones SPA con ASP.NET Core. Para obtener documentación sobre estas plantillas anteriores, consulte la versión ASP.NET Core 7.0 de la información general de SPA y los artículos de Angular y React.
Arquitectura de plantillas de aplicación de página única
Las plantillas de aplicación de página única (SPA) para Angular y React ofrecen la capacidad de desarrollar aplicaciones Angular y React hospedadas dentro de un servidor back-end de .NET.
En el momento de la publicación, los archivos de la aplicación Angular y React se copian en la carpeta wwwroot
y se sirven a través del middleware de archivos estáticos.
En lugar de devolver HTTP 404 (No encontrado), una ruta de reserva controla las solicitudes desconocidas al back-end y atiende la página index.html
para SPA.
Durante el desarrollo, la aplicación está configurada para usar el proxy de front-end. React y Angular usan el mismo proxy de front-end.
Cuando se inicia la aplicación, la página index.html
se abre en el explorador. Un middleware especial que solo está habilitado en el desarrollo:
- Intercepta las solicitudes entrantes.
- Comprueba si se está ejecutando el proxy.
- Redirige a la dirección URL del proxy si se está ejecutando o inicia una nueva instancia del proxy.
- Devuelve una página al explorador que se actualiza automáticamente cada pocos segundos hasta que el proxy está arriba y se redirige al explorador.
La principal ventaja que proporcionan las plantillas de SPA de ASP.NET Core:
- Inicia un proxy si aún no se está ejecutando.
- Configuración de HTTPS.
- Configuración de algunas solicitudes con proxy en el servidor back-end de ASP.NET Core.
Cuando el explorador envía una solicitud para un punto de conexión de back-end, por ejemplo /weatherforecast
, en las plantillas. El proxy SPA recibe la solicitud y la devuelve al servidor de forma transparente. El servidor responde y el proxy SPA devuelve la solicitud al explorador:
Aplicaciones de página única publicadas
Cuando se publica la aplicación, SPA se convierte en una colección de archivos en la carpeta wwwroot
.
No se requiere ningún componente en tiempo de ejecución para atender la aplicación:
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();
En el archivo Program.cs
generado por la plantilla anterior:
app.
UseStaticFiles permite que se atiendan los archivos.app.
MapFallbackToFile("index.html")
permite atender el documento predeterminado para cualquier solicitud desconocida que reciba el servidor.
Cuando la aplicación se publica con dotnet publish, las siguientes tareas del archivo csproj
garantizan que npm restore
se ejecute y que se ejecute el script npm adecuado para generar los artefactos de producción:
<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>
Desarrollo de aplicaciones de página única
El archivo de proyecto define algunas propiedades que controlan el comportamiento de la aplicación durante el desarrollo:
<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
: controla la dirección URL en la que el servidor espera que se ejecute el proxy SPA. Esta es la dirección URL:- El servidor hace ping después de iniciar el proxy para saber si está listo.
- Donde redirige el explorador después de una respuesta correcta.
SpaProxyLaunchCommand
: el comando que usa el servidor para iniciar el proxy SPA cuando detecta que el proxy no se está ejecutando.
El paquete Microsoft.AspNetCore.SpaProxy
es responsable de la lógica anterior para detectar el proxy y redirigir el explorador.
El ensamblado de inicio de hospedaje definido en Properties/launchSettings.json
se usa para agregar automáticamente los componentes necesarios durante el desarrollo necesarios para detectar si el proxy se está ejecutando e iniciarlo de otro modo:
{
"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"
}
}
}
}
Generación de la aplicación cliente
Esta configuración es específica del marco de front-end que usa la aplicación, pero muchos aspectos de la configuración son similares.
Configuración de Angular
El archivo ClientApp/package.json
generado por la plantilla:
{
"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": {}
}
Contiene scripts que inician el servidor de desarrollo de Angular:
El script
prestart
invoca aClientApp/aspnetcore-https.js
, que es responsable de garantizar que el certificado HTTPS del servidor de desarrollo esté disponible para el servidor proxy SPA.El
start:windows
ystart:default
:- Inicie el servidor de desarrollo de Angular a través de
ng serve
. - Proporcione el puerto, las opciones para usar HTTPS y la ruta de acceso al certificado y a la clave asociada. El número de puerto proporcionado coincide con el número de puerto especificado en el archivo
.csproj
.
- Inicie el servidor de desarrollo de Angular a través de
El archivo ClientApp/angular.json
generado por la plantilla contiene:
El comando
serve
.Un elemento
proxyconfig
de la configuracióndevelopment
para indicar queproxy.conf.js
se debe usar para configurar el proxy de front-end, tal como se muestra en el siguiente JSON resaltado:{ "$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
define las rutas con proxy al back-end del servidor. El conjunto general de opciones se define en http-proxy-middleware para React y Angular, ya que ambos usan el mismo proxy.
El código resaltado siguiente de ClientApp/proxy.conf.js
usa lógica basada en las variables de entorno establecidas durante el desarrollo para determinar el puerto en el que se ejecuta el back-end:
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;
Configuración de React
La sección de scripts
package.json
contiene los siguientes scripts que inician la aplicación React durante el desarrollo, como se muestra en el código resaltado siguiente:{ "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" ] } }
El script
prestart
invoca:aspnetcore-https.js
, que es responsable de garantizar que el certificado HTTPS del servidor de desarrollo esté disponible para el servidor proxy SPA.- Invoca a
aspnetcore-react.js
para configurar el archivo adecuado.env.development.local
para usar el certificado de desarrollo local HTTPS.aspnetcore-react.js
configura el certificado de desarrollo local HTTPS agregandoSSL_CRT_FILE=<certificate-path>
ySSL_KEY_FILE=<key-path>
al archivo.
El archivo
.env.development
define el puerto para el servidor de desarrollo y especifica HTTPS.
El archivo src/setupProxy.js
configura el proxy SPA para reenviar las solicitudes al back-end. El conjunto general de opciones se define en http-proxy-middleware.
El código resaltado siguiente en ClientApp/src/setupProxy.js
usa lógica basada en las variables de entorno establecidas durante el desarrollo para determinar el puerto en el que se ejecuta el back-end:
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);
};
Versión del marco de SPA compatible en las plantillas SPA de ASP.NET Core
Las plantillas de proyecto de SPA que se incluyen con cada versión de ASP.NET Core hacen referencia a la versión más reciente del marco de SPA adecuado.
Normalmente, los marcos de SPA tienen un ciclo de versión más corto que .NET. Debido a los dos ciclos de versión diferentes, la versión compatible del marco de SPA y .NET puede salir de la sincronización: la versión principal del marco de SPA, de la que depende una versión principal de .NET, puede dejar de ser compatible, mientras que la versión de .NET con la que se incluye el marco de SPA sigue siendo compatible.
Las plantillas de SPA de ASP.NET Core se pueden actualizar en una versión de revisión a una nueva versión del marco de SPA para mantener las plantillas en un estado compatible y seguro.