Саша Беспоясов
Это я.

Переписал свой сайт на Next и TypeScript

В этом году моему блогу 10 лет. В честь этого я немножко обновил сайт внешне и «множко» под капотом. Решил полностью сменить стек и переписать сайт на Next.js с TypeScript. В этом посте делюсь впечатлениями от стека, процесса переезда и использования.

Мотивация

Основная претензия к моему прошлому стеку — неудобство в использовании.

Мой велосипедный Gulp-сборщик работал, но с огромным количеством условностей. Самой проблемной условностью был инфраструктурный код. Если я, например, хотел добавить новый шаблон для страницы на сайт, то мне приходилось писать не только код этого шаблона, но ещё и его обработчик.

Если с новыми постами в блог это не особо мешало, то обновление дизайна — сильно тормозило. Изменить дизайн страницы становилось целым приключением, начинать которое не очень-то хотелось.

Я хотел получить такой стек, который бы не нагружал меня дополнительной работой.

Ограничения

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

Всё ещё в топку CMS

Я всё ещё хотел простенький сайт безо всяких CMS, бекендов и баз данных. Мне всё ещё хотелось хранить заметки в независимом формате, например, в текстовых файлах.

Серверный рендеринг

Хотелось, чтобы результат сборки можно было просто посмотреть в браузере, не поднимая сервер. А ещё хотелось чтобы контент оставался доступен, если отключён JavaScript.

DX ≤ UX

Удобство разработки (Developer Experience, DX) — насколько удобно писать код и насколько комфортны инструменты разработки — это важно, однако удобство конечных пользователей (User Experience, UX) важнее. Соответственно и вкладывать больше ресурсов по умолчанию следует в UX.

Но так как проект в первую очередь для меня, я не хотел жертвовать и своим комфортом тоже. Хотелось найти такой инструмент, чтобы количество ресурсов на разработку было минимально, а удобно было бы и пользователям, и мне. В идеале неравенство «DX ≤ UX» должно было превратиться в равенство «DX = UX».

Хотелки

Кроме принципиальных ограничений были ещё и хотелки, которые сделать «было бы неплохо, но необязательно».

Доменные типы

Я чё-то привык отделять бизнес-логику в коде ото всего остального. Обычно всю дополнительную информацию о сущности такого плана я храню в типах, поэтому в требования попал TypeScript.

Компоненты

Прошлый сборщик был плох тем, что компоненты в нём были компонентами курильщика.

Я использовал pug и «компоненты» были кусками разметки, которые подключались в шаблон. Стили были размазаны по файловой системе, компоненты было трудно подключать, я постоянно забывал синтаксис шаблонизатора. Короче, хотелось React — у него концепция компонентов реализована прекрасно, да и я привык к нему.

Инфраструктура из коробки

Под инфраструктурой я имею в виду всякие оптимизации графики, настройки service-worker, работу в офлайне и прочее. Хотелось, чтобы какие-нибудь умные люди подумали, придумали и сделали всё за меня.

Новый стек

Так как я хотел статический сайт, я выбирал между генераторами статики: Eleventy, Gatsby и Next.js. Пройдусь по всем и расскажу, почему в итоге выбрал Next.

Eleventy

Eleventy — явно не тот инструмент, который бы мне подошёл. От компонентов в нём одно слово: всё очень похоже на мой предыдущий самописный сборщик, который меня не устраивал. Да и чтобы TypeScript нормально настроить, нужно много думать.

Я также не понял, как удобно и без костылей сделать навигацию со ссылками на предыдущий и следующий пост.

Кроме этого, Eleventy мне не понравился его многословным конфигом, который бы появился, когда я настроил всё, что мне нужно. Ну вот не люблю я, когда код не помещается в экран. И если на работе я могу с этим мириться (привет, Webpack), то в проекте для души хотелось этого избежать.

(Внимание! Я не говорю, что Eleventy плохой; я говорю, что он не подходит для моей задачи.)

Gatsby

В Gatsby есть компоненты и TypeScript. Почти ура, но я невзлюбил его из-за проприетарных команд в консоли.

Ну то есть как, к самим командам я никаких претензий не имею. Но когда первое, что я вижу в документации — это настройка Vendor CLI, меня почему-то коробит.

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

(Внимание! Я не говорю, что Gatsby плохой; я говорю, что он не подходит для моей задачи.)

Next.js

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

(Внимание! Я не говорю, что Next идеальный; я говорю, что он подошёл для моей задачи.)

TypeScript

На вопрос «Зачем TypeScript?» у меня есть два ответа. Первый — я привык к его удобству. Я чувствую гораздо меньше уверенности в своём коде, если пишу на JS, а не на TS. Рефакторить код на TS тоже гораздо удобнее из-за встроенных инструментов в IDE.

Второй ответ — мне хочется читать свой код, как предложение. Я хочу, чтобы в нём были все необходимые данные, чтобы понять написанное. TypeScript даёт инструменты, чтобы выражать свои мысли точнее, полнее и адекватнее.

