Usare JavaScript Services per creare applicazioni a pagina singola in ASP.NET Core

Di Fiyaz Hasan

Avviso

Le funzionalità descritte in questo articolo sono obsolete a partire da ASP.NET Core 3.0. Un meccanismo di integrazione dei framework SPA più semplice è disponibile nel pacchetto NuGet Microsoft.AspNetCore.SpaServices.Extensions . Per altre informazioni, vedere [Annuncio] Obsoleto Microsoft.AspNetCore.SpaServices e Microsoft.AspNetCore.NodeServices.

Un'applicazione a pagina singola è un tipo diffuso di applicazione Web grazie alla sua esperienza utente avanzata intrinseca. L'integrazione di framework o librerie SPA sul lato client, ad esempio Angular o React, con framework lato server come ASP.NET Core può essere difficile. JavaScript Services è stato sviluppato per ridurre l'attrito nel processo di integrazione. Consente un funzionamento senza problemi tra i diversi stack di tecnologie client e server.

Che cos'è JavaScript Services

JavaScript Services è una raccolta di tecnologie lato client per ASP.NET Core. L'obiettivo è posizionare ASP.NET Core come piattaforma sul lato server preferita degli sviluppatori per la creazione di applicazioni a pagina singola.

JavaScript Services è costituito da due pacchetti NuGet distinti:

Questi pacchetti sono utili negli scenari seguenti:

  • Eseguire JavaScript nel server
  • Usare un framework o una libreria SPA
  • Creare asset sul lato client con Webpack

Gran parte dell'attenzione di questo articolo viene posta sull'uso del pacchetto SpaServices.

Che cos'è SpaServices

SpaServices è stato creato per posizionare ASP.NET Core come piattaforma sul lato server preferita degli sviluppatori per la creazione di applicazioni a pagina singola. SpaServices non è necessario per sviluppare applicazioni a pagina singola con ASP.NET Core e non blocca gli sviluppatori in un framework client specifico.

SpaServices offre un'infrastruttura utile, ad esempio:

Collettivamente, questi componenti dell'infrastruttura migliorano sia il flusso di lavoro di sviluppo che l'esperienza di runtime. I componenti possono essere adottati singolarmente.

Prerequisiti per l'uso di SpaServices

Per usare SpaServices, installare quanto segue:

  • Node.js (versione 6 o successiva) con npm

    • Per verificare che questi componenti siano installati e sono disponibili, eseguire quanto segue dalla riga di comando:

      node -v && npm -v
      
    • Se si esegue la distribuzione in un sito Web di Azure, non è necessaria alcuna azione: Node.js viene installato e disponibile negli ambienti server.

  • .NET Core SDK 2.0 o versione successiva

    • In Windows con Visual Studio 2017 l'SDK viene installato selezionando il carico di lavoro sviluppo multipiattaforma .NET Core.
  • Pacchetto NuGet Microsoft.AspNetCore.SpaServices

Prerendering lato server

Un'applicazione universale (nota anche come isomorfica) è un'applicazione JavaScript in grado di eseguire sia nel server che nel client. Angular, React e altri framework diffusi offrono una piattaforma universale per questo stile di sviluppo di applicazioni. L'idea consiste innanzitutto nel eseguire il rendering dei componenti del framework nel server tramite Node.js e quindi delegare un'ulteriore esecuzione al client.

ASP.NET core tag helper forniti da SpaServices semplificano l'implementazione del prerendering sul lato server richiamando le funzioni JavaScript nel server.

Prerequisiti di prerendering sul lato server

Installare il pacchetto npm aspnet-prerendering :

npm i -S aspnet-prerendering

Configurazione di prerendering lato server

Gli helper tag vengono resi individuabili tramite la registrazione dello spazio dei nomi nel file del _ViewImports.cshtml progetto:

@using SpaServicesSampleApp
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"

Questi helper tag astraggono le complessità della comunicazione diretta con API di basso livello sfruttando una sintassi simile a HTML all'interno della Razor visualizzazione:

<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>

Helper tag asp-prerender-module

L'helper asp-prerender-module tag, usato nell'esempio di codice precedente, viene eseguito ClientApp/dist/main-server.js nel server tramite Node.js. Per maggiore chiarezza, main-server.js il file è un artefatto dell'attività di transpilazione da TypeScript a JavaScript nel processo di compilazione Webpack . Webpack definisce un alias del punto di ingresso di main-servere l'attraversamento del grafico delle dipendenze per questo alias inizia nel ClientApp/boot-server.ts file:

entry: { 'main-server': './ClientApp/boot-server.ts' },

