Однозначная конфигурация Webpack с Typescript

Вы можете написать конфигурацию Webpack на Typescript, и это избавит вас от лишних хлопот. Документы Webpack могут заставить вас поверить в то, что использование Typescript требует сложной индивидуальной настройки, но на самом деле это так же просто, как установить один модуль и изменить ваши расширения с .js на .ts!

Если вы знакомы с трудностями Webpack и просто хотите знать, как написать свою конфигурацию на Typescript, вы можете перейти ко второму разделу Typescript на помощь.

Webpack известен своей сложностью. Просто спросите авторов:

Проблемы, которые необходимо решить: слишком низкий уровень Webpack; Нет разумного набора значений по умолчанию; Терминология не имеет смысла; Слишком много шаблонов в обработке логики окружения; В CLI отсутствуют строительные леса и инициализация; Конфигурация "запутанная"

И вам не нужно далеко ходить за сообщениями в Medium вроде этого:

Сколько времени вы потратили на то, чтобы возиться с конфигурацией Webpack, чтобы попытаться сделать его производительным и без проблем работать с вашим HMR, вашим линтером, TypeScript, Babel - чтобы вы наконец смогли достичь Zen для разработчиков. Сколько времени вы потратили зря на то, чтобы заставить Webpack делать то, что вы хотите, вместо того, чтобы фактически отгрузить продукт?

В этой постоянной ветке по удобству использования есть несколько отличных идей о том, как улучшить инструмент сборки. Это делает меня счастливым, потому что я, со своей стороны, потратил слишком много своей энергии на то, чтобы возиться с моей конфигурацией, вместо того, чтобы создавать что-то крутое. Назову несколько выделяющихся вопросов:

  • Множество способов изобразить одно и то же. Возьмем лишь один пример: loaders интерфейс Webpack невероятно либерален. Загрузчик может быть строкой, состоящей только из имени, объектом, содержащим набор параметров, или строкой с параметрами запроса. У вас могут быть preLoaders и postLoaders, и у вас могут быть ключи с именами loaders (множественное число) и / или loader (единственное число) или use (почему бы и нет?). Вы можете передавать параметры с помощью options, а иногда с query. Я мог бы продолжить. Есть много способов добиться желаемого эффекта, хотя некоторые из них более верны, чем другие. Это ошеломляет новичка и может привести к досадным ошибкам для любого. Вот лишь некоторые из возможностей:
  • Непрозрачные или несуществующие сообщения об ошибках. Трудно отследить ошибки в вашей конфигурации, когда единственное принудительное ограничение - это экспорт объекта Javascript. Шокирующее количество ответов Stack Overflow о Webpack сводится к у вас есть опечатка, и проверка на это - первое предложение в этом посте, озаглавленном Как исправить Webpack, если он не может найти ваши модули . Это верхушка айсберга, когда речь идет о способах, которыми Webpack позволяет незаметно ломать вещи.
  • Легко случайно использовать старый синтаксис - авторы Webpack постарались обеспечить обратную совместимость. Это хорошо с их стороны, потому что позволяет разработчикам быть уверенными, что Webpack не сломается под вами (и это было бы катастрофой, если бы это произошло, потому что это инструмент сборки, который выбирают для увеличения часть Интернета). Кроме того, он не накладывает никаких ограничений на дизайн или предпочтения людей. Но это отсутствие ограничений также является кошмаром, потому что использовать устаревшие API слишком просто. В новых проектах следует использовать Webpack 2, но при просмотре примеров в Интернете практически невозможно различить их. У этих двух версий много общих соглашений об именах, и они не обеспечивают согласованности использования. В результате большинство людей используют смесь Webpack 1 и 2, даже не подозревая об этом.
  • Отсутствие разумных настроек по умолчанию. Возможности настройки Webpack очень мощные, но они также оставляют в покое новичка (или даже того, кто просто хочет что-то быстро придумать). Интерфейс не приводит вас к разумным настройкам по умолчанию; это требует предварительных знаний об основном инструменте, прежде чем вы даже сможете начать работу.

Каждая из этих областей представляет собой серьезные проблемы, но они также свидетельствуют о самой сильной стороне Webpack: его гибкости. Это классический пример компромиссов. Сосредоточение внимания на этих недостатках может принести в жертву другие характеристики, которые делают его таким мощным инструментом сборки. Удобство использования Webpack будет продолжать улучшаться, но, в конце концов, мы должны принять сложность, присущую всем, что можно настраивать.

