Развернутая тема: разделение ASP.NET на Front-End (Angular) и Back-End (WebApi)

Особенности: корпоративная разработка (следовательно основной браузер — IE, веб сервер — IIS, среда — Windows); это частичный рефакторинг, а скорее редизайн веб части (имеется legacy код, ориентация на имеющийся UX);

Причины и цели: Цель — редизайн архитектуры веб составляющей (в текущей версии ASP.NET Forms + WCF), по причине невозможности/сложности решения возникших проблем и новых требований (полное обновление страниц после постбэка, повторная отправка формы, сложная навигация и связанные с этим проблемы с данными в формах).
image

Все описанное базируется на личном опыте (или, соответственно, его отсутствии — еще месяц назад о Node.js и Angular я не знал ничего кроме названия). Если краткое описание статьи заинтересовало — начнем.

В самый разгар поиска новой архитектуры (на тот момент пытался использовать ASP.NET MVC) мне попалось видео от channel9 “Building web apps powered by Angular 2.x using Visual Studio 2017” и его текстовая вариация. Почитав параллельно официальный сайт Angular я проникся и начал пробовать, обнаружив следующие плюсы:

  • Современная (кое-где даже чересчур) и популярная технология;
  • Подходящий под VisualStudio (из-за Typescript) Front-End фреймворк;
  • Модульная архитектура;
  • Без особых трудностей написанная тестовая программа решала основные проблемы (в том числе которые я не мог решить с MVC).

Естественно нашлись и минусы:

  • Мало нацелено на IE: ошибки в браузере лечатся с помощью polyfills/shim, но во время дебаггинга в консоли Visual Studio остаются постоянные исключения в javascript (вроде никак не влияющие на работу, но кто знает как это проявится при общем усложнении программы);
  • Трудности при коммуникации с legacy частью (WCF сервис): Core и .NET Framework не полностью совместимы (в Core 2.0 обещают улучшить эту ситуацию);
  • Дополнительные сложности/особенности развертывания в IIS;
  • Готовая сложная конфигурация: непонятно что, как и почему работает, сложно модифицировать;
  • (Из предыдущего пункта вытекает) привязка к версии и имеющейся конфигурации.

