Dela via


Använda JavaScript-tjänster för att skapa ensidesprogram i ASP.NET Core

Av Fiyaz Hasan

Varning

Funktionerna som beskrivs i den här artikeln är föråldrade från och med ASP.NET Core 3.0. En enklare integreringsmekanism för SPA-ramverk finns i Microsoft.AspNetCore.SpaServices.Extensions NuGet-paketet. För mer information, se [Meddelande] Föråldrande Microsoft.AspNetCore.SpaServices och Microsoft.AspNetCore.NodeServices.

Ett spa-program (Single Page Application) är en populär typ av webbprogram på grund av dess inneboende omfattande användarupplevelse. Det kan vara svårt att integrera spa-ramverk eller bibliotek på klientsidan, till exempel Angular eller React, med ramverk på serversidan som ASP.NET Core. JavaScript Services har utvecklats för att minska friktionen i integreringsprocessen. Det möjliggör sömlös drift mellan de olika klient- och serverteknikstackarna.

Vad är JavaScript-tjänster

JavaScript Services är en samling tekniker på klientsidan för ASP.NET Core. Målet är att placera ASP.NET Core som utvecklarnas föredragna plattform på serversidan för att skapa SPA:er.

JavaScript Services består av två distinkta NuGet-paket:

Dessa paket är användbara i följande scenarier:

  • Kör JavaScript på servern
  • Använda ett SPA-ramverk eller -bibliotek
  • Skapa tillgångar på klientsidan med Webpack

Mycket av fokus i den här artikeln ligger på att använda SpaServices-paketet.

Vad är SpaServices

SpaServices skapades för att positionera ASP.NET Core som utvecklarnas föredragna plattform på serversidan för att skapa SPA:er. SpaServices krävs inte för att utveckla SPA:er med ASP.NET Core, och det låser inte utvecklare till ett visst klientramverk.

SpaServices tillhandahåller användbar infrastruktur som:

Tillsammans förbättrar dessa infrastrukturkomponenter både utvecklingsarbetsflödet och körningsmiljön. Komponenterna kan användas individuellt.

Förutsättningar för att använda SpaServices

Installera följande för att arbeta med SpaServices:

  • Node.js (version 6 eller senare) med npm

    • Kontrollera att dessa komponenter är installerade och kan hittas genom att köra följande från kommandoraden:

      node -v && npm -v
      
    • Om du distribuerar till en Azure-webbplats krävs ingen åtgärd–Node.js installeras och är tillgänglig i servermiljöerna.

  • .NET Core SDK 2.0 eller senare

    • På Windows med Visual Studio 2017 installeras SDK:et genom att välja .NET Core utveckling för flera plattformar arbetsbelastning.
  • Microsoft.AspNetCore.SpaServices NuGet-paket

Förinläsning på serversidan

Ett universellt program (även kallat isomorft) är ett JavaScript-program som kan köras både på servern och klienten. Angular, React och andra populära ramverk är en universell plattform för den här programutvecklingsstilen. Tanken är att först återge ramverkskomponenterna på servern via Node.jsoch sedan delegera ytterligare körning till klienten.

ASP.NET Core Tag Helpers tillhandahålls av SpaServices förenklar implementeringen av serversidans prerendering genom att anropa JavaScript-funktionerna på servern.

Grundkrav för serverside-förhandsrendering

Installera aspnet-prerendering npm-paketet:

npm i -S aspnet-prerendering

Konfiguration av prerendering på serversidan

Tagghjälparna kan identifieras via namnområdesregistrering i projektets _ViewImports.cshtml-fil:

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

Dessa Tag Helpers abstraherar bort invecklingarna i kommunikationen direkt med API:er på låg nivå genom att använda en HTML-liknande syntax i vyn Razor:

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

asp-prerender-module Tag Helper (Etikett Hjälpare)

asp-prerender-module Tag Helper, som används i föregående kodexempel, kör ClientApp/dist/main-server.js på servern via Node.js. För tydlighetens skull är main-server.js fil en artefakt av transpileringsuppgiften TypeScript-till-JavaScript i Webpack-byggprocessen. Webpack definierar ett startpunktsalias för main-server; och traversering av beroendediagrammet för det här aliaset börjar vid ClientApp/boot-server.ts-filen:

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

I följande Angular-exempel använder ClientApp/boot-server.ts-filen funktionen createServerRenderer och RenderResult typ av aspnet-prerendering npm-paketet för att konfigurera serverrendering via Node.js. HTML-koden som är avsedd för återgivning på serversidan skickas till ett lösningsfunktionsanrop, som är inslaget i ett starkt skrivet JavaScript-Promise-objekt. Det Promise objektets betydelse är att det asynkront levererar HTML-markering till sidan för inmatning i DOM:s platshållarelement.

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