Я использую типы для передачи контекста и бизнес-логики сущностей. Мне нравится, использовать тип-алиасы над примитивами, объясняя, почему я возвращаю именно такое значение. Так как этот проект для души, хочется, чтобы было красиво.

Кроме этого, я буду открывать этот проект время от времени, а не каждый день. Типы и интерфейсы помогут мне быстрее въехать в контекст и вспомнить, что я имел в виду.

React

На вопрос «Зачем React?» у меня тоже есть ответ. Я хочу нормальных компонентов.

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

Оно конечно хорошо, но это не компоненты. Вот я добавил новое меню: мне надо обновить шаблон и отдельно стили. Если я убрал какой-то блок, мне надо найти его стили и убедиться, что они больше нигде не используются, перед тем, как убить их. Я хочу удаляя компонент сразу быть уверенным, что я удалил всё, что с этим компонентом связано, отовсюду, не проверяя.

Но если со стилями я ещё как-то мирился, то вот кусочки pug-шаблонов, для которых данные надо прокидывать абсолютно костыльнейшим образом, меня бесили. Я не хочу писать коряво, если знаю, что можно удобнее.

Ну а снова ударяться в пуризм и писать совсем голые HTML-страницы и CSS-стили я не хочу. (Как бы я ни убеждал себя 5 лет назад в обратном, это неудобно.)

Ожидания

Идеальный стек для меня представляется таким, на поддержание которого сил не уходит в принципе, а использование максимально прозрачно и просто. Спойлер: стек с Next не такой :–D

Наверное, корневая проблема — в моём перфекционизме и стиле написания, но я ожидал, что у меня как минимум не будет проблем с графикой и экспортом в статический HTML, так как Next позиционирует себя как статический генератор. Так же я ожидал, что не будет проблем с импортом текста постов. Но оказалось, что это ожидания…

…Которых Next не оправдал

Заранее: если вы знаете, как правильно, мой репозиторий открыт для пул-реквестов :–)
Буду рад услышать идеи. Теперь поехали.

Frontmatter?..

Я храню тексты постов в mdx-файлах. Меня удивило, что по умолчанию Next не поддерживает frontmatter для метаданных.

Мне нравится идея хранить метаданные о посте в том же файле, что и сам пост — типа всё сразу в одном месте. Но для поддержки Frontmatter мне бы пришлось ставить ещё один пакет. А это ещё одна зависимость, которую потом надо менеджить и обновлять.

Ну а так как эта зависимость мне бы потом никак не помогла собирать метаданные для списков постов на странице блога, я решил использовать обычные экспорты внутри mdx.

Вот так выглядит исходник этого поста: mdx-файл с экспортом метаданных вначале
Вот так выглядит исходник этого поста: mdx-файл с экспортом метаданных вначале

Статические свойства и соседние посты

Вторая проблема, с которой я столкнулся, — это статические свойства у страниц.

Next использует функции getStaticProps и getStaticPaths, чтобы определить, какие страницы и данные нужны, чтоб нагенерировать HTML. Я использую эти функции чтобы получить списки всех постов и проектов, которые есть. Эти списки мне помогают определить, ссылки на какие посты будут соседними.

Первая претензия — по правилам Next эти функции и компонент страницы надо экспортировать из модуля страницы и только.

Из-за этого требования файл жирнеет
Из-за этого требования файл жирнеет

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

Реальная же проблема возникла, когда я захотел создать компонент со ссылками на предыдущий и следующий посты для страницы поста. Использовать getStaticProps можно только внутри файла-страницы, но при этом использовать getStaticProps внутри mdx-страниц оказалось нельзя.

В обсуждении разработчики предлагают много решений, но ни одно мне не показалось «не костылём». Я решил, что использовать хрупкие решения, которые опираются на недоработки фреймворка, — себе дороже. Поэтому решил не использовать mdx-файлы как страницы, а сделать компонент страницы, который бы импортировал в себя содержимое mdx-файла.

Переезжать с mdx-страниц на компонент затратно

Чтобы переехать, надо не только создать страницу для, например, поста, но ещё и обновить все бывшие mdx-страницы и создать лейаут.

Мне повезло, что я делал это в начале работы, когда я перенёс ещё мало постов. На большем количестве это стало бы проблемой.

Не удалось избавиться от дублирования данных

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

Я так и не нашёл красивого решения для этой проблемы: получалось либо слишком много кода, либо появлялись циклические зависимости. Пока что я оставил адреса постов в их метаданных. Эти адреса я потом использую, когда ссылаюсь с соседних постов. Если у вас есть идеи, как сделать лучше — напишите, пожалуйста.

Получение метаданных

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

Я использую метаданные страниц, чтобы определять порядок сортировки, узнавать название и адрес страниц. Для получения метаданных я использую модуль api, который обращается к persistence за содержимым mdx-файлов. Из содержимого он вычленяет все экспорты и находит объявления const metadata, которые потом превращает в список объектов.