Так я начал читать, разбираться и наткнулся на простой шаблон проекта WebApi + Angular 2 основанный на официальном руководстве Visual Studio 2015 QuickStart. Оттолкнувшись от этого шаблона я начал модифицировать проект под себя (полностью с кодом можно ознакомится по ссылке на GitHub ниже):

  • Убрал лишние npm пакеты — все что связано с тестированием (karma, protractor etc.) и не является необходимым для минимального старта;
  • Обновил до Angular 4.x.

    package.json
    Итоговый вариант

    {
      "name": "angular-quickstart",
      "version": "1.0.0",
      "description": "QuickStart package.json from the documentation for visual studio 2017 & WebApi",
      "scripts": {
        "build:prod": "webpack --config config/webpack.prod.js --colors --progress",
        "build": "webpack --colors",
        "build:vendor": "webpack --config config/webpack.vendor.ts --colors",
        "typings": "typings install"
      },
      "keywords": [],
      "author": "",
      "license": "MIT",
      "dependencies": {
        "@angular/common": "^4.1.3",
        "@angular/compiler": "^4.1.3",
        "@angular/core": "^4.1.3",
        "@angular/forms": "^4.1.3",
        "@angular/http": "^4.1.3",
        "@angular/platform-browser": "^4.1.3",
        "@angular/platform-browser-dynamic": "^4.1.3",
        "@angular/router": "^4.1.3",
    
        "bootstrap": "^3.3.7",
        "core-js": "^2.4.1",
        "jquery": "1.12.4",
        "moment": "^2.18.1",
        "rxjs": "^5.4.0",
        "zone.js": "^0.8.12"
      },
      "devDependencies": {
        "@types/node": "^6.0.46",
        "@types/core-js": "^0.9.41",
        "angular2-template-loader": "^0.6.2",
        "awesome-typescript-loader": "^3.1.3",
        "css-loader": "^0.28.4",
        "extract-text-webpack-plugin": "^2.1.0",
        "file-loader": "^0.11.1",
        "html-loader": "^0.4.5",
        "raw-loader": "^0.5.1",
        "script-loader": "^0.7.0",
        "style-loader": "^0.18.1",
        "typescript": "~2.3.4",
        "webpack": "^2.6.1",
        "webpack-merge": "^4.1.0"
      }
    }
    


  • Сменил systemjs на webpack с разделением всего кода на три пакета (vendor, polyfills, app) — пока без автоматической (пере-)сборки и “ускорялок” (полная сборка занимает 15сек на среднем ноутбуке)

    webpack.config.js
    Common:
    module.exports = {
        entry: {
            'polyfills': './app/polyfills.ts',
            'vendor': './app/vendor.ts',
            'app': './app/main.ts'
        },
    
        resolve: {
            extensions: ['.ts', '.js']
        },
    
        module: {
            rules: [
                {
                    test: /\.ts$/,
                    use: [
                        {
                            loader: 'awesome-typescript-loader',
                            options: {
                                configFileName: helpers.root('', 'tsconfig.json')
                            }
                        },
                        {
                            loader: 'angular2-template-loader'
                        }
                    ]
                },
                {
                    test: /\.html$/,
                    use: [{
                        loader: 'html-loader',
                        options: {
                            minimize: false,
                            removeComments: false,
                            collapseWhitespace: false
                        }
                    }]
                },
                {
                    test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                    loader: 'file-loader?name=dist/assets/[name].[hash].[ext]'
                },
                {
                    test: /\.css$/,
                    exclude: helpers.root('app'),
                    loader: ExtractTextPlugin.extract({
                        fallback: 'style-loader',
                        use: 'css-loader?sourceMap'
                    })
                },
                {
                    test: /\.css$/,
                    include: helpers.root('app'),
                    loader: 'raw-loader'
                }
            ]
        },
    
        plugins: [
            new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable)
            // Workaround for angular/angular#11580 for angular v4
            new webpack.ContextReplacementPlugin(
                /angular(\\|\/)core(\\|\/)@angular/,
                helpers.root('./app'), // location of your src
                {} // a map of your routes
            ),
    
            new webpack.optimize.CommonsChunkPlugin({
                //order is important: 
                //The CommonsChunkPlugin identifies the hierarchy among three chunks: app -> vendor -> polyfills. 
                //Where Webpack finds that app has shared dependencies with vendor, it removes them from app. 
                //It would remove polyfills from vendor if they shared dependencies, which they don't.
                name: ['app', 'vendor', 'polyfills']
            }),
        ]
    };

    Dev:
    module.exports = webpackMerge(commonConfig, {
      devtool: 'source-map',
      
      output: {
          path: helpers.root('dist'),
          publicPath: '/',
        filename: '[name].js',
        chunkFilename: '[id].chunk.js'
      },
      
      plugins: [
          new ExtractTextPlugin('[name].css')
      ]
    });


  • Попытался разобраться с IE и упомянутыми исключениями в VS: выяснил что webpack что то делает с shim/polyfill скриптами и если использовать ссылку на оригинальную версию исключения пропадают

    index.html
    <!DOCTYPE html>
    <html>
    <head>
        <title>Angular.io QuickStart</title>
        <base href=/ >
        <meta charset=UTF-8>
        <meta name=viewport content="width=device-width,initial-scale=1">    
        <link rel="stylesheet" href="./dist/vendor.css" />
    </head>
    <body>
        <my-app>Loading App</my-app>
        <script src="node_modules/core-js/client/shim.min.js"></script>
        <!--<script src="node_modules/es6-shim/es6-shim.min.js"></script>
        <script src="node_modules/core-js/client/shim.js"></script>
        <script src="node_modules/zone.js/dist/zone.js"></script>-->
    <script type="text/javascript" src="./dist/polyfills.js"></script>
    <script type="text/javascript" src="./dist/vendor.js"></script>
    <script type="text/javascript" src="./dist/app.js"></script></body>
    </html>


  • Добавил bootstrap и jQuery;
  • Усложнил структуру программы, стараясь следовать официальному гиду по стилю:

    • Core-модуль для сервисов и единичных компонентов (например header.component);
    • shared модуль для общих компонентов;
    • пару feature модулей представляющих собой отдельные независимые области сайта.

    screenshot
    image

  • Добавил WebApi контроллер и Angular сервис для общения с api

    api.service.ts
    @Injectable()
    export class ApiService {
        private apiUrl: string;
    
        constructor(private http: Http) {
            this.apiUrl = "/api";
        }
    
        private setHeaders(): Headers {
            const headersConfig = {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            };
    
            return new Headers(headersConfig);
        }
    
        private formatErrors(error: any) {
            return Observable.throw(error.json());
        }
    
        get(path: string, params: URLSearchParams = new URLSearchParams()): Observable<any> {
            return this.http.get(`${this.apiUrl}${path}`, { headers: this.setHeaders(), search: params })
                .catch(this.formatErrors)
                .map((res: Response) => res.json());
        }
    
        //put(path: string, body: Object = {}): Observable<any> {
        //    return this.http.put(...);
        //}
    
        //post(path: string, body: Object = {}): Observable<any> {
        //    return this.http.post(...);
        //}
    
        //delete(path): Observable<any> {
        //    return this.http.delete(...));
        //}
    }

    Пример использования:
    export class HomeSiteComponent {
        title = "I'm home-site component with WebApi data fetching";
        public ctrlData: DummyData[];
    
        constructor(apiService: ApiService) {
            apiService.get('/Dummier/Get').subscribe(result => {
                this.ctrlData = <DummyData[]>result;
            });
        }
    }
    
    interface DummyData {
        clientData: string;
        serverData: string;
    }


  • Попробовал развернуть все в полноценном IIS — все работает. Вернул проект на IIS Express;
  • Настроил маршрутизацию, добавив URL Rewrite Rules в Web.config. Теперь сервер принимает и обрабатывает api запросы и перенаправляет на Angular все остальные новые запросы. Сам же Angular отвечает за навигацию на стороне клиента (в том числе отвечает за “страницу 404”).

    Routing
    const routes: Routes = [    
        { path: 'welcome', component: WelcomeComponent },       //Component w/o Menu item
        { path: 'home', loadChildren: () => HomeSiteModule },   //Feature Modul with own Routing
        { path: 'area1', loadChildren: () => Area1SiteModule }, //Feature Modul with own Routing
        { path: '', redirectTo: 'home', pathMatch: 'full' },    //Empty Route
        { path: '**', component: PageNotFoundComponent }        //"404" Route
    ];
    
    @NgModule({
        imports: [RouterModule.forRoot(routes)],
        exports: [RouterModule],
    })
    export class AppRoutingModule { }

    Web.Config:
    <system.webServer>
       ...
        <rewrite>
          <rules>
            <rule name="WebApi Routes" stopProcessing="true">
              <match url="^api/" />
              <action type="None" />
            </rule>
            <rule name="Angular Routes" stopProcessing="true">
              <match url=".*" />
              <conditions logicalGrouping="MatchAll">
                <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
              </conditions>
              <action type="Rewrite" url="/" />
            </rule>     
          </rules>
        </rewrite>
      </system.webServer>



