Delen via


JavaScript Services gebruiken om toepassingen met één pagina te maken in ASP.NET Core

Door Fiyaz Hasan-

Waarschuwing

De functies die in dit artikel worden beschreven, zijn verouderd vanaf ASP.NET Core 3.0. Een eenvoudiger SPA-frameworks-integratiemechanisme is beschikbaar in het Microsoft.AspNetCore.SpaServices.Extensions NuGet-pakket. Zie voor meer informatie [Aankondiging] Het overbodig maken van Microsoft.AspNetCore.SpaServices en Microsoft.AspNetCore.NodeServices.

Een SPA (Single Page Application) is een populair type webtoepassing vanwege de inherente rijke gebruikerservaring. Het integreren van client-side SPA-frameworks of -bibliotheken, zoals Angular of React, met server-side frameworks zoals ASP.NET Core kan lastig zijn. JavaScript Services is ontwikkeld om wrijving in het integratieproces te verminderen. Het maakt naadloze werking mogelijk tussen de verschillende client- en servertechnologiestacks.

Wat is JavaScript Services?

JavaScript Services is een verzameling technologieën aan de clientzijde voor ASP.NET Core. Het doel is om ASP.NET Core te positioneren als het voorkeursplatform aan de serverzijde van ontwikkelaars voor het bouwen van SPA's.

JavaScript Services bestaat uit twee afzonderlijke NuGet-pakketten:

Deze pakketten zijn handig in de volgende scenario's:

  • JavaScript uitvoeren op de server
  • Gebruik een Single Page Application-framework of -bibliotheek
  • Assets aan clientzijde bouwen met Webpack

Veel van de focus in dit artikel wordt gelegd op het gebruik van het SpaServices-pakket.

Wat is SpaServices?

SpaServices is gemaakt om ASP.NET Core als het voorkeursplatform aan de serverzijde van ontwikkelaars voor het ontwikkelen van SPAs te positioneren. SpaServices is niet vereist om SPA's te ontwikkelen met ASP.NET Core en vergrendelt ontwikkelaars niet in een bepaald clientframework.

SpaServices biedt een nuttige infrastructuur zoals:

Gezamenlijk verbeteren deze infrastructuuronderdelen zowel de ontwikkelwerkstroom als de runtime-ervaring. De onderdelen kunnen afzonderlijk worden aangenomen.

Vereisten voor het gebruik van SpaServices

Als u met SpaServices wilt werken, installeert u het volgende:

  • Node.js (versie 6 of hoger) met npm

    • Als u wilt controleren of deze onderdelen zijn geïnstalleerd en u deze kunt vinden, voert u het volgende uit vanaf de opdrachtregel:

      node -v && npm -v
      
    • Als u implementeert op een Azure-website, is er geen actie vereist:Node.js is geïnstalleerd en beschikbaar in de serveromgevingen.

  • .NET Core SDK 2.0 of hoger

    • Op Windows met Visual Studio 2017 wordt de SDK geïnstalleerd door de .NET Core cross-platformontwikkeling workload te selecteren.
  • Microsoft.AspNetCore.SpaServices NuGet-pakket

Prerendering aan de serverzijde

Een universele toepassing (ook wel isomorf genoemd) is een JavaScript-toepassing die zowel op de server als op de client kan worden uitgevoerd. Angular, React en andere populaire frameworks bieden een universeel platform voor deze toepassingsontwikkelingsstijl. Het idee is eerst de frameworkonderdelen op de server weer te geven via Node.jsen vervolgens verdere uitvoering aan de client te delegeren.

ASP.NET Core Tag Helpers geleverd door SpaServices vereenvoudigen de implementatie van prerendering aan de serverzijde door de JavaScript-functies op de server aan te roepen.

Vereisten voor prerendering aan de serverzijde

Installeer het aspnet-prerendering npm-pakket:

npm i -S aspnet-prerendering

Prerenderingsconfiguratie aan de serverzijde

De Tag Helpers kunnen worden gedetecteerd via naamruimteregistratie in het _ViewImports.cshtml-bestand van het project:

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