Nell'esempio angular seguente il ClientApp/boot-server.ts file usa la createServerRenderer funzione e RenderResult il tipo del pacchetto npm per configurare il aspnet-prerendering rendering del server tramite Node.js. Il markup HTML destinato al rendering lato server viene passato a una chiamata di funzione resolve, di cui viene eseguito il wrapping in un oggetto JavaScript Promise fortemente tipizzato. Il Promise significato dell'oggetto è che fornisce in modo asincrono il markup HTML alla pagina per l'inserimento nell'elemento segnaposto del DOM.

import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {
    const providers = [
        { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
        { provide: 'ORIGIN_URL', useValue: params.origin }
    ];

    return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
        const appRef = moduleRef.injector.get(ApplicationRef);
        const state = moduleRef.injector.get(PlatformState);
        const zone = moduleRef.injector.get(NgZone);
        
        return new Promise<RenderResult>((resolve, reject) => {
            zone.onError.subscribe(errorInfo => reject(errorInfo));
            appRef.isStable.first(isStable => isStable).subscribe(() => {
                // Because 'onStable' fires before 'onError', we have to delay slightly before
                // completing the request in case there's an error to report
                setImmediate(() => {
                    resolve({
                        html: state.renderToString()
                    });
                    moduleRef.destroy();
                });
            });
        });
    });
});

Helper tag asp-prerender-data

In combinazione con l'helper tag, è possibile usare l'helper asp-prerender-moduleasp-prerender-data tag per passare informazioni contestuali dalla Razor visualizzazione a JavaScript sul lato server. Ad esempio, il markup seguente passa i dati utente al main-server modulo:

<app asp-prerender-module="ClientApp/dist/main-server"
        asp-prerender-data='new {
            UserName = "John Doe"
        }'>Loading...</app>

L'argomento ricevuto UserName viene serializzato usando il serializzatore ON predefinito JSe viene archiviato nell'oggetto params.data . Nell'esempio angular seguente i dati vengono usati per costruire un messaggio di saluto personalizzato all'interno di un h1 elemento :

import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {
    const providers = [
        { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
        { provide: 'ORIGIN_URL', useValue: params.origin }
    ];

    return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
        const appRef = moduleRef.injector.get(ApplicationRef);
        const state = moduleRef.injector.get(PlatformState);
        const zone = moduleRef.injector.get(NgZone);
        
        return new Promise<RenderResult>((resolve, reject) => {
            const result = `<h1>Hello, ${params.data.userName}</h1>`;

            zone.onError.subscribe(errorInfo => reject(errorInfo));
            appRef.isStable.first(isStable => isStable).subscribe(() => {
                // Because 'onStable' fires before 'onError', we have to delay slightly before
                // completing the request in case there's an error to report
                setImmediate(() => {
                    resolve({
                        html: result
                    });
                    moduleRef.destroy();
                });
            });
        });
    });
});

I nomi delle proprietà passati negli helper tag sono rappresentati con la notazione PascalCase . A differenza di JavaScript, dove gli stessi nomi di proprietà sono rappresentati con camelCase. La configurazione di serializzazione ON predefinita JSè responsabile di questa differenza.

Per espandere l'esempio di codice precedente, i dati possono essere passati dal server alla visualizzazione idratando la globals proprietà fornita alla resolve funzione:

import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {
    const providers = [
        { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
        { provide: 'ORIGIN_URL', useValue: params.origin }
    ];

    return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
        const appRef = moduleRef.injector.get(ApplicationRef);
        const state = moduleRef.injector.get(PlatformState);
        const zone = moduleRef.injector.get(NgZone);
        
        return new Promise<RenderResult>((resolve, reject) => {
            const result = `<h1>Hello, ${params.data.userName}</h1>`;

            zone.onError.subscribe(errorInfo => reject(errorInfo));
            appRef.isStable.first(isStable => isStable).subscribe(() => {
                // Because 'onStable' fires before 'onError', we have to delay slightly before
                // completing the request in case there's an error to report
                setImmediate(() => {
                    resolve({
                        html: result,
                        globals: {
                            postList: [
                                'Introduction to ASP.NET Core',
                                'Making apps with Angular and ASP.NET Core'
                            ]
                        }
                    });
                    moduleRef.destroy();
                });
            });
        });
    });
});

La postList matrice definita all'interno dell'oggetto globals è collegata all'oggetto globale window del browser. Questa variabile di archiviazione nell'ambito globale elimina la duplicazione del lavoro, in particolare per quanto riguarda il caricamento degli stessi dati una volta sul server e di nuovo sul client.

global postList variable attached to window object

Webpack Dev Middleware

Webpack Dev Middleware introduce un flusso di lavoro di sviluppo semplificato in cui Webpack crea risorse su richiesta. Il middleware compila e gestisce automaticamente le risorse lato client quando una pagina viene ricaricata nel browser. L'approccio alternativo consiste nell'richiamare manualmente Webpack tramite lo script di compilazione npm del progetto quando viene modificata una dipendenza di terze parti o il codice personalizzato. Un script di compilazione npm nel package.json file è illustrato nell'esempio seguente:

"build": "npm run build:vendor && npm run build:custom",

Prerequisiti del middleware di Sviluppo Webpack

Installare il pacchetto npm aspnet-webpack :

npm i -D aspnet-webpack

Configurazione del middleware di Sviluppo Webpack

Webpack Dev Middleware viene registrato nella pipeline di richiesta HTTP tramite il codice seguente nel Startup.cs metodo del Configure file:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseWebpackDevMiddleware();
}
else
{
    app.UseExceptionHandler("/Home/Error");
}

// Call UseWebpackDevMiddleware before UseStaticFiles
app.UseStaticFiles();

Il UseWebpackDevMiddleware metodo di estensione deve essere chiamato prima di registrare l'hosting di file statici tramite il UseStaticFiles metodo di estensione. Per motivi di sicurezza, registrare il middleware solo quando l'app viene eseguita in modalità di sviluppo.

La webpack.config.js proprietà del output.publicPath file indica al middleware di controllare la dist cartella per le modifiche:

module.exports = (env) => {
        output: {
            filename: '[name].js',
            publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
        },

Sostituzione dei moduli ad accesso frequente

Si pensi alla funzionalità HMR (Hot Module Replacement) di Webpack come un'evoluzione del middleware di Sviluppo Webpack. HMR introduce tutti gli stessi vantaggi, ma semplifica ulteriormente il flusso di lavoro di sviluppo aggiornando automaticamente il contenuto della pagina dopo la compilazione delle modifiche. Non confondere questo con un aggiornamento del browser, che interferisce con lo stato corrente in memoria e la sessione di debug dell'applicazione a pagina singola. Esiste un collegamento live tra il servizio Middleware di Sviluppo Webpack e il browser, il che significa che le modifiche vengono spostate nel browser.

Prerequisiti di sostituzione dei moduli ad accesso frequente

Installare il pacchetto npm webpack-hot-middleware :

npm i -D webpack-hot-middleware

Configurazione della sostituzione dei moduli ad accesso frequente

Il componente HMR deve essere registrato nella pipeline di richiesta HTTP di MVC nel Configure metodo :

app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
    HotModuleReplacement = true
});

Come è stato vero con Il middleware di Sviluppo Webpack, il UseWebpackDevMiddleware metodo di estensione deve essere chiamato prima del UseStaticFiles metodo di estensione. Per motivi di sicurezza, registrare il middleware solo quando l'app viene eseguita in modalità di sviluppo.

Il webpack.config.js file deve definire una plugins matrice, anche se rimane vuota:

module.exports = (env) => {
        plugins: [new CheckerPlugin()]

Dopo il caricamento dell'app nel browser, la scheda Console degli strumenti di sviluppo fornisce la conferma dell'attivazione HMR:

Hot Module Replacement connected message

Helper di routing

Nella maggior parte dei ASP.NET spA basati su Core, il routing lato client è spesso desiderato oltre al routing lato server. I sistemi di routing SPA e MVC possono funzionare in modo indipendente senza interferenze. Esiste tuttavia un caso perimetrale che pone problemi: identificare le risposte HTTP 404.

Si consideri lo scenario in cui viene usata una route senza estensione di /some/page . Si supponga che la richiesta non corrisponda a una route lato server, ma il modello corrisponde a una route lato client. Si consideri ora una richiesta in ingresso per /images/user-512.png, che in genere prevede di trovare un file di immagine nel server. Se il percorso della risorsa richiesto non corrisponde a una route o a un file statico sul lato server, è improbabile che l'applicazione sul lato client la gestisca, in genere restituendo un codice di stato HTTP 404.

Prerequisiti degli helper di routing

Installare il pacchetto npm di routing lato client. Uso di Angular come esempio:

npm i -S @angular/router

Configurazione degli helper di routing

Nel metodo viene usato un Configure metodo di estensione denominato MapSpaFallbackRoute :

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");

    routes.MapSpaFallbackRoute(
        name: "spa-fallback",
        defaults: new { controller = "Home", action = "Index" });
});

Le route vengono valutate nell'ordine in cui sono configurate. Di conseguenza, la default route nell'esempio di codice precedente viene usata prima per la corrispondenza dei criteri.

Crea un nuovo progetto

I servizi JavaScript forniscono modelli di applicazione preconfigurati. SpaServices viene usato in questi modelli in combinazione con framework e librerie diversi, ad esempio Angular, React e Redux.

Questi modelli possono essere installati tramite l'interfaccia della riga di comando di .NET Core eseguendo il comando seguente:

dotnet new --install Microsoft.AspNetCore.SpaTemplates::*

Viene visualizzato un elenco dei modelli spa disponibili:

Modelli Nome breve Lingua Tag
MVC ASP.NET Core con Angular angular [C#] Web/MVC/SPA
MVC ASP.NET Core con React.js react [C#] Web/MVC/SPA
MVC ASP.NET Core con React.js e Redux reactredux [C#] Web/MVC/SPA

Per creare un nuovo progetto usando uno dei modelli spa, includere il nome breve del modello nel comando dotnet new . Il comando seguente crea un'applicazione Angular con ASP.NET Core MVC configurato per il lato server:

dotnet new angular

Impostare la modalità di configurazione del runtime

Esistono due modalità di configurazione del runtime primario:

  • Sviluppo:
    • Include mappe di origine per semplificare il debug.
    • Non ottimizza il codice lato client per le prestazioni.
  • Produzione:
    • Esclude le mappe di origine.
    • Ottimizza il codice lato client tramite bundle e minimizzazione.

ASP.NET Core usa una variabile di ambiente denominata ASPNETCORE_ENVIRONMENT per archiviare la modalità di configurazione. Per altre informazioni, vedere Impostare l'ambiente.

Eseguire con l'interfaccia della riga di comando di .NET Core

Ripristinare i pacchetti NuGet e npm necessari eseguendo il comando seguente nella radice del progetto:

dotnet restore && npm i

Compilare ed eseguire l'applicazione:

dotnet run

L'applicazione viene avviata in localhost in base alla modalità di configurazione del runtime. Passando a http://localhost:5000 nel browser viene visualizzata la pagina di destinazione.

Eseguire con Visual Studio 2017

Aprire il .csproj file generato dal comando dotnet new . I pacchetti NuGet e npm necessari vengono ripristinati automaticamente all'apertura del progetto. Questo processo di ripristino può richiedere fino a pochi minuti e l'applicazione è pronta per l'esecuzione al termine. Fare clic sul pulsante di esecuzione verde o premere Ctrl + F5e il browser si apre alla pagina di destinazione dell'applicazione. L'applicazione viene eseguita in localhost in base alla modalità di configurazione del runtime.

Testare l'app

I modelli SpaServices sono preconfigurati per eseguire test sul lato client usando Karma e Jasmine. Jasmine è un framework di unit test diffuso per JavaScript, mentre Karma è un testrunner per tali test. Karma è configurato per funzionare con il middleware di sviluppo Webpack in modo che lo sviluppatore non sia necessario arrestare ed eseguire il test ogni volta che vengono apportate modifiche. Sia che si tratti del codice in esecuzione sul test case o sul test case stesso, il test viene eseguito automaticamente.

Usando l'applicazione Angular come esempio, nel file sono già disponibili CounterComponentcounter.component.spec.ts due test case di Jasmine:

it('should display a title', async(() => {
    const titleText = fixture.nativeElement.querySelector('h1').textContent;
    expect(titleText).toEqual('Counter');
}));

it('should start with count 0, then increments by 1 when clicked', async(() => {
    const countElement = fixture.nativeElement.querySelector('strong');
    expect(countElement.textContent).toEqual('0');

    const incrementButton = fixture.nativeElement.querySelector('button');
    incrementButton.click();
    fixture.detectChanges();
    expect(countElement.textContent).toEqual('1');
}));

Aprire il prompt dei comandi nella directory ClientApp . Esegui questo comando:

npm test

Lo script avvia lo strumento di esecuzione test Karma, che legge le impostazioni definite nel karma.conf.js file. Tra le altre impostazioni, identifica karma.conf.js i file di test da eseguire tramite la relativa files matrice:

module.exports = function (config) {
    config.set({
        files: [
            '../../wwwroot/dist/vendor.js',
            './boot-tests.ts'
        ],

Pubblicazione dell'app

Per altre informazioni sulla pubblicazione in Azure, vedere questo problema di GitHub.

La combinazione degli asset lato client generati e degli artefatti ASP.NET Core pubblicati in un pacchetto pronto per la distribuzione può essere complessa. Per fortuna, SpaServices orchestra l'intero processo di pubblicazione con una destinazione MSBuild personalizzata denominata RunWebpack:

<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
  <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
  <Exec Command="npm install" />
  <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
  <Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />

  <!-- Include the newly-built files in the publish output -->
  <ItemGroup>
    <DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
    <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
      <RelativePath>%(DistFiles.Identity)</RelativePath>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </ResolvedFileToPublish>
  </ItemGroup>
</Target>

La destinazione MSBuild ha le responsabilità seguenti:

  1. Ripristinare i pacchetti npm.
  2. Creare una build di livello di produzione degli asset lato client di terze parti.
  3. Creare una compilazione di livello di produzione degli asset lato client personalizzati.
  4. Copiare gli asset generati da Webpack nella cartella di pubblicazione.

La destinazione MSBuild viene richiamata durante l'esecuzione:

dotnet publish -c Release

Risorse aggiuntive