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:
- Microsoft.AspNetCore.NodeServices (NodeServices)
- Microsoft.AspNetCore.SpaServices (SpaServices)
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:
- Prerendering lato server
- Webpack Dev Middleware
- Sostituzione dei moduli ad accesso frequente
- Helper di routing
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:
Nodo.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 è 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.
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 ejs 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-server
e 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 di 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-module
asp-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 JSON predefinito e 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 JSON predefinita è 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.
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:
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 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
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 + F5
e 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 CounterComponent
counter.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:
- Ripristinare i pacchetti npm.
- Creare una build di livello di produzione degli asset lato client di terze parti.
- Creare una compilazione di livello di produzione degli asset lato client personalizzati.
- Copiare gli asset generati da Webpack nella cartella di pubblicazione.
La destinazione MSBuild viene richiamata durante l'esecuzione:
dotnet publish -c Release