Tworzenie aplikacji jednostronicowych w usłudze ASP.NET Core za pomocą usług JavaScript

Autor: Fiyaz Hasan

Ostrzeżenie

Funkcje opisane w tym artykule są przestarzałe w wersji ASP.NET Core 3.0. Prostszy mechanizm integracji struktur SPA jest dostępny w pakiecie NuGet Microsoft.AspNetCore.SpaServices.Extensions . Aby uzyskać więcej informacji, zobacz [Anons] Obsoleting Microsoft.AspNetCore.SpaServices i Microsoft.AspNetCore.NodeServices.

Aplikacja jednostronicowa (SPA) to popularny typ aplikacji internetowej ze względu na jej bogate środowisko użytkownika. Integrowanie struktur SPA po stronie klienta lub bibliotek, takich jak Angular lub React, z platformami po stronie serwera, takimi jak ASP.NET Core, może być trudne. Usługi JavaScript zostały opracowane w celu zmniejszenia problemów w procesie integracji. Umożliwia bezproblemową operację między różnymi stosami technologii klienta i serwera.

Co to są usługi JavaScript

Usługi JavaScript to zbiór technologii po stronie klienta dla platformy ASP.NET Core. Jego celem jest pozycjonowanie ASP.NET Core jako preferowanej platformy po stronie serwera deweloperów do tworzenia spAs.

Usługi JavaScript składają się z dwóch odrębnych pakietów NuGet:

Te pakiety są przydatne w następujących scenariuszach:

  • Uruchamianie języka JavaScript na serwerze
  • Korzystanie z struktury SPA lub biblioteki
  • Tworzenie zasobów po stronie klienta przy użyciu pakietu WebPack

Większość uwagi w tym artykule znajduje się na korzystaniu z pakietu SpaServices.

Co to jest SpaServices

SpaServices został stworzony, aby umieścić ASP.NET Core jako preferowana platforma po stronie serwera dla deweloperów do tworzenia SPAs. SpaServices nie jest wymagany do opracowywania umów SPA z ASP.NET Core i nie blokuje deweloperów w określonej strukturze klienta.

SpaServices zapewnia przydatną infrastrukturę, taką jak:

Zbiorczo te składniki infrastruktury zwiększają zarówno przepływ pracy programowania, jak i środowisko uruchomieniowe. Składniki można stosować indywidualnie.

Wymagania wstępne dotyczące korzystania z usługi SpaServices

Aby pracować z usługami SpaServices, zainstaluj następujące elementy:

  • Node.js (wersja 6 lub nowsza) z serwerem npm

    • Aby sprawdzić, czy te składniki są zainstalowane i można je znaleźć, uruchom następujące polecenie w wierszu polecenia:

      node -v && npm -v
      
    • W przypadku wdrażania w witrynie internetowej platformy Azure nie jest wymagana żadna akcja — środowisko Node.js jest zainstalowane i dostępne w środowiskach serwera.

  • Zestaw .NET Core SDK 2.0 lub nowszy

    • W systemie Windows przy użyciu programu Visual Studio 2017 zestaw SDK jest instalowany, wybierając pakiet roboczy programowanie dla wielu platform .NET Core.
  • Pakiet NuGet Microsoft.AspNetCore.SpaServices

Prerendering po stronie serwera

Aplikacja uniwersalna (znana również jako izomorficzne) to aplikacja JavaScript, która może działać zarówno na serwerze, jak i na kliencie. Platforma Angular, React i inne popularne platformy zapewniają platformę uniwersalną dla tego stylu tworzenia aplikacji. Chodzi o to, aby najpierw renderować składniki struktury na serwerze za pośrednictwem środowiska Node.js, a następnie delegować dalsze wykonywanie do klienta.

ASP.NET Podstawowe pomocniki tagów udostępniane przez spaServices upraszczają implementację prerenderingu po stronie serwera przez wywoływanie funkcji JavaScript na serwerze.

Wymagania wstępne wstępne po stronie serwera

Zainstaluj pakiet npm aspnet-prerendering:

npm i -S aspnet-prerendering

Konfiguracja prerenderingu po stronie serwera

Pomocnicy tagów są odnajdywalni za pośrednictwem rejestracji przestrzeni nazw w pliku projektu _ViewImports.cshtml :

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