Deze Tag Helpers abstraheren de complexiteit van het rechtstreeks communiceren met API's op laag niveau door gebruik te maken van een HTML-achtige syntaxis in de Razor weergave:

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

asp-prerender-module Tag Helper

De asp-prerender-module Tag Helper, die in het voorgaande codevoorbeeld wordt gebruikt, voert ClientApp/dist/main-server.js uit op de server via Node.js. In het belang van duidelijkheid is main-server.js bestand een artefact van de Transpilatietaak TypeScript-naar-JavaScript in het Webpack-buildproces. Webpack definieert een toegangspuntalias main-serveren het doorlopen van de afhankelijkheidsgrafiek met deze alias begint in het ClientApp/boot-server.ts bestand.

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

In het volgende Angular-voorbeeld maakt het ClientApp/boot-server.ts bestand gebruik van de functie createServerRenderer en RenderResult type van het aspnet-prerendering npm-pakket om serverweergave te configureren via Node.js. De HTML-markering die is bestemd voor rendering aan de serverzijde, wordt doorgegeven aan een functieaanroep voor oplossen, die is verpakt in een sterk getypeerd JavaScript-Promise-object. De betekenis van het Promise object is dat het asynchroon de HTML-markering levert aan de pagina voor injectie in het tijdelijke aanduidingselement van de 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();
                });
            });
        });
    });
});

asp-prerender-data Tag Helper

In combinatie met de asp-prerender-module Tag Helper kan de asp-prerender-data Tag Helper worden gebruikt om contextuele informatie uit de Razor weergave door te geven aan de JavaScript-serverzijde. Met de volgende markering worden gebruikersgegevens bijvoorbeeld doorgegeven aan de main-server-module:

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

Het ontvangen UserName argument wordt geserialiseerd met behulp van de ingebouwde JSON-serializer en wordt opgeslagen in het params.data-object. In het volgende Angular-voorbeeld worden de gegevens gebruikt om een gepersonaliseerde begroeting te maken binnen een h1 element:

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();
                });
            });
        });
    });
});

Eigenschapsnamen die worden doorgegeven in Tag Helpers, worden weergegeven met PascalCase notatie. Vergelijk dit met JavaScript, waarbij dezelfde eigenschapsnamen worden weergegeven met camelCase. De standaardconfiguratie voor JSON-serialisatie is verantwoordelijk voor dit verschil.

Als u het voorgaande codevoorbeeld wilt uitbreiden, kunnen gegevens van de server worden doorgegeven aan de weergave door de eigenschap globals te hydrateren die aan de resolve functie is verstrekt:

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();
                });
            });
        });
    });
});

De postList matrix die in het globals-object is gedefinieerd, wordt gekoppeld aan het globale window-object van de browser. Deze variabele die naar een globaal bereik wordt ge hoist, elimineert duplicatie van de inspanning, met name omdat het betrekking heeft op het laden van dezelfde gegevens eenmaal op de server en opnieuw op de client.

globale postList-variabele gekoppeld aan vensterobjecten

Webpack Dev Middleware

Webpack Dev Middleware introduceert een gestroomlijnde ontwikkelwerkstroom waarbij Webpack resources op aanvraag bouwt. De middleware compileert automatisch en dient client-side bronnen wanneer een pagina opnieuw wordt geladen in de browser. De alternatieve methode is om webpack handmatig aan te roepen via het npm-buildscript van het project wanneer een afhankelijkheid van derden of de aangepaste code wordt gewijzigd. In het volgende voorbeeld wordt een npm-buildscript in het package.json-bestand weergegeven:

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

Vereisten voor Webpack Dev Middleware

Installeer het aspnet-webpack npm-pakket:

npm i -D aspnet-webpack

Configuratie van Webpack Dev Middleware

Webpack Dev Middleware is geregistreerd in de HTTP-aanvraagpijplijn via de volgende code in de Configure methode van het Startup.cs-bestand:

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

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

De UseWebpackDevMiddleware-extensiemethode moet worden aangeroepen voordat het hosten van statische bestanden registreert via de UseStaticFiles-extensiemethode. Registreer om veiligheidsredenen de middleware alleen wanneer de app wordt uitgevoerd in de ontwikkelmodus.

