Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 2.

В предыдущей части мы создали простой каркас веб сайта, который использует ASP.NET MVC и Angular. Но основная проблема – у нас как бы 2 приложения. Одно – это ASP.NET MVC, которое можно вызвать при помощи URL /home/index, а второе приложение – это Angular, которое доступно запускается из файла /index.html. Теперь давайте попробуем соединить эти два приложения в одно.

Тесная интеграция ASP.NET MVC и Angular

Итак, у нас есть рабочий проект. Многие останавливаются на этом этапе и строят свое приложение вокруг файла index.html используя обычные HTML странички. Некоторые идут далее и используют серверный рендеринг Angular кода. Это отличная вещь, особенно для высоконагруженных сайтов, но такой подход добавляет дополнительную сложность в проект, заставляет поддерживать намного больше кода и утилит. И еще одна реализация – создание RESTful API используя ASP.NET Web API и использовать его для обслуживания Angular приложения. Но все эти методы оставляют большую часть возможностей ASP.NET MVC Core за бортом и просто не используют их.

Как альтернативу вы можете использовать частичные представления (partial views) ASP.NET MVC, используя обычные контроллеры (Controllers) MVC и действия (Actions) для выполнения серверного кода C#, использовать разметку Razor и custom tag helpers в своем представлении (Views) и использовать кэширование на стороне сервера ASP.NET Core — все это вместе может рендерить и отсылать ваши HTML-шаблоны для приложения Angular. Однако шаблоны уже не в виде статического HTML, а как часть конвейера, который предоставляет множество дополнительных возможностей по тонкой настройки обработки запросов.

Хотя ASP.NET Web API и RESTful протокол тоже имеют право на жизнь.

Вот я и попробую реализовать этот метод для более тесной интеграции ASP.NET MVC Core и Angular. Начнем с перемещения домашней страницы Angular, страницы index.html в Home контроллер и представление index.cshtml, перемещения общих разделяемых библиотек в общие представления (Shared Views) и создания контроллера MVC, который будет поставлять частичные представления (partial views) и замены встроенных и внешних Angular шаблонов.

Использование ASP.NET Core совместно с Angular

Итак, давайте добавим пользовательский tag helper, который будет использоваться для предварительного заполнения Angular шаблонов HTML, стилями и кодом/разметкой для проверки данных. Данные не будут предварительно загружаться в шаблоны, вместо этого мы создадим службы RESTful в ASP.NET Core для доставки данных на страницы Angular по запросу от клиента более обычным способом.
Открываем главную страницу Angular приложения /wwwroot/index.html. Сейчас этот файл выглядит следующим образом:

<!DOCTYPE html>
<html>
  <head>
    <title>Angular QuickStart</title>
    <base href="/">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">

    <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>

    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('app/main.js').catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>Loading AppComponent content here ...</my-app>
  </body>
</html>

Сам шаблон Angular приложения хранится в файле /wwwroot/app/main.ts. Этот файл выглядит следующим образом:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

Однако Angular компонент может также ссылаться на внешний HTML-шаблон, как показано ниже. В этом примере предпологается, что рядом с файлом main.ts есть файл HTML appComponent.html:

import { Component } from '@angular/core';

@Component({
    selector: 'my-app',
    templateUrl: './appComponent.html'
})
export class AppComponent { name = 'Angular'; }

Идея состоит в том, чтобы использовать синтаксис templateUrl, похожий на пример выше, чтобы указать на новый контроллер PartialController.cs и создать представление, чтобы доставить то, что нам нужно. Т.е. HTML шаблон берется не из статического файла, а рендерится при помощи контролера и частичного представления ASP.NET MVC.

Приступим к кодированию. Создаем PartialController в папке /Controllers. Файл должен называться PartialController.cs.

Visual Studio 2017 - Добавление нового контроллера

Visual Studio 2017 — Добавление нового контроллера

 

Visual Studio 2017 - Добавление нового контроллера

Visual Studio 2017 — Добавление нового контроллера