Te pomocniki tagów oddzielają zawiłości komunikacji bezpośrednio z interfejsami API niskiego poziomu, wykorzystując składnię przypominającą kod HTML wewnątrz Razor widoku:

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

Pomocnik tagów asp-prerender-module

Pomocnik asp-prerender-module tagów używany w poprzednim przykładzie kodu jest wykonywany ClientApp/dist/main-server.js na serwerze za pośrednictwem pliku Node.js. Aby uzyskać jasność, main-server.js plik jest artefaktem zadania transpilacji TypeScript-to-JavaScript w procesie kompilacji pakietu Webpack . Pakiet Webpack definiuje alias main-serverpunktu wejścia ; i przechodzenie grafu zależności dla tego aliasu ClientApp/boot-server.ts rozpoczyna się od pliku:

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

W poniższym przykładzie ClientApp/boot-server.ts platformy Angular plik korzysta z createServerRenderer funkcji i RenderResult typu aspnet-prerendering pakietu npm w celu skonfigurowania renderowania serwera za pośrednictwem biblioteki Node.js. Znacznik HTML przeznaczony do renderowania po stronie serwera jest przekazywany do wywołania funkcji rozpoznawania, które jest opakowane w silnie typizowanego obiektu JavaScript Promise . Istotność Promise obiektu polega na tym, że asynchronicznie dostarcza znacznik HTML do strony w celu wstrzyknięcia elementu zastępczego 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();
                });
            });
        });
    });
});

Pomocnik tagów asp-prerender-data

W połączeniu z pomocnikiem tagów asp-prerender-module pomocnik tagów asp-prerender-data może służyć do przekazywania informacji kontekstowych z Razor widoku do języka JavaScript po stronie serwera. Na przykład następujący znacznik przekazuje dane użytkownika do modułu main-server :

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

Otrzymany UserName argument jest serializowany przy użyciu wbudowanego JSserializatora ON i jest przechowywany w params.data obiekcie. W poniższym przykładzie usługi Angular dane są używane do konstruowania spersonalizowanego powitania w elemecie h1 :

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

Nazwy właściwości przekazane w pomocnikach tagów są reprezentowane za pomocą notacji PascalCase . Porównaj to z językiem JavaScript, w którym te same nazwy właściwości są reprezentowane przy użyciu biblioteki camelCase. Domyślna JSkonfiguracja serializacji ON jest odpowiedzialna za tę różnicę.

Aby rozwinąć powyższy przykład kodu, dane można przekazać z serwera do widoku, nawilżając globals właściwość udostępnioną resolve funkcji:

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

Tablica zdefiniowana postList wewnątrz globals obiektu jest dołączona do obiektu globalnego window przeglądarki. Ta zmienna wciągnięta do zakresu globalnego eliminuje duplikowanie nakładu pracy, szczególnie w odniesieniu do ładowania tych samych danych raz na serwerze i ponownie na kliencie.

global postList variable attached to window object

Oprogramowanie pośredniczące pakietu WebPack

Pakiet Webpack Dev Middleware wprowadza usprawniony przepływ pracy tworzenia, w którym pakiet Webpack tworzy zasoby na żądanie. Oprogramowanie pośredniczące automatycznie kompiluje i obsługuje zasoby po stronie klienta po ponownym załadowaniu strony w przeglądarce. Alternatywne podejście polega na ręcznym wywołaniu pakietu Webpack za pośrednictwem skryptu kompilacji npm projektu, gdy zależność innej firmy lub niestandardowy kod ulegnie zmianie. Skrypt kompilacji npm w package.json pliku jest pokazany w poniższym przykładzie:

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

Wymagania wstępne dotyczące oprogramowania deweloperskiego pakietu WebPack

Zainstaluj pakiet npm aspnet-webpack:

npm i -D aspnet-webpack

Konfiguracja oprogramowania pośredniczącego pakietu WebPack

Oprogramowanie WebPack Dev Middleware jest rejestrowane w potoku żądania HTTP za pomocą następującego kodu w Startup.cs metodzie Configure pliku:

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

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

Przed UseWebpackDevMiddleware zarejestrowaniem hostowania plików statycznych za pomocą UseStaticFiles metody rozszerzenia należy wywołać metodę rozszerzenia. Ze względów bezpieczeństwa zarejestruj oprogramowanie pośredniczące tylko wtedy, gdy aplikacja działa w trybie programowania.

