У нас в солюшене 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
Комментарии (11)
justboris
09.01.2017 18:27А что такое "солюшен"?
TheHat
09.01.2017 18:35В Visual Studio группа зависимых проектов объединяется в solution. В русской версии MSDN используется дословный перевод «решение». А я по привычке говорю «солюшен».
justboris
09.01.2017 19:48Спасибо за объяснение.
Как человеку, не знакомому с Visual Studio, это показалось странным англицизмом.
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?TheHat
10.01.2017 11:30Если я просто кладу пустой файл tsconfig в корень проекта, то компиляция в VisualStudio отключается совсем
Не так. Точнее, зависит от версии плагина TypeScript. Если мне не изменяет память, плагин использует tsconfig при его наличии с версии 1.6.
А у нас много скриптов в "основном" проекте все еще билдится студией — не все за раз на Webpack перевели. Поэтому их игнорим директивой
exclude
.
А в паре "зависимых" проектов удалось все перевести на Webpack. Но т.к. студия в любом случае силится что-нибудь сблидить, пришлось создать пустой файл ts. Это костыль, связанный с работой плагина TypeScript, и я хочу от него (костыля) избавиться. Буду рад советам.
Alexus1024
10.01.2017 12:07Странно, у меня теперь и не пытается или я чего то не понимаю.
У вас VS2016 с установленными сважими
- TypeScript for Visual Studio 2015
- vswebessentials
?TheHat
10.01.2017 12:31Visual 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. Может, он у вас не в корне проекта?
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
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) — работает в обоих случаях.
kahi4
Вебпак это хорошо. Но предложенное вами решение все еще далеко от идеала.
Когда у вас много общих "библиотек", таскать их извне (вообще ходить вне папки с исходниками) плохо. Следующий ваш шаг, чтобы это исправить:
написать команду
npm init
в папкеbuild
(или как она у вас там называется), присвоить какое-то имя этой библиотеки, напримерmy-lib-base
.npm link
npm link my-lib-base
.Это позволяет не делать огромные пути "вверх", не лазить вне папки с исходниками, структурировать проекты. При этом вебпак будет подхватывать изменения библиотеки
base
на лету и пересобирать зависимые библиотеки.В идеале развернуть свой npm репозиторий, чтобы можно было разрабатывать независимо библиотеки разным людям, при этом тем, кто разрабатывает "ядро" не нужно было запускать локально вебпак со сборкой, например, ui-элементов.
TheHat
Спасибо за совет!