К счастью, эту проблему мы можем решить с помощью типов, не жертвуя гибкостью!

Машинопись приходит на помощь

Примерно в то время, когда я начал использовать Webpack, друг познакомил меня с Typescript, и с тех пор я использовал его во всех своих побочных проектах. Я не буду здесь торопиться - я позволю основной команде Typescript сделать это, но скажу, что это изменило то, как я создаю программное обеспечение. В течение нескольких недель я заменил каждую строку Javascript строгим Typescript. Единственным исключением были мои файлы конфигурации Webpack, потому что инструмент не поддерживал TS, насколько я мог судить по документации. Я подумал, что мне придется вручную компилировать конфигурацию в файлы JS перед каждой компиляцией, что было бы медленным и еще одним источником разочарования по поводу Webpack.

Так что вы можете представить себе мою радость, когда я обнаружил, что был неправ! Вы можете написать свою конфигурацию Webpack на Typescript, и это несложно. Насколько мне известно, в документации Webpack об этом нигде не упоминается, но после некоторых поисков я обнаружил этот одинокий переполнение стека, в котором отмечалось, что в исходном коде есть подсказка.

Изменить: с тех пор, как я написал этот пост в июне 2017 года, команда Webpack добавила отличную документацию о том, как писать файлы конфигурации в Typescript. Вы можете найти его здесь.

Все, что вам нужно сделать, это:

  1. Добавьте ts-node как devDependency.
  2. Замените расширение вашей конфигурации с .js на .ts.
  3. Запускаем webpack - конфиг webpack.config.ts как обычно.

Это действительно так просто.

Открытие с типами

Вы можете сделать больше, но даже после этих двух крошечных изменений вы сразу заметите преимущества. (Я займусь расширениями позже.) В частности, фантастические определения типов для Webpack, и вы получаете их бесплатно. Автозаполнение и защита типов в Typescript направят вас к правильному использованию, без затруднений при поиске в Google или угадывании того, как что-то называется. Вместо того, чтобы искать в Google перегруженные термины, такие как разрешить или модуль, которые приведут к множеству нерелевантных или устаревших результатов, вы можете увидеть параметры, предоставляемые интерфейсом в текстовом редакторе. А если вы хотите видеть, что происходит под обложками, Перейти к определению переместит вас прямо к месту в .d.ts файле.

Пакет Typescript для Sublime великолепен. Вы можете добавить ярлык к вашей раскладке, который упростит навигацию по коду, даже в /node_modules и другие каталоги, исключенные вашим sublime-project файлом:

{ "keys": ["ctrl+d"], "command": "typescript_go_to_definition" }

Возьмем интерфейс загрузчика, упомянутый в начале этого поста. Его гибкость может быть ошеломляющей. Кажется, каждый придумывает свои собственные идеи, и непонятно, почему вы использовали один подход вместо другого.

Копание в типах дает большую ясность. Типы могут заставить гибкость Webpack работать на вас, а не против вас. Для иллюстрации давайте создадим простой конвейер для компиляции файлов CSS. Начнем с этого простого скелета:

Обратите внимание, что в следующих примерах я сократил комментарии и лишние строки, чтобы сосредоточиться. Полные определения типов Webpack вы можете найти здесь.

Определив, что конфигурация имеет тип webpack.Configuration, мы можем следить за типами, чтобы заполнить огромное количество информации.

1) Перейти к определению для webpack.Configuration

Щелкните по типу, и функция «Перейти к определению» (GTD) Typescript переместит нас прямо туда, где определен интерфейс webpack.Configuration. Мы видим, что он принимает необязательный модуль типа Module, который, как мы знаем, является местом, где мы хотим определить наши правила загрузчика. Опять же, используйте GTD, чтобы перейти к определению интерфейса Module.

2) Привязать модуль к интерфейсу Webpack 2

Здесь мы видим, что Module - это тип объединения OldModule или NewModule. Это интересно. Если бы мы были новичками в Webpack, мы могли бы не знать, что модуль узла webpack одновременно поддерживает как Webpack 1, так и 2. Мы могли бы даже не знать, что существуют две отдельные версии. Это дало бы нам первую подсказку.

Ага! Вот наш первый взгляд на некоторые сложности интерфейса. Мы видим, что OldModule и NewModule имеют разные имена ключей для массива преобразований, примененных к нашим файлам: loaders и rules. И оба они принимают массив Rules, подразумевая, что они делают в основном одно и то же. Прохладный. Теперь, когда мы видим в Интернете примеры с разными именами ключей, мы можем знать несколько вещей: (1) их интерфейс эквивалентен, если не считать другого соглашения об именах, и (2) если кто-то использует loaders, они используют Webpack 1.