I kombination med asp-prerender-module Tag Helper kan asp-prerender-data Tag Helper användas för att skicka kontextuell information från Razor-vyn till JavaScript på serversidan. Följande markering skickar till exempel användardata till modulen main-server:

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

Argumentet mottaget UserName serialiseras med den inbyggda JSON-serialiseraren och lagras i objektet params.data. I följande Angular-exempel används data för att skapa en personlig hälsning i ett 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();
                });
            });
        });
    });
});

Egenskapsnamn som skickas i Tag Helpers representeras med PascalCase notation. Jämför det med JavaScript, där samma egenskapsnamn representeras med camelCase. Standardkonfigurationen för JSON-serialisering ansvarar för den här skillnaden.

För att utöka det tidigare kodexemplet kan data skickas från servern till gränssnittet genom att hydrera egenskapen globals som tillhandahålls till funktionen resolve.

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

Den postList matris som definierats i globals-objektet är kopplad till webbläsarens globala window-objekt. Den här variabelhissningen till det globala omfånget eliminerar duplicering av arbete, särskilt eftersom den gäller inläsning av samma data en gång på servern och igen på klienten.

global postList-variabel som är kopplad till fönsterobjektet

Webpack Dev Middleware

Webpack Dev Middleware introducerar ett effektivt arbetsflöde för utveckling där Webpack skapar resurser på begäran. Mellanprogrammet kompilerar och hanterar resurser på klientsidan automatiskt när en sida läses in på nytt i webbläsaren. Den alternativa metoden är att manuellt anropa Webpack via projektets npm-byggskript när ett beroende från tredje part eller den anpassade koden ändras. Ett npm-byggskript i package.json-filen visas i följande exempel:

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

Krav för Webpack Dev Middleware

Installera aspnet-webpack npm-paketet:

npm i -D aspnet-webpack

Konfiguration av Webpack Dev Middleware

Webpack Dev Middleware registreras i HTTP-begärandepipelinen via följande kod i Startup.cs-filens Configure-metod:

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

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

UseWebpackDevMiddleware-tilläggsmetoden måste anropas innan registrering av statiska filvärdar via UseStaticFiles-tilläggsmetoden. Av säkerhetsskäl registrerar du endast mellanprogrammet när appen körs i utvecklingsläge.

webpack.config.js-filens egenskap output.publicPath instruerar mellanlagret att övervaka mappen dist för ändringar.

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

Ersättning av frekvent modul

Tänk på webpacks HMR-funktion (Hot Module Replacement) som en utveckling av Webpack Dev Middleware. HMR medför samma fördelar, men det effektiviserar utvecklingsarbetsflödet ytterligare genom att automatiskt uppdatera sidinnehållet efter kompileringen av ändringarna. Blanda inte ihop detta med en uppdatering av webbläsaren, vilket skulle störa det aktuella minnesinterna tillståndet och felsökningssessionen för SPA. Det finns en livelänk mellan Tjänsten Webpack Dev Middleware och webbläsaren, vilket innebär att ändringar skickas till webbläsaren.

Förutsättningar för Hot Module Replacement

Installera webpack-hot-middleware npm-paketet:

npm i -D webpack-hot-middleware

Konfiguration av ersättning av frekventa moduler

HMR-komponenten måste registreras i MVC:s HTTP-begärandepipeline i metoden Configure:

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

Precis som med Webpack Dev Middlewaremåste UseWebpackDevMiddleware-tilläggsmetoden anropas före UseStaticFiles-tilläggsmetoden. Av säkerhetsskäl registrerar du endast mellanprogrammet när appen körs i utvecklingsläge.

Filen webpack.config.js måste definiera en plugins matris, även om den lämnas tom:

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

När appen har lästs in i webbläsaren visar fliken Konsol för utvecklarverktyg bekräftelse på HMR-aktivering:

het modulbyte anslutningsmeddelande

Routningshjälpare

I de flesta ASP.NET Core-baserade SPA:er är routning på klientsidan ofta önskvärd utöver routning på serversidan. SPA- och MVC-routningssystemen kan fungera oberoende av varandra utan interferens. Det finns dock ett gränsfall som utgör utmaningar: att identifiera 404 HTTP-svar.

Tänk på scenariot där en tilläggslös väg för /some/page används. Anta att begäran inte mönstermatchar en väg på serversidan, men dess mönster matchar en väg på klientsidan. Tänk nu på en inkommande begäran för /images/user-512.png, som vanligtvis förväntar sig att hitta en bildfil på servern. Om den begärda resurssökvägen inte matchar någon väg på serversidan eller en statisk fil är det osannolikt att programmet på klientsidan skulle hantera den– vanligtvis är det önskvärt att returnera en HTTP-statuskod på 404.