После этого добавляем в него следующий код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace AngularSpa.Controllers
{
    public class PartialController : Controller
    {
        public IActionResult AboutComponent() =&amp;gt; PartialView();

        public IActionResult AppComponent() =&amp;gt; PartialView();

        public IActionResult ContactComponent() =&amp;gt; PartialView();

        public IActionResult IndexComponent() =&amp;gt; PartialView();
    }
}

Этот новый контроллер будет использоваться для доставки HTML-шаблонов и представлений на клиентскую сторону с Angular компонентами.

Теперь создаем папку Partial в папке /Views, куда будут помещены наши новые представления:
Скопируем 3 существующих файла ASP.NET MVC из папки /Views/Home в новую папку /Views/Partial:

About.cshtml
Contact.cshtml
Index.cshtml

После копирования переименуйте скопированные файлы, находящиеся в /Home/Partial, в AboutComponent.cshtml, ContactComponent.cshtml и IndexComponent.cshtml.
Вот как будет выглядеть проект на данном этапе:

Копирование файлов в новую папку представления

Копирование файлов в новую папку представления

Теперь мы можем очистить наш HomeController.cs, удалив методы, которые нам больше не нужны. Это поможет нам избежать ошибок с несуществующими представлениями и путаницы в маршрутизации.

Итак, HomeController.cs файл выглядел вот так:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace AngularSpa.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";

            return View();
        }

        public IActionResult Contact()
        {
            ViewData["Message"] = "Your contact page.";

            return View();
        }

        public IActionResult Error()
        {
            return View();
        }
    }
}

После редактирования он должен выглядеть следующим образом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace AngularSpa.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            ViewData["Title"] = "Home";
            return View();
        }

        public IActionResult Error()
        {
            return View();
        }
    }
}

Обычно каждое представление в ASP.NET MVC имеет много общего кода и разметки связанного с файлом /Views/Shared/_Layout.cshtml. Каждое представление (index.cshtml, about.cstml, contacts.cshtml) использует разметку из файла /Views/Shared/_Layout.cshtml, а свое содержимое отрисовывает в месте вызова Razor кода @RenderBody().

    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; 2017 - AngularSpa</p>
        </footer>
    </div>

Конечно, можно отказаться от использования этого общего шаблона разметки, если в начале страницы указать директиву

@{
    Layout = null;
}

Или создать другой шаблон страницы и использовать его, указав его имя в директиве из примера выше.
В моем проекте я по-прежнему буду использовать /Views/Home/Index и /Views/Shared/_Layout для доставки HTML контента. Однако, поскольку Angular берет на себя клиентскую часть, представление /Views/Partial/AppComponent.cshtml будет брать на себя некоторые из задач, которые ранее были свойственны файлу _Layout.cshtml, поскольку представление AppComponent.cshtml будет использоваться для загрузки других представлений Angular приложения.
Что бы стало более понятно как это будет работать, давайте добавим файл /Views/Partial/AppComponent.cshtml и реализуем связывание элементов меню используя роутинг Angular. Для начала открываем файл PartialController.cs, делаем правый клик мышки методе AppComponent и выбираем из меню пункт Add View … В открывшемся окошке убираем опцию Use a layout page и нажимаем Add. Новый файл представления будет создан. Его содержимое должно быть следующим:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>AppComponent</title>
</head>
<body>
</body>
</html>

Заменим содержимое файла таким кодом:

<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header" (click)="setTitle('Home - AngularSpa')">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a routerLink="/home" routerLinkActive="active" class="navbar-brand">AngularSpa</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>
                    <a class="nav-link" (click)="setTitle('Home - AngularSpa')" routerLink="/home" routerLinkActive="active">Home</a>
                </li>
                <li>
                    <a class="nav-link" (click)="setTitle('About - AngularSpa')" routerLink="/about">About</a>
                </li>
                <li>
                    <a class="nav-link" (click)="setTitle('Contact - AngularSpa')" routerLink="/contact">Contact</a>
                </li>
            </ul>
        </div>
    </div>
</div>

<div class="container body-content">

    <router-outlet></router-outlet>

    @{
        string razorServerSideData = "ASP.NET Core";
    }

    <hr />
    <footer>
        <p>&copy; 2017 - AngularSpa = (@razorServerSideData + {{angularClientSideData}})<sup>4</sup></p>
    </footer>
</div>