Давайте придерживаться метода Webpack 2 NewModule. Если мы будем использовать GTD для Rule, мы увидим:

Начнем снизу. Мы видим, что Rule представляет собой объединение нескольких конкретных типов: LoaderRule, UseRule, RulesRule и OneOfRule. Некоторые из них представляют собой объединение правил Old* и New*. Мы хотим использовать только функции Webpack 2, поэтому давайте удалим Old* интерфейсы, чтобы сосредоточиться на важных для нас параметрах.

NewLoaderRule - это просто сокращение для NewUseRule. В первом случае применяется один загрузчик, а во втором - массив загрузчиков. Я предпочитаю использовать use для всех правил, включая правила с одним загрузчиком, потому что это делает использование более последовательным во всей конфигурации. Для будущего разработчика гораздо более очевидно, что они делают то же самое, вместо того, чтобы копаться в типах, чтобы понять, что loader - это просто сокращение.

Итак, давайте еще больше ограничим наш интерфейс, удалив NewLoaderRule из наших консолидированных типов. Это оставляет нам:

3) Ограничьте интерфейс правил только тем типом, который нам нужен

Это немного упрощает наши варианты. Теперь давайте рассмотрим вопрос о том, что делают «правила делегирования». До написания этой статьи я никогда не сталкивался с использованием RulesRule или OneOfRule, так что для меня это тоже было в новинку. Оба они принимают массив из Rule, поэтому мы могли бы вложить Rule произвольной глубины. Странный. Комментарии к атрибутам BaseRule объясняют:

  • rules - это «массив Rule, который также используется, когда [родительский] Rule совпадает», и
  • oneOf - это «массив Rule, из которого используется только первое совпадение Rule, когда совпадает [родительский] Rule»

Другими словами, родительский Rule действует как фильтр, и любые файлы, соответствующие этому фильтру, затем передаются по конвейеру через списки Rule в rules и oneOf. Например, вы можете захотеть применить разные загрузчики к одному и тому же расширению файла в зависимости от издателя (т. Е. Места, где был импортирован ресурс). Вы можете использовать правила, чтобы указать использование style-loader только тогда, когда файл CSS импортируется в файл Javascript:

Вы можете использовать oneOf для маршрутизации к определенному загрузчику на основе совпадения, связанного с ресурсом:

Полезно знать о rules и oneOf на случай, если мы наткнемся на них в будущем, но они чрезмерны для того, что мы пытаемся сделать: компилировать файлы CSS. Итак, давайте удалим RulesRule и OneOfRule из наших объединенных типов. Это оставляет нам:

4) Напишите свой NewUseRule

Теперь ясно, что нам нужно делать. Нам нужно написать правило, определяющее тест и использование. Мы можем придумать и определить другие вещи, но эти два ключа - самый минимум для удовлетворения интерфейса. Вернемся к скелету, который мы определили в начале, и заполним его:

Потрясающие! Теперь у нас есть простая конфигурация Webpack, которая компилирует файлы CSS. Подводя итог тому, что мы здесь сделали:

  1. Используйте GTD, чтобы просмотреть определение webpack.Configuration.
  2. Ограничьте интерфейс Module только функциями Webpack 2.
  3. Ограничьте интерфейс правила до NewUseRule.
  4. Напишите правило для передачи .css файлов через style-loader.

Это была стена текста, но процесс разделения интерфейса, чтобы понять, что вы хотите сделать, на практике происходит быстро. Эти шаги могут занять секунды, если вы научитесь разбираться в определениях типов.

Документация в вашем текстовом редакторе

Не только подробные определения типов Webpack, но и сопровождающие добавили ко всему обширные комментарии. Если вы когда-нибудь задумывались, как повлияет изменение параметра devtool, вы увидите хороший комментарий, подобный этому, над этой строкой в ​​определении вместе со всеми возможными значениями:

Определения типов часто служат лучшей документацией, чем сайт проекта. Примеры и руководства бесценны, но настоящая проблема Webpack возникает всякий раз, когда вы делаете что-то нестандартное. Официальная документация может перечислить только определенное количество перестановок параметров конфигурации. Даже если бы они отметили все до единого, всплытие этих примеров было бы кошмаром.

