История о том, как подружить два отдельных проекта ASP.NET Core Web API и Angular 5, и заставить их работать, как одно целое.

Вступление


Данная статья рассчитана на новичков, которые делают первые шаги в изучении Angular в связке с .NET Core.

Если вы используете Visual Studio для разработки, то наверное уже встречались с готовыми шаблонами проектов с подключенным Angular. Данные шаблоны позволяют в пару кликов создать приложение, которое уже имеет настроенный роутер и несколько готовых компонент. Вам не нужно тратить время на минимальную настройку рабочего приложения: вы уже имеете рабочий WebPack, отдельный модуль для общих компонент, настроенный роутер, подключенный Bootstrap. Возможно, вы подумаете: «Супер! Круто! Половина дела сделана!». Но на самом деле все немного сложнее…

Сложность и подводные камни заключаются в том, что у такого подхода и в стандартных шаблонах есть несколько существенных недостатков:
  1. Жесткая связь веб-интерфейса с серверной часть
  2. Сильно усложненная минимально рабочая версия приложения
  3. Отсутствие возможности использовать Angular CLI
  4. Лишние предустановленные пакеты
  5. Нарушение некоторых принципов из Angular Style Guide

Различные best-practice советуют нам разделять приложение на два отдельных проекта, что в нашем случае — .NET Core Web API и Angular проекты. Основными преимуществами такого подхода будет следующее:
  1. Два независимых друг от друга проекта, что позволит нам в дальнейшем реализовать альтернативный интерфейс, не трогая проект с серверной частью
  2. Суженный глобальный search scope, что позволяет эффективнее и проще производить поиск
  3. Абстрагированность от рабочего окружения, в котором разрабатывается серверная часть, Visual Studio например — мы можем использовать VS Code, Sublime Text, Atom или другой удобный для вас редактор

При таком раскладе конечных сценария продакшена два:
  1. Вы хостите веб-интерфейс на одном адресе, а сервер — на другом
  2. Либо собираете магическим образом проекты в один и хостите только его

Моей задачей являлся как раз второй сценарий, так был он был более предпочтительным по экономическим соображениям. И вот, когда я пытался разобраться с тем, каким же все-таки образом подружить .NET Core Web API проект с Angular проектом, так, чтобы во время разработки у нас было два отдельных проекта, а в продакшене — всего один, а конкретно .NET Core веб-сайт, то я так и не смог найти полноценного руководства «с нуля до рабочего приложения». Пришлось собирать по кусочкам решения с англоговорящих форумов и блогов. Если у вас вдруг появилась такая же задача, то достаточно будет прочитать мою статью.

Поехали!


Итак, что мы будем использовать? Нам потребуются следующие вещи:

Если у вас уже установлена Visual Studio 2017 и при установке вы выбирали .NET Core Development, то .NET Core SDK у вас уже есть и устанавливать его не нужно. Однако Node.js отдельно придется установить даже если был выбран Node.js Development. Npm установится вместе с Node.js. Angular CLI устанавливается глобально из командной строки через npm (инструкция есть по ссылке выше).

Теперь нам следует проверить, все ли установлено и готово к работе. Для этого откройте командную строку (терминал) и выполните подряд команды, перечисленные ниже:

dotnet --version #Версия .NET Core
node --version   #Версия Node.js
npm --version    #Версия npm
ng --version     #Версия Angular CLI

Результат командной строки (версии могут отличаться, ничего страшного)


Создаем проект .NET Core Web API


В данной статье я буду выполнять все действия через командную строку и VS Code, так как он поддерживает .NET Core. Однако, если для вас предпочтительна Visual Studio 2017 для работы с .NET проектами, то можете смело создать и редактировать проект через нее.

Шаг первый


Создаем корневую папку проекта Project, открываем ее в VS Code, запускаем терминал сочетанием клавиш Ctrl + ~ (тильда, буква ё). Пока ничего сложного:)

Окно VS Code и запущенный терминал


Шаг второй


Теперь нам нужно создать проект. Для этого выполняем команду:

dotnet new webapi -n Project.WebApi

Окно VS Code с открытым проектом и запущенный терминал


Шаг третий


Проверяем все ли работает. Через терминал переходим в папку с только что созданным проектом, после выполняем команду:

dotnet run

Окно VS Code с открытым проектом и запущенный терминал


Шаг четвертый


