У нас в солюшене 51 проект. В 10 из них используется TypeScript. Объем минимизированного JavaScript-кода ~1 MB. TypeScript-код одних проектов зависит от кода других проектов. Для многих React-компонентов используются глобальные переменные.
Все вместе это приводит к долгим часам отладки front-end кода. Чтобы упростить себе жизнь, мы внедрили Webpack. А по пути отловили грабли.
TL;DR
- Устанавливаем node 7 + npm
- Выполняем в консоли
npm i -g webpack typescript
- Устанавливаем Webpack Task Runner
- Добавляем webpack.config.js
в папку "основного" проекта - Добавляем 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 проектов — попробуйте способ, описанный в статье
Ссылки
- Шаблон webpack.config.js
- Шаблон webpack.config.part.js
- Webpack Task Runner
Автор: Mindbox