Файлы определения типа имеют дополнительное преимущество, позволяя вам оставаться в текстовом редакторе. Часто я могу решить свою проблему, не покидая Sublime, даже когда я изучаю совершенно новый инструмент в обширном наборе функций Webpack. Эти инструменты позволяют мне находить ответы, скрытые в определении типа, когда раньше мне приходилось их выслеживать.

Вдобавок ко всему, «документация», предоставляемая типами, не устаревает. Сообщения в блогах и примеры в документации могут рассинхронизироваться с инструментом по мере того, как его версия меняется. За последние несколько лет Webpack быстро улучшился, поэтому, работая над примером, который я нашел в Интернете, я всегда беспокоился, что он больше не применим. Я никогда не испытывал этого беспокойства при отработке типов. Их определения отображаются непосредственно на интерфейс, от которого вы зависите, и их обновление является частью изменения версии. Вам никогда не придется беспокоиться, если вы ошиблись в типах.

Ограничение интерфейса тем, что вам нужно

Чтобы получить все преимущества написания конфигурации в Webpack, вам нужно добавить еще несколько вещей. Одним из дополнений является то, что вы можете добавлять типы возвращаемых данных к различным компонентам конфигурации. Например, мне нравится добавлять тип webpack.Configuration к частям, потому что это гарантирует, что они могут быть webpackMerged в файле конфигурации верхнего уровня.

Мне также нравится создавать пользовательский тип Options, который можно передать частичным функциям для создания большей конфигурации. Это хорошо, потому что обычно вы заботитесь только о настройке нескольких параметров Webpack между средами разработки, тестирования и живыми средами. Это позволяет ограничить огромное количество возможностей интерфейсом гораздо меньшего размера. Конечно, вы уже можете сделать это с помощью типичного Javascript, просто создав функцию-оболочку, которая принимает подмножество параметров в качестве аргументов, но с Typescript у вас есть дополнительная возможность отслеживать, какие параметры явно определены конфигурацией верхнего уровня, а какие - последующие эффекты этих вариантов. Короче говоря, Typescript позволяет ограничить интерфейсы, с которыми вы работаете, только теми частями, которые вам нужны.

В предыдущем примере с загрузчиком мы мысленно консолидировали интерфейс Loader. Это было полезно для минимизации площади интерфейса до того, что нам было нужно. Мы можем пойти еще дальше и применить эти типы.

Например, мы можем ограничить интерфейс, чтобы он использовал только функции Webpack 2. Распространенная жалоба заключается в том, что трудно различить документацию для Webpack 1 и 2, и легко непреднамеренно использовать устаревший подход. Чтобы избежать этого, вы можете расширить тип webpack.Configuration, чтобы ограничить интерфейс только тем стилем, который вы хотите использовать. Машинопись позволяет вам:

Таким образом, вы можете сделать что-то вроде этого, если хотите ограничить конфигурацию разрешения только типом NewResolve, а не типом по умолчанию, который также позволяет OldResolve:

Вы можете принудительно использовать только NewUseRule, как в примере с загрузчиками в начале:

Теперь ваш Config тип гарантирует, что никто случайно не использует другой интерфейс. Вы не сможете нарушить соглашение о кодовой базе без явного указания изменений в интерфейсе. Конфигурация Webpack достаточно сложна, чтобы держать ее в голове, а распространять знания о том, как должна работать каждая часть, еще сложнее в большой организации. Типы могут сделать за вас большую часть этой работы. Они берут на себя большую часть умственных затрат, связанных со следованием соглашениям или распутыванием всех возможностей, скрытых в Webpack.

Аннотации типов снимают большую часть боли, создаваемой сложным и чрезмерно разрешающим интерфейсом Webpack. Есть и другие способы, которыми проект может улучшить удобство использования, но Typescript помогает решить эти проблемы.

Этот пост - всего лишь начальный набор идей о том, как я использовал инструменты Typescript для улучшения своего рабочего процесса - я с нетерпением жду возможности изучить его дальше. Я также хочу услышать, как другие использовали типы (и другие инструменты!) Для улучшения опыта разработчиков Webpack, так что не стесняйтесь обращаться к нам.

Спасибо Джону Бэкусу за его помощь в проведении мозгового штурма и редактировании этого сообщения вместе со мной.

Нет времени помогать? Хотите отдать другим способом? Станьте спонсором или спонсором webpack, сделав пожертвование нашему открытому коллективу. Open Collective не только помогает поддерживать основную команду, но также поддерживает участников, которые в свое свободное время потратили много времени на улучшение нашей организации! ❤