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, consulte 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, consulte 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, use 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:

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.
  • Ejecute 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.

Diagrama del servidor proxy del 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:

Diagrama del servidor proxy

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 a ClientApp/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 y start: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.

El archivo ClientApp/angular.json generado por la plantilla contiene:

  • El comando serve.

  • Un elemento proxyconfig de la configuración development para indicar que proxy.conf.js se debe usar para configurar el proxy de front-end, tal como se muestra en el siguiente código 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 agregando SSL_CRT_FILE=<certificate-path> y SSL_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.

Recursos adicionales