Если на прошлом шаге все прошло успешно и в консоль было выведено Now listening on: localhost:5000, значит сервер успешно запущен. Перейдем по адресу localhost:5000/api/values (тестовый контроллер, который создается автоматически). Вы должны увидеть JSON с тестовыми данными.

Результат в браузере


Шаг пятый


Возвращаемся в VS Code и в терминале нажимаем Ctrl + C, чтобы остановить работу сервера.

Окно VS Code с открытым проектом и запущенный терминал


Создаем проект Angular


Теперь создадим проект Angular. Для этого будем использовать команды Angular CLI, VS Code и встроенный терминал.

Шаг первый


В терминале переходим в корневую папку нашего проекта Project и создаем новый проект с названием Project.Angular (придется немного подождать):

cd ..ng new Project.Angular

Окно VS Code с открытым проектом и запущенный терминал


Шаг второй


Перейдем в терминале в папку только что созданного проекта и запустим его:

cd ./Project.Angular
ng serve --open

Окно VS Code с открытым проектом и запущенный терминал


Шаг третий


Если на прошлом шаге все прошло успешно и в консоль было выведено NG Live Development Server is listening on localhost:4200, значит сервер успешно запущен. Перейдем по адресу localhost:4200. Вы должны увидеть тестовую страничку Angular.

Результат в браузере


Шаг четвертый


Возвращаемся в VS Code и в терминале нажимаем Ctrl + C, вводим Y, чтобы остановить работу сервера.

Окно VS Code с открытым проектом и запущенный терминал


Настраиваем проект Angular


Теперь нам нужно настроить две вещи: proxy.config.json для перенаправления запросов к серверу на нужный порт, и самое главное — настройка сборки в папку wwwroot.

Шаг первый


Создаем в корне проекта Project.Angular файл с названием proxy.config.json и добавляем в него следующее содержимое:

{
    "/api/*": {
        "target": "http://localhost:5000/",
        "secure": false,
        "logLevel": "debug"
    }
}

proxy.config.json
{
    "/api/*": {
        "target": "http://localhost:5000/",
        "secure": false,
        "logLevel": "debug"
    }
}


Данная настройка указывает на то, что все запросы начинающиеся с /api/… будут попадать на localhost:5000/. То есть результирующим запросом будет localhost:5000/api/…

Шаг второй


Укажем Angular, что в режиме разработки нам нужно использовать этот proxy.config. Для этого открываем файл package.json (который находится там же, в корне), находим команду scripts -> start и заменяем значение на:

{
    ...
    scripts: {
        ...
        "start": "ng serve --proxy-config proxy.config.json",
    }
}    

package.json
{
    {
        "name": "project.angular",
        "version": "0.0.0",
        "license": "MIT",
        "scripts": {
            "ng": "ng",
            "start": "ng serve --proxy-config proxy.config.json",
            "build": "ng build --prod",
            "test": "ng test",
            "lint": "ng lint",
            "e2e": "ng e2e"
        },
        "private": true,
        "dependencies": {
            "@angular/animations": "^5.2.0",
            "@angular/common": "^5.2.0",
            "@angular/compiler": "^5.2.0",
            "@angular/core": "^5.2.0",
            "@angular/forms": "^5.2.0",
            "@angular/http": "^5.2.0",
            "@angular/platform-browser": "^5.2.0",
            "@angular/platform-browser-dynamic": "^5.2.0",
            "@angular/router": "^5.2.0",
            "core-js": "^2.4.1",
            "rxjs": "^5.5.6",
            "zone.js": "^0.8.19"
        },
        "devDependencies": {
            "@angular/cli": "1.6.7",
            "@angular/compiler-cli": "^5.2.0",
            "@angular/language-service": "^5.2.0",
            "@types/jasmine": "~2.8.3",
            "@types/jasminewd2": "~2.0.2",
            "@types/node": "~6.0.60",
            "codelyzer": "^4.0.1",
            "jasmine-core": "~2.8.0",
            "jasmine-spec-reporter": "~4.2.1",
            "karma": "~2.0.0",
            "karma-chrome-launcher": "~2.2.0",
            "karma-coverage-istanbul-reporter": "^1.2.1",
            "karma-jasmine": "~1.1.0",
            "karma-jasmine-html-reporter": "^0.2.2",
            "protractor": "~5.1.2",
            "ts-node": "~4.1.0",
            "tslint": "~5.9.1",
            "typescript": "~2.5.3"
        }
    }      
}    