Förutsättningar för ruttplaneringshjälpmedel

Installera NPM-paketet för routning på klientsidan. Använda Angular som exempel:

npm i -S @angular/router

Konfiguration av routningshjälpare

En tilläggsmetod med namnet MapSpaFallbackRoute används i metoden 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" });
});

Vägar utvärderas i den ordning de konfigureras. Följaktligen används rutt default i föregående kodexempel först för mönstermatchning.

Skapa ett nytt projekt

JavaScript Services tillhandahåller förkonfigurerade programmallar. SpaServices används i dessa mallar tillsammans med olika ramverk och bibliotek som Angular, React och Redux.

Dessa mallar kan installeras via .NET CLI genom att köra följande kommando:

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

En lista över tillgängliga SPA-mallar visas:

Mallar Kort namn Språk Taggar
MVC ASP.NET Core med Angular kantig [C#] Webb/MVC/SPA
MVC ASP.NET Core med React.js reagera [C#] Webb/MVC/SPA
MVC ASP.NET Core med React.js och Redux reactredux [C#] Webb/MVC/SPA

Om du vill skapa ett nytt projekt med någon av SPA-mallarna inkluderar du Kortnamn för mallen i kommandot dotnet new. Följande kommando skapar ett Angular-program med ASP.NET Core MVC konfigurerat för serversidan:

dotnet new angular

Ange körningskonfigurationsläget

Det finns två primära körningskonfigurationslägen:

  • Utveckling:
    • Innehåller källkartor för att underlätta felsökning.
    • Optimerar inte koden på klientsidan för prestanda.
  • Production:
    • Exkluderar källkartor.
    • Optimerar koden på klientsidan via paketering och minifiering.

ASP.NET Core använder en miljövariabel med namnet ASPNETCORE_ENVIRONMENT för att lagra konfigurationsläget. Mer information finns i Ange miljön.

Kör med .NET CLI

Återställ nödvändiga NuGet- och npm-paket genom att köra följande kommando i projektroten:

dotnet restore && npm i

Skapa och kör programmet:

dotnet run

Programmet startar på localhost enligt körningskonfigurationsläge. När du navigerar till http://localhost:5000 i webbläsaren visas landningssidan.

Kör med Visual Studio 2017

Öppna .csproj-filen som genereras av kommandot dotnet new. Nödvändiga NuGet- och npm-paket återställs automatiskt när projektet öppnas. Den här återställningsprocessen kan ta upp till några minuter och programmet är redo att köras när det är klart. Klicka på den gröna körningsknappen eller tryck på Ctrl + F5, så öppnas webbläsaren på programmets landningssida. Programmet körs på localhost enligt körningskonfigurationsläge.

Testa appen

SpaServices-mallar är förkonfigurerade för att köra tester på klientsidan med hjälp av Karma och Jasmine. Jasmine är ett populärt ramverk för enhetstestning för JavaScript, medan Karma är en testlöpare för dessa tester. Karma har konfigurerats för att fungera med Webpack Dev Middleware så att utvecklaren inte behöver stoppa och köra testet varje gång ändringar görs. Oavsett om det är koden som körs mot testfallet eller själva testfallet körs testet automatiskt.

Med Angular-programmet som exempel tillhandahålls redan två Jasmine-testfall för CounterComponent i filen 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');
}));

Öppna kommandotolken i katalogen ClientApp. Kör följande kommando:

npm test

Skriptet startar Karma-testkören, som läser de inställningar som definierats i filen karma.conf.js. Bland andra inställningar identifierar karma.conf.js testfilerna som ska köras via dess files matris:

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

Publicera appen

Se det här GitHub-ärendet för mer information om publicering till Azure.

Det kan vara besvärligt att kombinera de genererade tillgångarna på klientsidan och de publicerade ASP.NET Core-artefakterna till ett färdigt distributionspaket. Tack och lov dirigerar SpaServices hela publiceringsprocessen med ett anpassat MSBuild-mål med namnet 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>

MSBuild-målet har följande ansvarsområden:

  1. Återställ npm-paketen.
  2. Skapa en produktionsfärdig version av klientsidans tredjepartsresurser.
  3. Skapa en produktionsfärdig byggversion av de anpassade resurserna på klientsidan.
  4. Kopiera de Webpack-genererade tillgångarna till publiceringsmappen.

MSBuild-målet anropas när du kör kommandot:

dotnet publish -c Release

Ytterligare resurser