Обратите внимание, новая Angular директива загружает наше Angular содержимое. Т.е. Angular заменяет эту директиву основываясь на текущем состоянии объекта роутинга.
Обновляем /Views/Home/Index.cshtml:


<my-app>Loading AppComponent content here ...</my-app>

Так как мы по-прежнему будем использовать /Views/Shared/_Layout.cshtml, его нужно изменить, так как в нем больше не обрабатывается меню (мы перенесли его в файл AppComponent.cshtml). Поэтому заменим текущее содержимое _Layout.cshtml на это:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - AngularSpa</title>
    <base href="~/">

    <environment names="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
        <link rel="stylesheet" href="~/css/site.css" />
    </environment>
    <environment names="Staging,Production">
        <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
        <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
    </environment>

</head>
<body>

    @RenderBody()

    <environment names="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
        <!-- Polyfill(s) for older browsers -->
        <script src="/node_modules/core-js/client/shim.min.js"></script>
        <script src="/node_modules/zone.js/dist/zone.js"></script>
        <script src="/node_modules/systemjs/dist/system.src.js"></script>
        <script src="~/systemjs.config.js"></script>
        <script>
            System.import('app/main.js').catch(function (err) { console.error(err); });
        </script>
    </environment>
    <environment names="Staging,Production">
        <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
                asp-fallback-test="window.jQuery">
        </script>
        <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
        </script>
        <script src="~/js/site.min.js" asp-append-version="true"></script>
        <!-- Polyfill(s) for older browsers -->
        <script src="/node_modules/core-js/client/shim.min.js"></script>
        <script src="/node_modules/zone.js/dist/zone.js"></script>
        <script src="/node_modules/systemjs/dist/system.src.js"></script>
        <script src="~/systemjs.config.js"></script>
        <script>
            System.import('app/main.js').catch(function (err) { console.error(err); });
        </script>
    </environment>

    @RenderSection("scripts", required: false)

</body>
</html>

Далее нам нужно исправить код в файле pp.component.ts. Тут мы должны указать, что шаблон нужно загружать используя наше новое частичное представление с URL /Partial/appComponent.
До изменений файл выглядел так:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `<h1>Hello {{name}}</h1>`,
})
export class AppComponent  { name = 'Angular'; }

После изменений он должен выглядеть так:

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
    selector: 'my-app',
    templateUrl: '/partial/appComponent'
})
export class AppComponent {
    public constructor(private titleService: Title) { }
 
    angularClientSideData = 'Angular';
 
    public setTitle(newTitle: string) {
        this.titleService.setTitle(newTitle);
    }
}

Немного объяснений, что ту происходит. Title – это специальный сервис, который помогает изменять заголовок HTML страницы. Так как в Angular  приложение не может быть загружено на всю страницу, то и изменить заголовок страницы оно не может. Для этого был разработан специальный сервис, который позволяет устанавливать и читать заголовок страницы.

Создание компонентов Angular приложения

Теперь нам нужно создать три новых компонента, для наших страниц Index, About и Contact. Для этого нужно создать три новых файла в папке /wwwroot/app/
Файл about.component.ts

import { Component } from '@angular/core';
 
@Component({
    selector: 'my-about',
    templateUrl: '/partial/aboutComponent'
})
 
export class AboutComponent {
}

Файл contact.component.ts

import { Component } from '@angular/core';
 
@Component({
    selector: 'my-contact',
    templateUrl: '/partial/contactComponent'
})
 
export class ContactComponent {
}

Файл index.component.ts

import { Component } from '@angular/core';
 
@Component({
    selector: 'my-index',
    templateUrl: '/partial/indexComponent'
})
 
export class IndexComponent {
}

Теперь добавим логику маршрутизации. Для этого создадим файл TypeScript /wwwroot/app/app.routing.ts и добавим в него следующий код:

Visual Studio 2017 - Создание новго TypeScript файла

Visual Studio 2017 — Создание новго TypeScript файла

import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
import { IndexComponent } from './index.component';
import { ContactComponent } from './contact.component';

const appRoutes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: IndexComponent, data: { title: 'Home' } },
{ path: 'about', component: AboutComponent, data: { title: 'About' } },
{ path: 'contact', component: ContactComponent, data: { title: 'Contact' } }
];