De eigenschap output.publicPath van het webpack.config.js-bestand instrueert de middleware de map dist in de gaten te houden op wijzigingen.

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

Vervanging van hotmodule

Denk aan de functie Hot Module Replacement (HMR) van Webpack als een evolutie van Webpack Dev Middleware. HMR introduceert dezelfde voordelen, maar het stroomlijnt de ontwikkelwerkstroom verder door pagina-inhoud automatisch bij te werken na het compileren van de wijzigingen. Verwar dit niet met een vernieuwen van de browser, wat de huidige in-memory toestand en foutopsporingssessie van de Single Page Application (SPA) zou verstoren. Er is een livekoppeling tussen de Webpack Dev Middleware-service en de browser. Dit betekent dat wijzigingen naar de browser worden gepusht.

Vereisten voor vervanging van hotmodules

Installeer het webpack-hot-middleware npm-pakket:

npm i -D webpack-hot-middleware

Vervangingsconfiguratie voor hotmodules

Het HMR-onderdeel moet worden geregistreerd bij de HTTP-aanvraagpijplijn van MVC in de Configure methode:

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

Zoals het geval was met Webpack Dev Middleware, moet de UseWebpackDevMiddleware-extensiemethode worden aangeroepen vóór de UseStaticFiles-extensiemethode. Registreer om veiligheidsredenen de middleware alleen wanneer de app wordt uitgevoerd in de ontwikkelmodus.

Het webpack.config.js-bestand moet een plugins matrix definiëren, zelfs als het leeg blijft:

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

Na het laden van de app in de browser, biedt het consoletabblad van de ontwikkelhulpprogramma's een bevestiging van HMR-activering:

Boodschap voor Hot Module Vervanging verbonden

Hulp voor routering

In de meeste ASP.NET Core-SPA's is routering aan de clientzijde vaak gewenst naast routering aan de serverzijde. De SPA- en MVC-routeringssystemen kunnen onafhankelijk werken zonder interferentie. Er is echter één edge-case die uitdagingen met zich mee brengt: het identificeren van 404 HTTP-antwoorden.

Houd rekening met het scenario waarin een extensieloze route van /some/page wordt gebruikt. Stel dat de aanvraag niet overeenkomt met een route aan de serverzijde, maar het patroon komt wel overeen met een route aan de clientzijde. Overweeg nu een binnenkomende aanvraag voor /images/user-512.png, die over het algemeen verwacht een afbeeldingsbestand op de server te vinden. Als het aangevraagde resourcepad niet overeenkomt met een route aan de serverzijde of een statisch bestand, is het onwaarschijnlijk dat de toepassing aan de clientzijde dit zou verwerken. Over het algemeen is het gewenst om een 404 HTTP-statuscode te retourneren.

Vereisten voor routeringshelpers

Installeer het npm-pakket voor routering aan de clientzijde. Angular gebruiken als voorbeeld:

npm i -S @angular/router

Configuratie van routeringshelpers

Een extensiemethode met de naam MapSpaFallbackRoute wordt gebruikt in de methode Configure:

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

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

Routes worden geëvalueerd in de volgorde waarin ze zijn geconfigureerd. Daarom wordt de default route in het voorgaande codevoorbeeld eerst gebruikt voor patroonkoppeling.

Een nieuw project maken

JavaScript Services bieden vooraf geconfigureerde toepassingssjablonen. SpaServices wordt gebruikt in deze sjablonen in combinatie met verschillende frameworks en bibliotheken, zoals Angular, React en Redux.

Deze sjablonen kunnen worden geïnstalleerd via de .NET CLI door de volgende opdracht uit te voeren:

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

Er wordt een lijst met beschikbare Single Page Application-sjablonen weergegeven:

Sjablonen Korte naam Taal Tags
MVC ASP.NET Core met Angular hoekig [C#] Web/MVC/SPA
MVC ASP.NET Core met React.js reageren [C#] Web/MVC/SPA
MVC ASP.NET Core met React.js en Redux reactredux [C#] Web/MVC/SPA

Als u een nieuw project wilt maken met een van de beveiligd-WACHTWOORDVERIFICATIE-sjablonen, neemt u de korte naam van de sjabloon op in de opdracht dotnet new. Met de volgende opdracht maakt u een Angular-toepassing met ASP.NET Core MVC geconfigureerd voor de serverzijde:

dotnet new angular

Stel de runtimeconfiguratiemodus in

Er bestaan twee primaire runtimeconfiguratiemodi:

  • Ontwikkeling:
    • Bevat brontoewijzingen om foutopsporing te vereenvoudigen.
    • Optimaliseert de code aan de clientzijde niet voor prestaties.
  • Productie:
    • Hiermee worden brontoewijzingen uitgesloten.
    • Optimaliseert de code aan de clientzijde via bundeling en minificatie.

ASP.NET Core maakt gebruik van een omgevingsvariabele met de naam ASPNETCORE_ENVIRONMENT om de configuratiemodus op te slaan. Zie De omgeving instellenvoor meer informatie.

Uitvoeren met .NET CLI

Herstel de vereiste NuGet- en NPM-pakketten door de volgende opdracht uit te voeren in de hoofdmap van het project:

dotnet restore && npm i

Bouw en voer de toepassing uit:

dotnet run

De toepassing wordt gestart op localhost volgens de runtimeconfiguratiemodus. Als u in de browser naar http://localhost:5000 navigeert, wordt de landingspagina weergegeven.

Uitvoeren met Visual Studio 2017

Open het .csproj bestand dat is gegenereerd door de opdracht dotnet new. De vereiste NuGet- en NPM-pakketten worden automatisch hersteld wanneer het project is geopend. Dit herstelproces kan enkele minuten duren en de toepassing kan worden uitgevoerd wanneer deze is voltooid. Klik op de groene knop Uitvoeren of druk op Ctrl + F5en de browser wordt geopend op de landingspagina van de toepassing. De toepassing wordt uitgevoerd op localhost volgens de runtime-configuratiemodus.

De app testen

SpaServices-sjablonen zijn vooraf geconfigureerd voor het uitvoeren van tests aan de clientzijde met behulp van Karma en Jasmine-. Jasmine is een populair framework voor eenheidstests voor JavaScript, terwijl Karma een testloper is voor deze tests. Karma is geconfigureerd om te werken met de Webpack Dev Middleware zodat de ontwikkelaar niet hoeft te stoppen en de test uit te voeren telkens wanneer er wijzigingen worden aangebracht. Of het nu gaat om de code die wordt uitgevoerd voor de testcase of de testcase zelf, de test wordt automatisch uitgevoerd.

Met behulp van de Angular-toepassing als voorbeeld zijn er al twee Jasmine-testcases opgegeven voor de CounterComponent in het bestand counter.component.spec.ts:

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');
}));

Open de opdrachtprompt in de map ClientApp. Voer de volgende opdracht uit:

npm test

Het script start de Karma-testrunner, die de instellingen leest die zijn gedefinieerd in het bestand karma.conf.js. Onder andere identificeert de karma.conf.js de testbestanden die moeten worden uitgevoerd via de files array.

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

De app publiceren

Zie dit GitHub-issue voor meer gegevens over het publiceren naar Azure.

Het combineren van de gegenereerde assets aan de clientzijde en de gepubliceerde ASP.NET Core-artefacten in een kant-en-klaar pakket kan lastig zijn. Gelukkig organiseert SpaServices dat hele publicatieproces met een aangepast MSBuild-doel met de naam 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>

Het MSBuild-doel heeft de volgende verantwoordelijkheden:

  1. Herstel de npm-pakketten.
  2. Maak een build op productieniveau van de assets aan de clientzijde van derden.
  3. Maak een build op productieniveau van de aangepaste assets aan de clientzijde.
  4. Kopieer de door Webpack gegenereerde assets naar de publicatiemap.

Het MSBuild-doel wordt aangeroepen bij het uitvoeren:

dotnet publish -c Release

Aanvullende informatiebronnen