В дальнейшем для запуска проекта Angular будем использовать команду npm start вместе ng serve. Команда npm start является сокращением для команды, которая указана у вас в package.json.

Шаг третий


Последним шагом будет простая настройка сборки (по команде) проекта в папку wwwroot .NET Core Web API проекта. В открытом файле package.json находим команду scripts -> build и заменяем значение на следующее:

{
    ...
    scripts: {
        ...
        "build": "ng build --prod --output-path ../Project.WebApi/wwwroot",
    }
}    

package.json
{
    {
        "name": "project.angular",
        "version": "0.0.0",
        "license": "MIT",
        "scripts": {
            "ng": "ng",
            "start": "ng serve --proxy-config proxy.config.json",
            "build": "ng build --prod --output-path ../Project.WebApi/wwwroot",
            "test": "ng test",
            "lint": "ng lint",
            "e2e": "ng e2e"
        },
        "private": true,
        "dependencies": {
            "@angular/animations": "^5.2.0",
            "@angular/common": "^5.2.0",
            "@angular/compiler": "^5.2.0",
            "@angular/core": "^5.2.0",
            "@angular/forms": "^5.2.0",
            "@angular/http": "^5.2.0",
            "@angular/platform-browser": "^5.2.0",
            "@angular/platform-browser-dynamic": "^5.2.0",
            "@angular/router": "^5.2.0",
            "core-js": "^2.4.1",
            "rxjs": "^5.5.6",
            "zone.js": "^0.8.19"
        },
        "devDependencies": {
            "@angular/cli": "1.6.7",
            "@angular/compiler-cli": "^5.2.0",
            "@angular/language-service": "^5.2.0",
            "@types/jasmine": "~2.8.3",
            "@types/jasminewd2": "~2.0.2",
            "@types/node": "~6.0.60",
            "codelyzer": "^4.0.1",
            "jasmine-core": "~2.8.0",
            "jasmine-spec-reporter": "~4.2.1",
            "karma": "~2.0.0",
            "karma-chrome-launcher": "~2.2.0",
            "karma-coverage-istanbul-reporter": "^1.2.1",
            "karma-jasmine": "~1.1.0",
            "karma-jasmine-html-reporter": "^0.2.2",
            "protractor": "~5.1.2",
            "ts-node": "~4.1.0",
            "tslint": "~5.9.1",
            "typescript": "~2.5.3"
        }
    }      
}    


Для выполнения этого действия выполните в терминале команду npm run build. Результатом будет собранные файлы проекта в папке wwwroot.

Настраиваем проект .NET Core Web API


Осталось научить серверную работать со статическими файлами и разрешать запросы с другого порта.

Шаг первый


Открываем Startup.cs и добавляем в метод Configure строчки, позволяющие серверу обрабатывать статические файлы:

app.UseDefaultFiles();
app.UseStaticFiles();

Метод Configure в Startup.cs
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseDefaultFiles();
        app.UseStaticFiles();

        app.UseMvc();
    }
    


Шаг второй


В Startup.cs, в метод Configure, добавляем строку, позволяющую серверу принимать запросы с порта 4200:

app.UseCors(builder => builder.WithOrigins("http://localhost:4200"));

Метод Configure в Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseDefaultFiles();
    app.UseStaticFiles();

    app.UseCors(builder => builder.WithOrigins("http://localhost:4200"));

    app.UseMvc();
}


Шаг третий


В методе ConfigureServices добавляем поддерку CORS:

services.AddCors();

Метод ConfigureServices в Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors();

    services.AddMvc();
}


В конечном итоге файл Startup.cs должен иметь содержимое, которое представлено ниже:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Project.WebApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddCors(); // <-- Добавили это
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseDefaultFiles(); // <-- Это
            app.UseStaticFiles(); // <-- Вот это

            app.UseCors(builder => builder.WithOrigins("http://localhost:4200")); // <-- И вот так:)

            app.UseMvc();
        }
    }
}    

Готово! Теперь вы можете смело обращаться к вашим API контроллерам из Angular проекта. Также, вызвав команду npm run build для Angular проекта, у вас будет версия Web API приложения готовая для деплоя.

Заключение


Это было краткое руководство по тому, что нужно сделать, чтобы иметь два отдельных проекта, и заставить их работать как одно целое.