Еще предстоит сделать:

  • Автоматизировать сборку webpack;
  • Разделить сборку на две части чтобы не пересобирать vendor пакет каждый раз;
  • Добавить Windows аутентификацию;
  • Перенаправить WebApi к существующему сервису WCF.

Итоговый проект можно найти на GitHub (на момент написания статьи commit 74e54cf).
С удовольствием отвечу на вопросы и подискутирую на тему «почему так, а не эдак».
Поделиться с друзьями
-->

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


  1. denismaster
    14.06.2017 13:07

    Еще доступны генераторы для yo и dotnet new, возможно вы найдете в их репозиториях что-то полезное, например, автосборку. Успехов!


    1. Shwed_Berlin
      14.06.2017 13:23

      Спасибо. Пользовался этим генератором. И в принципе слежу за парой репозиториев на GitHub по этой тематике.
      Автосборка (webpack dev middleware) там привазана к Core библиотеке.


  1. MasMaX
    14.06.2017 13:39

    Вместо npm попробуйте yarn. На фронтенде он выигрывает в скорости сборки значительно. И проще разрешает зависимости зависимостей.


    1. Shwed_Berlin
      14.06.2017 13:49

      Немного озадачен вашим комментарием. Сборка в моем случае осуществляется не npm, а webpack.
      npm лишь пакет-менеджер. Или я что-то неправильно понимаю?


      1. socrat3z
        14.06.2017 14:36

        Все верно. Yarn — замена npm (после выхода node.js 8 достаточно сомнительная). Для ускорения сборки используйте кеш к webpack и к awesome-typescript-loader + можно поэкспериментировать с HappyPack


        1. Shwed_Berlin
          14.06.2017 14:44

          Спасибо, буду разбираться.
          Пока приоритет поставлен на другие пункты — мне нужно понять подойдет ли для реального проекта (аутентификация и полная работоспособность в IE).


  1. zodchiy
    14.06.2017 14:09
    +1

    Перехожу с mvc 5 + Angular 1.x на net core webapi + Angular 2/4.
    Тоже корпоративная разработка.
    Прошел ваш путь. Понял, что этот путь для меня избыточен. Побаловался и пришел к выводу, что разделить проект на 2 части будет проще.

    Фронт-энд на VS Code под Angular 2/4 билдит файлы в wwwroot бэк-энд проекта.
    Бэк-энд на VS 2017 net core webapi.


    1. Shwed_Berlin
      14.06.2017 14:40

      У меня по сути два проекта, просто они в одной IDE.
      В остальном не вижу отличий и не понимаю какие сложности вы преодолели разделением.


      1. SergeyVoyteshonok
        14.06.2017 15:01

        В продакшене web api и view обычно разделяют, объясню:
        1. Часто разные люди отвечают за backend и front, соответственно удобно когда репозитории разделены, например ветвление по новым фичам, новому дизайну и тд в Git.
        2. Методы доставки и развертывания тоже разные, если backом обычно все сложно ( миграции, настройки, тестовые контуры и тд), то front достаточно доставить на сервер раздающий статику ( кстати он может быть отличен от сервера где сидит web api)
        3. Ну и в принципе смешивать настройки intellisense, сборки и тестирования клиента и сервера в одном workspace как-то не очень. Как вариант: хотя бы выделить клиент в отдельную папку в коде сервера.


  1. Rinz
    14.06.2017 14:29
    +1

    Заголовок хоть поправьте, а то выглядит как будто ведется переезд с Backend технологии на Frontend, меня аж завлекло в эту статью что за чудо тут, а оказывается переход с ASP-Form(Я удивлен что сие технологию еще не похоронили).
    Я лично больше предпочитаю Ember для фронта, ангуляр эта вещь которая не особо поддерживает обратную зависимость, медленная, жирная, отношения Гугла к ней вполне сомнительная т.к. то заявляли что бросают, потом выпускают новую версию которая чуть более чем ломает всю совместимость, позже выпустили Полимер, короче дело ваще но для продакшена я бы лично не выбрал странные технологии которые развиваются очень странным образом(пример с языком Ruby)


    1. Shwed_Berlin
      14.06.2017 14:31

      Это корпоративный продукт, тут и не такие динозавры водятся.
      ASP.NET (хоть MVC, хоть Forms) — это не бэк-енд, а «два в одном» в моем представлении.


      1. Rinz
        16.06.2017 13:40

        Нет, вы не правы. ASP.net это веб/бэкенд фреймворк, а использовать ли сахар типа форм это уже выбор каждого, это тоже самое говорить что JS это и бэкенд и фронтенд язык, ведь есть Nodejs а это тоже вроде JS и потому получаем что ангулар тоже два в одном, ведь можно впаять Nodejs скрипты в него, можно. Вот такая цепочка происходить если неккоректно называть продукт тем чем он не является, если ты видишь тесто, ты ведь не называешь его макаронами или хлебом?! Тут та же история, формы это сахар, а все остальное это цельный продукт, MVC это архитектура продукта т.е. некая схема по которой приготовлен… Я уже теряюсь с чего мне объяснять столь очевидные вещи.


        1. Shwed_Berlin
          16.06.2017 14:03

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


  1. SergeyVoyteshonok
    14.06.2017 14:30
    -1

    Пункт «еще предстоит сделать» я бы немного поправил:

    1. Убрать закомментированный код.
    2. Добавить в gitignore файлы, которые генерируются.
    3. Все остальное…


    1. Shwed_Berlin
      14.06.2017 14:36

      1. Резонно. Тороплюсь, пробую — не всегда успеваю удалять.
      2. Мало знаком с Git. Попробовал добавить /dist в gitignore уже после первых коммитов — папка коммитится вместе с остальными изменениями. Попробую позже разобраться.

      Спасибо


      1. zmeykas
        14.06.2017 18:16

        попробуйте
        git rm -r --cached dist/


      1. SoloMidPlzD
        14.06.2017 18:59
        +1

        @корп софт
        @мало знаком з гит


        1. Shwed_Berlin
          14.06.2017 19:01

          Как понимать ваш комментарий? Кому он предназначен?


  1. DimonDU
    15.06.2017 08:10

    Я тоже пытаюсь затащить Ангулар в корп софт. Больше всего понравилась серия статей на кодпроекте https://www.codeproject.com/Articles/1139558/Single-Page-Application-SPA-for-Enterprise-App-Ang


    1. Shwed_Berlin
      15.06.2017 08:19

      Спасибо, я посмотрю поподробнее.
      Но уже на первый взгляд — Angular там используется весьма странно. Например зачем то городится реализация IoC, хотя в Angular она уже реализованна.


  1. kosmonaFFFt
    15.06.2017 12:05

    Попробуйте @ angular/cli — тулза от ангуляра для генерации проекта, его сборки, тестирования и других вещей.

    Например:
    ng new mysuperproject
    cd mysuperproject
    ng serve

    создаст проект и запустит локальный сервер с горячей пересборкой проекта при изменениях и автообновлением его в браузере. Можно настроить прокси к бекенду и сразу использовать имеющееся API для разработки.

    ng generate component mysupercomponent — сгенерирует компонент (ts + html + css + unit test).
    Ну и другие вещи, которые можно узнать из ng --help…