Właściwość webpack.config.js pliku output.publicPath informuje oprogramowanie pośredniczące o obserwowaniu folderu pod kątem dist zmian:

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

Wymiana gorącego modułu

Pomyśl o funkcji wymiany hot module pakietu Webpack (HMR) jako ewolucji oprogramowania Webpack Dev Middleware. HmR wprowadza wszystkie te same korzyści, ale dodatkowo usprawnia przepływ pracy tworzenia, automatycznie aktualizując zawartość strony po skompilowaniu zmian. Nie należy mylić tego z odświeżaniem przeglądarki, co zakłóca bieżącego stanu w pamięci i sesji debugowania SPA. Istnieje połączenie na żywo między usługą Webpack Dev Middleware a przeglądarką, co oznacza, że zmiany są wypychane do przeglądarki.

Wymagania wstępne dotyczące wymiany modułu gorącego

Zainstaluj pakiet npm oprogramowania npm webpack-hot-middleware:

npm i -D webpack-hot-middleware

Konfiguracja wymiany modułu gorącego

Składnik HMR musi być zarejestrowany w potoku żądania HTTP MVC w metodzie Configure :

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

Podobnie jak w przypadku oprogramowania Webpack Dev Middleware, UseWebpackDevMiddleware należy wywołać metodę UseStaticFiles rozszerzenia przed metodą rozszerzenia. Ze względów bezpieczeństwa zarejestruj oprogramowanie pośredniczące tylko wtedy, gdy aplikacja działa w trybie programowania.

Plik webpack.config.js musi zdefiniować tablicę plugins , nawet jeśli pozostanie ona pusta:

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

Po załadowaniu aplikacji w przeglądarce karta Konsola narzędzi deweloperskich zapewnia potwierdzenie aktywacji HMR:

Hot Module Replacement connected message

Pomocnicy routingu

W większości ASP.NET opartych na rdzeniach SPA routing po stronie klienta jest często wymagany oprócz routingu po stronie serwera. Systemy routingu SPA i MVC mogą działać niezależnie bez zakłóceń. Istnieje jednak jeden przypadek brzegowy, który stwarza wyzwania: identyfikowanie odpowiedzi HTTP 404.

Rozważmy scenariusz, w którym jest używana trasa /some/page bez rozszerzenia. Załóżmy, że żądanie nie pasuje do trasy po stronie serwera, ale jego wzorzec jest zgodny z trasą po stronie klienta. Teraz rozważ żądanie przychodzące dla /images/user-512.pngelementu , które zwykle oczekuje znalezienia pliku obrazu na serwerze. Jeśli żądana ścieżka zasobu nie jest zgodna z żadną trasą po stronie serwera lub plikiem statycznym, jest mało prawdopodobne, aby aplikacja po stronie klienta go obsłużyła — zwykle zwracany jest kod stanu HTTP 404.

Wymagania wstępne pomocników routingu

Zainstaluj pakiet npm routingu po stronie klienta. Używanie platformy Angular jako przykładu:

npm i -S @angular/router

Konfiguracja pomocników routingu

Metoda rozszerzenia o nazwie MapSpaFallbackRoute jest używana w metodzie 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" });
});

Trasy są oceniane w kolejności, w której są skonfigurowane. default W związku z tym trasa w poprzednim przykładzie kodu jest używana jako pierwsza do dopasowywania wzorca.

Tworzenie nowego projektu

Usługi JavaScript udostępniają wstępnie skonfigurowane szablony aplikacji. SpaServices jest używany w tych szablonach w połączeniu z różnymi strukturami i bibliotekami, takimi jak Angular, React i Redux.

Te szablony można zainstalować za pomocą interfejsu wiersza polecenia platformy .NET Core, uruchamiając następujące polecenie:

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

Zostanie wyświetlona lista dostępnych szablonów SPA:

Szablony Krótka nazwa Language Tagi
MvC ASP.NET Core z platformą Angular Kątowe [C#] Sieć Web/MVC/SPA
MvC ASP.NET Core z platformą React.js Reagować [C#] Sieć Web/MVC/SPA
MvC ASP.NET Core z platformami React.js i Redux reactredux [C#] Sieć Web/MVC/SPA

Aby utworzyć nowy projekt przy użyciu jednego z szablonów SPA, dołącz krótką nazwę szablonu w dotnet new polecenia. Następujące polecenie tworzy aplikację Angular z ASP.NET Core MVC skonfigurowanym na potrzeby po stronie serwera:

dotnet new angular

Ustawianie trybu konfiguracji środowiska uruchomieniowego

Istnieją dwa podstawowe tryby konfiguracji środowiska uruchomieniowego:

  • Programowanie:
    • Zawiera mapy źródłowe ułatwiające debugowanie.
    • Nie optymalizuje kodu po stronie klienta pod kątem wydajności.
  • Produkcja:
    • Wyklucza mapy źródłowe.
    • Optymalizuje kod po stronie klienta przez łączenie i minimalizowanie.

ASP.NET Core używa zmiennej środowiskowej o nazwie ASPNETCORE_ENVIRONMENT do przechowywania trybu konfiguracji. Aby uzyskać więcej informacji, zobacz Ustawianie środowiska.

Uruchamianie przy użyciu interfejsu wiersza polecenia platformy .NET Core

Przywróć wymagane pakiety NuGet i npm, uruchamiając następujące polecenie w katalogu głównym projektu:

dotnet restore && npm i

Skompiluj i uruchom aplikację:

dotnet run

Aplikacja jest uruchamiana na hoście lokalnym zgodnie z trybem konfiguracji środowiska uruchomieniowego. Nawigowanie do http://localhost:5000 w przeglądarce powoduje wyświetlenie strony docelowej.

Uruchamianie przy użyciu programu Visual Studio 2017

.csproj Otwórz plik wygenerowany przez polecenie dotnet new. Wymagane pakiety NuGet i npm są przywracane automatycznie po otwarciu projektu. Ten proces przywracania może potrwać do kilku minut, a aplikacja jest gotowa do uruchomienia po zakończeniu. Kliknij zielony przycisk uruchamiania lub naciśnij klawisz Ctrl + F5, a przeglądarka zostanie otwarta na stronie docelowej aplikacji. Aplikacja działa na hoście lokalnym zgodnie z trybem konfiguracji środowiska uruchomieniowego.

Testowanie aplikacji

Szablony SpaServices są wstępnie skonfigurowane do uruchamiania testów po stronie klienta przy użyciu Karmy i Jasmine. Jasmine to popularna platforma testowania jednostkowego dla języka JavaScript, natomiast Karma jest modułem uruchamiającym testy dla tych testów. Karma jest skonfigurowana do pracy z oprogramowaniem Webpack Dev Middleware , tak aby deweloper nie był wymagany do zatrzymywania i uruchamiania testu za każdym razem, gdy zostaną wprowadzone zmiany. Niezależnie od tego, czy jest to kod uruchomiony względem przypadku testowego, czy sam przypadek testowy, test jest uruchamiany automatycznie.

Na przykład przy użyciu aplikacji Angular dwa przypadki testowe Jasmine są już dostępne CounterComponent w counter.component.spec.ts pliku :

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

Otwórz wiersz polecenia w katalogu ClientApp . Uruchom następujące polecenie:

npm test

Skrypt uruchamia moduł uruchamiający test Karma, który odczytuje ustawienia zdefiniowane w karma.conf.js pliku. Między innymi ustawienia identyfikują pliki testowe do karma.conf.js wykonania za pośrednictwem tablicy files :

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

Opublikuj aplikację

Zobacz ten problem z usługą GitHub, aby uzyskać więcej informacji na temat publikowania na platformie Azure.

Łączenie wygenerowanych zasobów po stronie klienta i opublikowanych artefaktów ASP.NET Core w pakiet gotowy do wdrożenia może być uciążliwe. Na szczęście SpaServices organizuje cały proces publikacji z niestandardowym obiektem docelowym MSBuild o nazwie 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>

Docelowy program MSBuild ma następujące obowiązki:

  1. Przywróć pakiety npm.
  2. Utwórz kompilację klasy produkcyjnej zasobów innych firm po stronie klienta.
  3. Utwórz kompilację klasy produkcyjnej niestandardowych zasobów po stronie klienta.
  4. Skopiuj zasoby wygenerowane przez pakiet web do folderu publikowania.

Obiekt docelowy MSBuild jest wywoływany podczas uruchamiania:

dotnet publish -c Release

Dodatkowe zasoby