export const routing = RouterModule.forRoot(appRoutes);

export const routedComponents = [AboutComponent, IndexComponent, ContactComponent];

Чтобы включить эти новые файлы, мы обновим app.module.ts. Было так:


import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';

@NgModule({
    imports: [ BrowserModule ],
    declarations: [ AppComponent ],
    bootstrap: [ AppComponent ]
})
export class AppModule { }

Должно стать так:


import { NgModule, enableProdMode } from '@angular/core';
import { BrowserModule, Title } from '@angular/platform-browser';
import { routing, routedComponents } from './app.routing';
import { APP_BASE_HREF, Location } from '@angular/common';
import { AppComponent } from './app.component';

// enableProdMode();

@NgModule({
    imports: [BrowserModule, routing],
    declarations: [AppComponent, routedComponents],
    providers: [Title, { provide: APP_BASE_HREF, useValue: '/' }],
    bootstrap: [AppComponent]
})
export class AppModule { }

Что бы иметь немного больше информации и записывать ее в консоль браузера изменим файл main.ts:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
    .then((success: any) =&amp;amp;gt; console.log('App Bootstrapped'))
    .catch((err: any) =&amp;amp;gt; console.error(err));

И последние штрихи. Так как мы используем представление /Home/Index что бы стартовать Angular приложение, мы можем спокойно удалить /wwwroot/index.html файл. Он больше не нужен.
Теперь сохраните все изменения во всех файлах (если вы не сделали этого раньше) и нажмите Ctrl + F5 что бы запустить приложение. Вы увидите все то же ASP.NET MVC Core веб сайт. Можете пощелкать по ссылкам в верхнем меню: Index, Contact, About. Вроде все то жесамое, но уже сеть несколько отличий.

ASP.NET MVC Core + Angular 4 SPA - Index Page

ASP.NET MVC Core + Angular 4 SPA — Index Page

 

ASP.NET MVC Core + Angular 4 SPA - Contact Page

ASP.NET MVC Core + Angular 4 SPA — Contact Page

 

ASP.NET MVC Core + Angular 4 SPA - About Page

ASP.NET MVC Core + Angular 4 SPA — About Page

Во первых, если открыть консоль разработчика в браузере и посмотреть запросы к серверу, вы увидите что при переключение по пунктам меню браузер не отсылает никаких запросов на сервер! Т.е. все страницы (компоненты) уже загружены при открытии веб сайта.
Во вторых, посмотрите на текст в подвале страничек. Этот текст формируется в файле /Views/Partial/AppComponent.cshtml

<div class="container body-content">

    <router-outlet></router-outlet>

    @{
        string razorServerSideData = "ASP.NET Core";
    }

    <hr />
    <footer>
        <p>&copy; 2017 - AngularSpa = (@razorServerSideData + {{angularClientSideData}})<sup>4</sup></p>
    </footer>
</div>

И тут происходит самое интересное. Вначале ASP.NET MVC вызывает Razor, что бы отрендерить View в статический HTML на сервере и отправить его клиенту вместе со всеми директивами Angular. Затем на клиенте Angular обрабатывает директивы: вначале , куда вставляется шаблон для компонента выбраного в меню, а так-же исполняет data binding {{angularClientSideData}}, который так же исполняется на клиенте.

Теперь у нас есть очень простое SPA приложение построенное на основе ASP.NET MVC и Angular 4. Но это только начало пути. Следите за следующими выпусками, в них я буду расширять функционал этого приложения, добавляя все более интересные фичи: Razor HTML помощники, валидация форм при помощи Angular, EF Core / entity framework для обслуживания и хранения данных и мног чего еще. Следите за блогом, будет интересно.

Исходный код

Полностью готовый Visual Studio 2017 проект для второй части можно загрузить тут: Visual Studio 2017 ASP.NET MVC Core + Angular SPA Project.

Все части серии

Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 1.
Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 2 [читаете сейчас].
Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 3.
Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 4.
Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 5.
Single Page Application : ASP.NET MVC .NET Core 2 + Angular 5. Часть 6.

Добавить комментарий

Ваш e-mail не будет опубликован.