Webpack в Visual Studio для больших солюшенов

    КПДВ У нас в солюшене 51 проект. В 10 из них используется TypeScript. Объем минимизированного JavaScript-кода ~1 MB. TypeScript-код одних проектов зависит от кода других проектов. Для многих React-компонентов используются глобальные переменные.


    Все вместе это приводит к долгим часам отладки front-end кода. Чтобы упростить себе жизнь, мы внедрили Webpack. А по пути отловили грабли.


    TL;DR


    1. Устанавливаем node 7 + npm
    2. Выполняем в консоли npm i -g webpack typescript
    3. Устанавливаем Webpack Task Runner
    4. Добавляем webpack.config.js
      в папку "основного" проекта
    5. Добавляем webpack.config.part.js
      в папку каждого зависимого проекта

    Самые распространенные проблемы, которые возникают у нас при работе с TypeScript-кодом — «непоследовательное» наследование и обилие глобальных переменных. Они появляются при дефолтных настройках студии и большом размере солюшена.


    Проблема — «непоследовательное» наследование


    Проблема с наследованием возникает, когда базовый класс подключается после дочернего. Когда в нашем приложении падает ошибка Uncaught TypeError: Cannot read property 'prototype' of undefined, это скорее всего проблема с «непоследовательным» наследованием.


    Приведу пример. На "старте" мы написали код ниже.


    namespace Sandbox {
        export class Base {
            protected foo() {
                console.log('foo');
            }
        }
    
        export class Derived extends Base {
            public baz() {
                this.foo();
                console.log('baz');
            }
        }
    }

    Мы хотим, чтобы каждый класс лежал в отдельном файле, потому что считаем, что так удобнее. И код из листинга выше мы разделили на два файла: base.ts и derived.ts.


    namespace Sandbox {
        export class Base {
            protected foo() {
                console.log('foo');
            }
        }
    }

    namespace Sandbox {
        export class Derived extends Base {
            public baz() {
                this.foo();
                console.log('baz');
            }
        }
    }

    Появился файл derived.ts, который неявно зависит от файла base.ts. Теперь важен порядок подключения этих двух файлов: если подключить сначала derived.js, а затем base.js, то получим сообщение об ошибке.


    При настройках студии "по умолчанию" важен порядок подключения зависимых скриптов.


    Проблема — глобальные переменные


    Чтобы TypeScript-код из разных проектов использовал общий код, приходится объявлять глобальные переменные.


    Например, мы написали полезную функцию makeSandwich в пространстве имен Utils в проекте Administration:


    namespace Utils {
        ...
        export function makeSandwich() {
            ...
        }
    }

    Получаем следующий код в JavaScript:


    var Utils;
    (function (Utils) {
        function makeSandwich() {
            ...
        }
        Utils.makeSandwich = makeSandwich;
    })(Utils || (Utils = {}));

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


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


    При настройках студии "по умолчанию" создаются глобальные переменные.


    Решение


    И «непоследовательное» наследование и глобальные переменные — проблемы решения зависимостей. В мире JavaScript зависимости разруливают специальные инструменты: Webpack, Browserify, RequireJS, SystemJS и прочие. Выбрал Webpack, так как я с ним раньше работал.


    В Visual Studio начиная с 13 версии появился Task Runner. Это такой инструмент, который привязывает задачу (task) к моменту жизненного цикла проекта: при открытии, на Clean, перед билдом или после билда. Расширяют Task Runner плагины для студии.


    Чтобы встроить Webpack в билд студии, используйте Task Runner и плагин Webpack Task Runner. Работать с ними просто. Создайте файл webpack.config.js в папке с проектом и повесьте таск "Watch - Development" на открытие проекта. Подробнее в этой статье.


    Каждый раз при запуске "Watch - Development" над файловой системой запускается watch — на каждое изменение в наблюдаемых файлах срабатывает билд. Без дополнительных настроек все прекрасно работает, если у вас один проект. Если же у вас два и более проектов, на каждый из них будет запущен watch. У нас 10 проектов с TypeScript. 10 вотчей на моем компьютере работают 3 минуты на билд скриптов. И это при изменении одного ts-файла. Нужно улучшить.


    Наши проекты устроены так, что есть "основной" проект с каркасом сайта и "зависимые" проекты с плагинами.


    Я настроил наш билд таким образом, что при загрузке "основного" проекта будет запущена задача "Watch - Development" при помощи студийного Task Runner'а. Базовые настройки лежат в webpack.config.js: лоадеры, tsconfig, плагины. Внутри webpack.config.js также написан код, который ищет по всем папкам солюшена файлы webpack.config.part.js. Каждый файл webpack.config.part.js содержит настройки билда для конкретного проекта: entry, оверрайды настроек tsconfig и прочее. Обычно, там только entry. И "основной" и "зависимые" проекты содержат файл webpack.config.part.js. Таким образом, для билда скриптов во всех проектах используется только один watch.


    Настройка билд-сервера банальна: запускаете webpack -p в папке "основного" проекта.


    Еще один нюанс — помимо Webpack, студия все еще билдит наш TypeScript. В TypeScript с версии 2.0 в tsconfig.json можно запретить билд файлов директивой exclude с паттерном ./Scripts/*. Webpack проигнорирует эту настройку. Но в TypeScript 2.1 компилятор должен найти хотя бы один файл для билда, иначе упадет ошибка. Пришлось оставить один пустой файл для принесения в жертву студии.


    Результаты


    И наследование и использование глобальных переменных выродилось в директивы импорта:


    import { Base } from '../../../../../BaseProject/Scripts/Base';
    import * as Utils from '../../../../../Utils';
    
    export class Derived extends Base {
        public baz() {
            this.foo();
            Utils.makeSandwich();
            console.log('baz');
        }
    }

    Обратите внимание на длинные относительные пути. Они тут нарочно, чтобы показать, что такое случается. Исплоьзуйте промежуточные файлы с импортами из других проектов и "заинкапсулировать" тонну длинных относительных путей.


    При использовании одного watch вместо нескольких, билд ускорился с 3 минут до нескольких секунд.


    Продуктом компиляции станет один файл, содержащий все заимпортированные файлы. Причем, при запуске с production-настройками, файл будет минимизирован. Подробнее о работе Webpack можно узнать на сайте webpack.js.org.


    Переписать весь наш код на использование импортов за один подход не представляется возможным. Но способ описанный в статье позволяет делать эту работу частями.


    Выводы


    • У вас один проект — все и так работает, не трожьте
    • У вас один проект, но хотите плюшки Webpack — вам сюда
    • У вас немного проектов (2-5) — вам все еще сюда
    • У вас более 5 проектов — попробуйте способ, описанный в статье

    Ссылки


    Mindbox 47,32
    Компания
    Поделиться публикацией
    Комментарии 11
    • +2

      Вебпак это хорошо. Но предложенное вами решение все еще далеко от идеала.


      Когда у вас много общих "библиотек", таскать их извне (вообще ходить вне папки с исходниками) плохо. Следующий ваш шаг, чтобы это исправить:


      1. написать команду
        npm init в папке build (или как она у вас там называется), присвоить какое-то имя этой библиотеки, например my-lib-base.


      2. Там же npm link
      3. В папке с вебпаком где лежат зависимые проекты написать npm link my-lib-base.
      4. Использовать как обычную npm библиотеку.

      Это позволяет не делать огромные пути "вверх", не лазить вне папки с исходниками, структурировать проекты. При этом вебпак будет подхватывать изменения библиотеки base на лету и пересобирать зависимые библиотеки.


      В идеале развернуть свой npm репозиторий, чтобы можно было разрабатывать независимо библиотеки разным людям, при этом тем, кто разрабатывает "ядро" не нужно было запускать локально вебпак со сборкой, например, ui-элементов.

      • 0
        Спасибо за совет!
      • 0

        А что такое "солюшен"?

        • 0
          В Visual Studio группа зависимых проектов объединяется в solution. В русской версии MSDN используется дословный перевод «решение». А я по привычке говорю «солюшен».
          • 0

            Спасибо за объяснение.
            Как человеку, не знакомому с Visual Studio, это показалось странным англицизмом.

        • 0
          Я вот этого момента не понял
          Еще один нюанс — помимо Webpack, студия все еще билдит наш TypeScript. В TypeScript с версии 2.0 в tsconfig.json можно запретить билд файлов директивой exclude с паттерном ./Scripts/*. Webpack проигнорирует эту настройку. Но в TypeScript 2.1 компилятор должен найти хотя бы один файл для билда, иначе упадет ошибка. Пришлось оставить один пустой файл для принесения в жертву студии.


          Если я просто кладу пустой файл tsconfig в корень проекта, то компиляция в VisualStudio отключается совсем, остаётся только мой Gulp+'gulp-typescript'

          Вы вроде как пытались добиться того же, к чему такие сложности с exclude и пустым файлом ts?
          • 0
            Если я просто кладу пустой файл tsconfig в корень проекта, то компиляция в VisualStudio отключается совсем

            Не так. Точнее, зависит от версии плагина TypeScript. Если мне не изменяет память, плагин использует tsconfig при его наличии с версии 1.6.


            А у нас много скриптов в "основном" проекте все еще билдится студией — не все за раз на Webpack перевели. Поэтому их игнорим директивой exclude.


            А в паре "зависимых" проектов удалось все перевести на Webpack. Но т.к. студия в любом случае силится что-нибудь сблидить, пришлось создать пустой файл ts. Это костыль, связанный с работой плагина TypeScript, и я хочу от него (костыля) избавиться. Буду рад советам.

          • 0
            Странно, у меня теперь и не пытается или я чего то не понимаю.
            У вас VS2016 с установленными сважими
            • TypeScript for Visual Studio 2015
            • vswebessentials

            ?
            • 0

              Visual Studio 2015 (14) Update 3 + TypeScript for Microsoft Visual Studio 2.0.6.
              На другой машине пробую Visual Studio 2017 RC (15.0.26014.0) + TypeScript for Microsoft Visual Studio 2.1.3.


              WebEssentials не установлен.


              В настройках проекта вот такая картинка, но студия использует tsconfig. Может, он у вас не в корне проекта?


              Настройки проекта

            • 0
              Да, у меня именно так как на картинке стало. И tsconfig у меня в корне проекта.
              (интересно, что до того как узнал от вас про tsconfig настраивал в проекте и не работало :) )

              Содержимое такое:
              {
                "compileOnSave": false,
              }
              

              compileOnSave — тоже не обязательно. стоит положит ьпустой файл с { } и всё отключается. Я искал этот способ, и вот наконец заработало, чему рад.

              А по версиям как то так
              Microsoft Visual Studio Enterprise 2015
              Version 14.0.25424.00 Update 3
              Microsoft .NET Framework Version 4.6.01038

              ASP.NET and Web Tools 2015.1 (Beta8) 14.1.11107.0
              ASP.NET and Web Tools 2015.1 (Beta8)

              ASP.NET Web Frameworks and Tools 2012.2 4.1.41102.0
              For additional information, visit http://go.microsoft.com/fwlink/?LinkID=309563

              ASP.NET Web Frameworks and Tools 2013 5.2.40314.0
              For additional information, visit http://www.asp.net/
              TypeScript 1.8.35.0
              TypeScript tools for Visual Studio



              И я не понял где эта ошибка вылазит, о которой вы говорите?
              • 0

                Заводим файл test.ts и tsconfig.json. В tsconfig.json пишем:


                {
                  "include": ["./test.ts"],
                  "exclude": ["./*"]
                }

                Если комплятор tsc версии 2.1.4 запустить в этой папке получим ошибку:
                No inputs were found in config file '<folder>/tsconfig.json'. Specified 'include' paths were '["./test.ts"]' and 'exclude' paths were '["./*"]'.


                Если "./*" в exclude поменять, например, на "./bin/*" — компилятор работает без ошибок.


                С более ранними версиями (до 2.1) — работает в обоих случаях.

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

              Самое читаемое