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 [Ogłoszenie] 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 dla deweloperów do tworzenia SPAs.
Usługi JavaScript składają się z dwóch odrębnych pakietów NuGet:
- Microsoft.AspNetCore.NodeServices (NodeServices)
- Microsoft.AspNetCore.SpaServices (SpaServices)
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 tworzenia aplikacji jednostronicowych z ASP.NET Core i nie wiąże deweloperów z konkretnym frameworkiem 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 programem 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 —Node.js jest zainstalowana i dostępna w środowiskach serwera.
-
- W systemie Windows przy użyciu programu Visual Studio 2017 zestaw SDK jest instalowany przez wybór pakietu roboczego wieloplatformowego programowania .NET Core.
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 Node.js, a następnie delegować dalsze wykonywanie do klienta.
Pomocniki tagów ASP.NET Core udostępniane przez SpaServices upraszczają implementację prerenderingu po stronie serwera poprzez wywołanie funkcji JavaScript na serwerze.
Wymagania wstępne dotyczące renderowania 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 upraszczają komunikację z interfejsami API niskiego poziomu, wykorzystując składnię przypominającą HTML wewnątrz widoku Razor.
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
Pomocnik tagów asp-prerender-module
Pomocnik tagów asp-prerender-module
używany w poprzednim przykładzie kodu wykonuje ClientApp/dist/main-server.js
na serwerze za pośrednictwem 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-server
punktu wejścia, a przechodzenie grafu zależności dla tego aliasu rozpoczyna się od pliku ClientApp/boot-server.ts
.
entry: { 'main-server': './ClientApp/boot-server.ts' },
W poniższym przykładzie platformy Angular plik ClientApp/boot-server.ts
korzysta z funkcji createServerRenderer
i RenderResult
typu pakietu aspnet-prerendering
npm w celu skonfigurowania renderowania serwera za pośrednictwem Node.js. Znacznik HTML przeznaczony do renderowania po stronie serwera jest przekazywany do wywołania funkcji resolve, które jest opakowane w obiekt JavaScript o ściśle określonym typie Promise
. Istotność obiektu Promise
polega na tym, że asynchronicznie dostarcza znacznik HTML do strony, aby wstrzyknąć go w element zastępczy 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 serializatora JSON 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 konfiguracja serializacji JSON jest odpowiedzialna za tę różnicę.
Aby rozwinąć powyższy przykład kodu, dane można przekazać z serwera do widoku, wypełniając właściwość globals
, którą przekazano do funkcji 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();
});
});
});
});
});
Tablica postList
zdefiniowana wewnątrz obiektu globals
jest dołączona do globalnego obiektu przeglądarki window
. 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.
Oprogramowanie pośredniczące Webpack Dev Middleware
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 middleware deweloperskiego 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();
Najpierw należy wywołać metodę rozszerzenia UseWebpackDevMiddleware
, a dopiero potem zarejestrować hostowanie plików statycznych za pomocą metody rozszerzenia UseStaticFiles
. 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
nakazuje middleware obserwować folder dist
w poszukiwaniu 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 Hot Module Replacement (HMR) w Webpacku jako o 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łóciłoby bieżący stan w pamięci oraz sesję 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 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 Webpack Dev Middleware, metodę rozszerzenia UseWebpackDevMiddleware
należy wywołać przed metodą rozszerzenia UseStaticFiles
. 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:
Pomocnicy routingu
W większości SPA opartych na ASP.NET Core routing po stronie klienta jest często pożądany 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ż przychodzące żądanie dla /images/user-512.png
, 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, 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 | Język | Etykiety |
---|---|---|---|
MvC ASP.NET Core z platformą Angular | angular | [C#] | Web/MVC/SPA |
MvC ASP.NET Core z React.js | reagować | [C#] | Web/MVC/SPA |
MVC ASP.NET Core z React.js i Redux | reactredux | [C#] | 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 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.
Uruchom z .NET CLI
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 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.
Używając aplikacji Angular jako przykładu, dwa przypadki testowe Jasmine są już dostępne dla CounterComponent
w pliku 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');
}));
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, karma.conf.js
identyfikuje pliki testowe do wykonania za pośrednictwem swojej tablicy files
.
module.exports = function (config) {
config.set({
files: [
'../../wwwroot/dist/vendor.js',
'./boot-tests.ts'
],
Opublikuj aplikację
Zobacz to zgłoszenie na 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:
- Przywróć pakiety npm.
- Utwórz kompilację produkcyjną zasobów zewnętrznych dostawców po stronie klienta.
- Utwórz kompilację klasy produkcyjnej niestandardowych zasobów po stronie klienta.
- Skopiuj zasoby wygenerowane przez pakiet web do folderu publikowania.
Obiekt docelowy MSBuild jest wywoływany podczas uruchamiania:
dotnet publish -c Release