И сделать архитектурно красиво, блин, — неоправданно сложно. Я так и не нашёл готовых решений, ни чтобы получать frontmatter, ни чтобы получать экспорты. Пришлось самому написать AST-трансформеры, всё ещё сомневаюсь, что нельзя было проще :–/

TypeScript не поддерживает MDX

Пока что поддержки MDX у TypeScript нет. Для меня это значит, что я не могу пользоваться автодополнением, когда указываю теги для поста. (Из-за чего, возможно, будут опечатки.)

Оптимизация графики

Вот уж тут-то я думал, будет всё хорошо, ведь есть next-images и next-optimized-images.

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

Но к тому моменту я уже как-то приуныл, подразочаровался в Next и написал свою ops-обвязку для этого.

На днях правда появилось обсуждение о нормальной оптимизации картинок, но кажется SSG хотят обойти стороной. Хотя казалось бы.

Генерация RSS

С генерацией RSS-фида не было бы проблем, если бы проект крутился на сервере с node.js. Я бы мог сделать отдельную страницу и указать ей canonical.

Но мой сайт статический, а значит мне надо было сделать RSS руками и встроить его генерацию в процесс сборки.

Телеметрия по умолчанию

Что меня конкретно выбесило — это включённая по умолчанию телеметрия. Как правильно заметили в обсуждении, Next должны были спросить меня, согласен ли я не неё, ещё до запуска первого npm run dev.

Но они мало того, что не сделали этого, так ещё и запрятали страницу, рассказывающую, как это отключить куда-то в жопу мира.

Вот так это выключается. Не через конфиг, блин, а командой в консоли
Вот так это выключается. Не через конфиг, блин, а командой в консоли

А узнал я об этой «фиче» случайно — когда собирал проект. Они перед началом сборки выводят маленькую приписочку в консоли, что «мы там собираем данные, вот уведомление, а то что оно пропадёт, как только появится лог сборки, ну так это так, ничего особенного».

Отключить один раз и навсегда — тоже нельзя. Это делается для каждой машины отдельно, ъуъ! Написал себе скрипт, который отключает это говно сразу после установки проекта.

Подписываюсь под каждым словом
Подписываюсь под каждым словом

Короче, Next как статический генератор — сыроват

Ну либо я его не до конца понял.

Я ещё готов мириться с какими-то штуками типа «страницы должны обязательно быть в папке pages» или там «экспортировать всё нужно из одного модуля» — пофиг. Но графика и телеметрия оставили неприятный осадок.

Что хорошего

Вы только не думайте, что Next совсем уж плох :–D
У него есть и плюсы.

Хотелки закрыты

Требования и хотелки, которые я предъявлял стеку, закрыты — это радует. Разве что инфраструктура слегка подкачала в плане графики, но всё же.

Удобнее чем велосипед

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

Если раньше каждый новый шаблон нужно было превратить в страницу, то сейчас этим занимается Next.

Удобство разработки выросло в разы. Я смог резко сократить дублирование кода, точно знаю, что при удалении модуля, удалятся и его стили отовсюду, рефакторить код стало приятнее.

Удобный офлайн

Для Next есть отличный пакет, который сам отлично настроил мне service-worker — next-offline.

Предлагает стратегии кеширования, удобное управление лимитами памяти, которую занимает кеш, обновление происходит без проблем. Короче, рекомендую.

Генерация статики

Я хотел HTML на выходе, я получил HTML на выходе.
Я хотел, чтобы оно работало в браузере без JS, оно работает в браузере без JS.
Замечательно.

Оптимизация кода

За меня уже настроены лоадеры, минификаторы, три-шейкеры, трансформеры стилей, бандлеры, и вот это вот всё. Очень не хотелось со всем этим иметь дело напрямую. Next меня освобождает от этой необходимости.

Нет сервера и БД

Мои посты всё ещё хранятся в простых текстовых файлах, и я могу съехать на другую технологию в любой момент. Мне не надо поднимать серверов и бекендов для продакшена, достаточно положить файлы в нужное место.

Иерархия страниц

В папке pages можно не только хранить файлы-страницы, но и создавать подпапки, из которых потом будет строиться структура путей.

Так, /pages/tag/travel.tsx превратится в страницу /tag/travel/index.html. При этом можно рядом иметь /pages/tag/[id].tsx который будет обрабатывать все остальные пути в /tag/travel/*, и не будет никаких конфликтов.

Структура папок и файлов внутри pages
Структура папок и файлов внутри pages

С прошлым стеком это было головной болью, сейчас же — радость, пони и радуга.

По итогу

Большая часть моих придирок к Next — это всё же пуризм и старческое кряхтение. В целом-то фреймворк годный, писать средней сложности приложения и сайтики на нём можно.

Что-то более сложное порождает велосипеды, о которых я писал выше. Буду рад, если у вас есть идеи, как можно улучшить мои решения проблемных проблем. Ишьи и пул-реквесты всегда открыты :–)

Почитать по теме

У меня в блоге

Технологии

Инструменты

Остальное

DI с TS на практике