КПДВ У нас в солюшене 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 проектов — попробуйте способ, описанный в статье

Ссылки


Поделиться с друзьями
-->

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


  1. kahi4
    09.01.2017 14:19
    +2

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


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


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


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

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


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


    1. TheHat
      09.01.2017 14:45

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


  1. justboris
    09.01.2017 18:27

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


    1. TheHat
      09.01.2017 18:35

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


      1. justboris
        09.01.2017 19:48

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


  1. Alexus1024
    10.01.2017 11:13

    Я вот этого момента не понял

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


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

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


    1. TheHat
      10.01.2017 11:30

      Если я просто кладу пустой файл tsconfig в корень проекта, то компиляция в VisualStudio отключается совсем

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


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


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


  1. Alexus1024
    10.01.2017 12:07

    Странно, у меня теперь и не пытается или я чего то не понимаю.
    У вас VS2016 с установленными сважими

    • TypeScript for Visual Studio 2015
    • vswebessentials

    ?


    1. TheHat
      10.01.2017 12:31

      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. Может, он у вас не в корне проекта?


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


  1. Alexus1024
    10.01.2017 14:05

    Да, у меня именно так как на картинке стало. И 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


    1. TheHat
      10.01.2017 14:46

      Заводим файл 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) — работает в обоих случаях.