Настройка CORS и автоматизация сборки даже далеко не претендует на продакшен версию. Однако, вы теперь знаете, куда смотреть и копать. Надеюсь моя статья окажется для кого-то полезной. Лично мне ее как раз и не хватало, когда я пытался наладить общение между этими двумя технологиями.

В данной статье я не осветил несколько моментов, связанных с маршрутизацией в Web API проекте, более гибкой настройкой CORS, автоматической сборкой и т.д. В планах написать более подробную статью, как собрать уже продакшен версию такого варианта приложения. Если вдруг у вас возникнут вопросы, то пишите в комментарии или на любой из контактов, указанных в профиле, я постараюсь вам помочь.

Комментарии (18)


  1. sergeyZ
    20.02.2018 15:25

    Проблемы, решение которых вы описываете в статье, были характерны для старых шаблонов, созданных еще для .NET Core 1.0. Команда ASP.NET Core хорошо поработала, теперь client не прибит гвоздями к серверу, рекомендую использовать template от команды asp.net. Можно использовать angular-cli. А вся необходимая настройка состоит из пары строк:

    context.UseSpaStaticFiles();
    context.UseSpa(spa =>
    {
        if (env.IsDevelopment())
        {
    	spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
        }
    });
    


    Вся магия в пакете Microsoft.AspNetCore.SpaServices.Extensions


    1. IliaTrifonov Автор
      20.02.2018 15:32

      Спасибо за совет! Однако упоминаний про это расширение я ни разу не встретил, когда пытался разобраться с темой. Надеюсь, что этот вариант удобнее и еще проще того, что я описал. Посмотрю.


      1. sergeyZ
        20.02.2018 20:00

        Вот документация: https://docs.microsoft.com/en-us/aspnet/core/spa/angular (если ссылка перестанет работать, вот еще одна)


        1. IliaTrifonov Автор
          20.02.2018 22:36

          В целом эта штука повторяет тоже самое, что я и описал в посте:) Единственные различие, что сам app находится внутри .NET Core проекта и простая настройка занимает чуть больше строк кода.
          Расскажите, в чем профит вообще этого шаблона, кроме того что создается все одной командой? Пока на первый взгляд ничего существенно полезного не увидел.


          1. sergeyZ
            20.02.2018 22:57

            Создание одной командой уже само по себе неплохо. Для меня главное
            преимущество — возможность использовать стандартную cookie-based авторизацию из asp.net core identity. Плюс возможность делать так:

            app.UseWhen(context => context.User.Identity.IsAuthenticated, context =>
            {
               context.UseSpaStaticFiles();
               context.UseSpa(...);
            }
            

            Страницы авторизации и оплаты — это asp.net вьюхи — можно не переживать за кражу паролей и кредитных карт с помощью внедрения зловредного кода в node_modules (проверить все зависимости, которые тащит за собой ангуляр, а тем более сторонние модули просто невозможно). Плюс не авторизованный пользователь даже не получит кода нашего клиентского приложения.

            Второй плюс — ноль самодельных костылей, которые надо потом поддерживать, все используемые функции — часть проекта ASP.NET Core и поддерживаются его командой.


            1. IliaTrifonov Автор
              20.02.2018 23:18

              Поставил бы плюс, если бы мог:) Спасибо, за разъяснение.
              У вас нет случайно ссылок на гитхаб или статьи, где есть такое «смешивание» asp.net view и angular страниц? Как сделать страницу авторизации и перекинуть потом в приложение понимаю. Но как внедрить страницу оплаты — что-то не очень. Единственное, что пока приходит в голову — это просто перенаправлять с приложения на отдельную страницу. Существуют варианты, как подгружать вьюхи внутрь Angular приложения?


              1. sergeyZ
                21.02.2018 08:42

                Не видел таких проектов на гитхабе, я знаю только один boilerplate с авторизацией, но там все сделано на клиенте, с другой стороны переделывать совсем немного.
                Нужно, чтобы маршруты в angular router не совпадали с маршрутами в asp.net, а потом при переходе на страницу с url, которого angular не знает он сделает запрос на сервер и пользователь увидит нужную страницу. Я не встраиваю эти вьюхи в приложение в своём проекте, но это легко можно сделать с помощью iframe.


  1. denismaster
    20.02.2018 15:53

    Было бы интересно также прочитать про миграцию приложений, построенных «старым способом», на новый, описанный в статье или, как выше указали, в шаблонах)
    Интересуют подводные камни и стоит ли вообще игра свеч)


    1. sergeyZ
      20.02.2018 20:20

      В сущности, никакой миграции не нужно. Всего 3 шага:

      1. Устанавливаем Microsoft.AspNetCore.SpaServices и Microsoft.AspNetCore.SpaServices.Extensions
      2. Правим конфигурацию в Startup.cs
      3. Создаём и настраиваем .angular-cli.json

      Всё описано в документации.

      Если до этого использовали webpack с хитрой конфигурацией, могут возникнуть трудности из-за того, что angular-cli на даёт такой гибкости. Да, можно извлечь weback.config из angular-cli, но это строго не рекомендуется разработчиками angular.

      В моём проекте обновление того стоило. Содержать webpack.config.dev.js и webpack.config.prod.js больше не нужно — это экономит кучу времени.

      Правда есть недостатки, с помощью angular-cli невозможно (пока) убрать локали moment.js из бандла. А это лишние 300 Кб.


  1. mokeev1995
    20.02.2018 17:56

    А теперь стоит вспомнить про SEO и Angular Universal и снова сменить архитектуру приложения. Либо на Node.Js -> Angular, либо интегрировать в Asp.Net Core. Таким образом ни одно из представленных решений не сможет нам помочь :)


    1. IliaTrifonov Автор
      20.02.2018 18:11

      Все знают, что ситуация с SEO для SPA приложений не очень хорошая. Но особого смысла делать SPA для обычного лендинга или сайта компании/продукта/итд нет. Single Page подход используются преимущественно именно для веб-приложений с широкой функциональностью. На мой взгляд, до тех пор пока Google не научится нормально индексировать одностраничные приложения, лучше сделать обычный лендинг по вашему продукту, а само приложение размещать отдельно, например на поддомене.
      Что касается Server-Side Rendering. С такой архитектурой не прокатит, верно. Но если вы хотите использовать его, чтобы увеличить производительность, то можно попробовать поэкспериментировать с Lazy Loading и настроить .NET Core под это дело. Признаюсь, я не пробовал. Но кейс интересный — проверю.


      1. mokeev1995
        20.02.2018 18:17

        SSR как раз таки главным образом и используют для того, чтоб можно было сгенерировать полноценную отрендеренную страницу и скормить её ботам поисковиков, но не для ускорения, его то как раз таким образом не достичь, ведь интерфейс быстрее не проинициализируется.


        Для ускорения как раз подойдёт, как вы уже заметили, LazyLoading и (частично не актуально начиная с 6й версии ангуляра) грамотное расставление импортов.


        1. IliaTrifonov Автор
          20.02.2018 18:26

          Если вы приведете примеры действительно оправданного использования Single Page для веб-сайтов, которые нужно индексировать, то я соглашусь с тем, что такой вариант не очень и поменяю свою мнение:) Пока я лично считаю, что SPA не нужно использовать повсеместно. А там, где его использование необходимо, то индексация поисковиком и SEO оптимизация отходит на второй план.


    1. sergeyZ
      20.02.2018 20:05

      На asp.net core SSR в angular работает из коробки, просто нужно его включить. Выше я привел ссылку на документацию, если интересно как оно работает.


      1. mokeev1995
        20.02.2018 20:47

        хорошая документашка, но правильно ли я понимаю, что это всё ещё в статусе beta/rc?


        1. sergeyZ
          20.02.2018 22:40

          Да, rc2. Релиз будет вместе со следующей версией ASP.NET Core 2.1 т.к. теперь шаблоны проектов включены в официальный репозиторий aspnet.


    1. navix
      20.02.2018 20:41

      Не важно на чем написан API, даже если это сторонний сервис. Node в проде тут выступит как прокси (без всякого отношения к бекенду или самому Angular-приложению), который соберет данные с нужных эндпоинтов и отрендерит html.


      1. mokeev1995
        20.02.2018 20:47

        Node участвует, главным образом, как среда выполнения js-кода :)
        а так да, в случае Node.js+angular обычно используется express, в случае asp net core — NodeServices, вроде как (а под капотом поднимается процесс ноды и она всё обрабатывает и возвращает вёрстку готовую).
        просто под ASP.NET Core + angular ещё ведь обычно предполагается, что будут использовать всякие razor helpers, передачу данных из controller-а во вьюхи и т.д., чтоб использовать эту связку по максимуму.
        или я не прав где-то?