Всем привет. Я являюсь ведущим frontend-разработчиком компании 21Yard. Мы разрабатываем сервис для поиска строительных подрядчиков.
На проект я пришел желторотым масленком, который мало смыслил в seo-продвижении продукта, но жизнь внесла свои коррективы, и сейчас я хочу рассказать о современном ssr-фреймворке -vike, показать его основные аспекты.
P.S. Статья рассчитана в первую очередь на таких же молодых и зеленых, но будет возможно полезна и матерым калачам.
Мотивация для написания статьи
Vike - молодой фреймврорк, который еще не успел добраться до версии 1.0.0. Он имеет неплохую документацию на английском, но, к сожалению, русскоязычных гайдов на него нет. Во многих микро-моментах приходилось разбираться самому, т.к. даже спросить было не у кого. Надеюсь, эта статья откроет для многих альтернативу next-у, а кому-то поможет разобраться в основных положениях этого замечательного фреймворка.
Еще немного введения...
Придя на проект, я с энтузиазмом взялся за дело. На момент старта моей работы у нас уже существовал интернет-портал, написанный на php. К сожалению, он был написан на устаревшем фреймворке, поэтому было принято решение переписать его с нуля на чем-то современном - выбор пал на React. Однако, параллельно кодингу шел и маркетинг. К работе был привлечен seo-специалист, по указаниям которого мне нужно было вносить микро-правки в старый портал. Тогда я узнал, что такое seo, и что для него нужен ssr...
Справка для самых маленьких
Обычное React-приложение, созданное через npm create vite (или, упаси господь, npx create react app) после сборки представляет собой набор статики - index.html, пачка стилей, пачка жаба-скрипта. Это значит, что пользователь, запросивший ресурс , получает пустой html файл, после загрузки которого начинается генерация видимого контента. К сожалению, поисковые роботы ленивы и не очень хотят ждать, пока весь исполняемый код будет выполнен и DOM пополнится новыми узлами, поэтому максимум, что мы можем ему предложить - пара статичных мета-тегов внутри head. Это значит, что в поисковой выдаче мы можем оказаться только в виде 1 единственной ссылки (ведь внутренний роутинг так же будет проигнорирован). Такой подход называется CSR - Client Side Rendering
Чтобы сделать возможным индексацию нашего сайта, нам требуется, чтобы весь html генерировался на стороне сервера, и только потом отправлялся поисковому роботу, со всеми мета-тегами, заголовками, артиклями, ссылками и прочими прелестями веб-разработки. Это называется SSR - Server Side Rendering. Чтобы заставить React делать SSR, требуется дополнительный фреймворк.
Именно это я выяснил, когда захотел поглубже узнать про seo. Тогда было принято решение в срочном порядке внедрять какой-либо SSR-фреймворк, пока разработка не зашла слишком далеко. Из подобных я знал только Next.js, отзывы о котором в телеграмм-сообществе реакта мне были не по душе. Мне не очень хотелось сильно перелопачивать на половину готовый продукт, переходить на другой СТМ и так далее, поэтому я пошел искать. На проекте использовался Effector, и именно у этого сообщества я решил спросить о подобном решении. И мне посоветовали Vike.
Что за зверь?
Vike - до одури гибкий фреймворк на базе vite. Он позволяет делать SSR, CSR, SSG, а так же гибридные и изоморфные (при первом запросе страницы содержимое генерируется на стороне сервера, а при дальнейшем роутинге все происходит динамически на клиенте) веб-приложения.
В чем гибкость?
-
Универсальность - с его помощью можно делать SSR, CSR, SSG, гибридные и изоморфные приложения
-
Свобода выбора - он предлагает заготовленный для каждого ui-фреймворка (react, vue, angular, тп) свой адаптер, но не запрещает написать его самостоятельно.
-
Мощный роутинг - позволяет настроить роутинг на базе архитектуры папок, или же написать более сложную логику роутинга для каждой страницы отдельно
-
Четкое разделение ответственности - защита от несанкционированного доступа, выборка данных, настройка содержимого хеда, тайтла, дескрипшна, верстка лайаута, эффекты начала и конца рендеринга и прочее - каждый этап рендеринга регулируется индивидуально
-
Совместимость с СТМ - при изоморфном подходе не требуется вообще никаких манипуляций для корректной работы СТМ; Для строго SSR-подхода существуют адаптеры.
-
Перспективы - vike активно развивается, появляются новые фичи и приколюхи, а по интересующим вопросам и предложениям можно пообщаться в дискорде с его мейнтейнерами
А минусы будут?
-
0 статей на тему на русском языке. Причина, по которой существует эта статья
-
Трудности с обратной совместимостью. Для новых пользователей будет не заметно, но свою боль я описал в пункте с лайаутами
-
Немного не очевидна настройка с нуля, документация не дает исчерпывающих ответов на возникающие вопросы, приходится конкретно покумекать. Темплейты же наполнены излишним мусором, перед разработкой приходится провести чистку.
Создание проекта
В документации рекомендуется создавать проект с помощью Bati - инструмент для настройки шаблона vike.
Весьма удобно - можно не утруждаться самостоятельным протягиванием базовых зависимостей. Я на проекте использовал связку React + Tailwind + Express, используем же ее и здесь!
Структура проекта не сильно отличается от обычного vite шаблона, мы рассмотрим главное:
express-entry - входная точка в приложение, обычный node сервер. По моему опыту, если мы делаем чисто фронтенд, то этот файл меняется не часто
vike-handler - входная точка в процесс рендеринга. В старых шаблонах этот код не выносился из express-entry
Папка pages - в корне папки содержатся общие для всех страниц настройки, а так же сами страницы.
Путь к странице по умолчанию строится аналогично пути к файлу, т.е. странице http://example.com/products/edit будет соответствовать путь pages/products/edit.
Исключения составляет папка index - для нее url к странице будет http://example.com, а так же папка _error - в ней хранится страница ошибок 404 и 500, которая не имеет своего url-а.
В папке index находится файл +Page.ts, в котором и описана главная страница.
Здесь никакой логики сверх стандартной реактовской не используется, поэтому пойдем дальше.
Рассмотрим страницу todo. В папке по мимо +Page.ts содержится так же +data.ts и +config.ts.
В +config находятся настройки, аналогичные корневой +config (vike в принципе позволяет переопределять все +хуки на более глубокой вложенности). В данном случае здесь содержится флаг prerender=false, что, вообще-то говоря не имеет смысла, т.к. это настройка по умолчанию. Гораздо интереснее будет рассмотреть файл +data
Хук, предназначенный для запроса данных. Здесь мы можем делать fetch-и и axios.get-ы сколько душе угодно. Главное помнить, что при использовании ssr запрос происходит не на клиенте, а на сервере, а значит document и window будут не доступны.
После получения данных в +data.ts мы можем обратиться к этим данным на нашей странице с помощью хука useData()
Стоит упомянуть параметр pageContext. Начало он берет из pageContextInit файла vike-handler. Туда мы можем заранее передать все нужные нам параметры, в том числе и результаты запросов к апи (но так делать лучше не стоит). Далее этот контекст передается во все требуемые узлы в плоть до самих реакт-компонентов. На самом деле содержимое data является частью pageContext, но имеет более удобный способ доступа. Чаще всего pageContext нам интересен из-за содержимых в нем urlPathName, urlParsed.search (доступ к query-параметрам), is404 и еще некоторых полей. Так же стоит отметить, что pageContext отличается на стороне сервера и на стороне клиента - часть информации во избежание утечек на клиент не передается. Однако, это можно регулировать с помощью настройки в +config - passToClient:
В данном примере мы говорим vike о том, что хотим иметь доступ к полям user и is404 на стороне клиента.
Кстати, параметр user не является стандартным для vike, в примере он является пользовательским полем. Чтобы определить свое свойство внутри pageContext, нужно передать его в pageContextInit, а так же определить тип передаваемого поля в vike-pageContext.d.ts
Теперь мы можем получить доступ к полю user в наших компонентах следующим образом:const {user} = usePageContext()
Продолжим рассматривать файловую систему. Обратим внимание на папку star-wars. В ней находятся две подпапки - index и @id. Первая представляет собой страницу с url-ом http://example/star-wars, а вот вторая - это страница с параметром id. Это значит, что этой странице соответствуют любые пути вида http://example/star-wars/@id , где @id - любая подстрока. Подобные конструкции c path-параметрами нужны, например, для индивидуальных страниц постов блога. Доступ к @id в коде мы можем получить через pageContext.routeParams - объект, содержащий в себе все path-параметры. Их может быть много, например http://example/star-wars/@id/@variant/@anyParam содержит сразу 3 параметра.
Так же стоит упомянуть о "глобусах" - именно так их называет яндекс-переводчик на странице с документацией. Мы можем задать путь вида http://example/star-wars/* - который будет означать любой url, начинающийся с http://example/star-wars/. Доступ ко всему содержимому после мы можем получить через pageContext.routeParams['*']. Однако, ситуации, где нужны глобусы случаются очень редко.
Может возникнуть вопрос — а как описать файловую структуру так, чтобы добавить глобус? Ответ — никак, ведь просто звездочка не может быть названием папки. Зато, мы плавно подошли к возможностям, не фигурирующим в разбираемом шаблоне — хук +route
+route хук нужен для случаев, когда мы хотим задать сложный путь к странице, не прибегая к манипуляциям с файловой структурой, или же в построении пути существует нестандартная логика. +route бывает двух видов:
-
Обычная строка, например
export default 'blog/posts/@id/@variant/*'
-
Функция-квази-предикат, который возвращает либо false, либо объект routeParams. Функция роута должна быть простой, т.к. она выполняется каждый раз, когда происходит роутинг. Перед переходом между страницами vike собирает все существующие пути, а так же выполняет все роут-функции в попытках выявить соответствие указанному url-у. Если в процессе выполнения функции произошел return false, vike считает, что url не соответствует данному роуту.
Так же для ограничений роутинга существует хук +guard, смысл которого прост - не допустить не санкционированный доступ к странице.
Я, как правило, выполняю здесь только проверку юзера, и, хотя данный хук не запрещает использовать запросы к апи, если они необходимы, как правило, лучше их делать в +data, чтобы была возможность в будущем получить нужную информацию через useData.
Более того, всю логику данного хука можно перенести внутрь +data, но для лучшего понимания кода стоит по возможности совершать проверки именно здесь.
Можно обратить внимание на конструкцию throw render(). Данный подход позволяет во время процедуры рендеринга (только на сервере) заменить рисуемую страницу на другую без замены url-а. Это хорошо подходит для страниц ошибок 404 и 500. Так же вместо номера статуса в качестве аргумента можно использовать строку пути к другой странице. Я таким образом иногда отрисовываю страницы, изображающие отсутствие контента в силу некоторых причин (у юзера нет доступа к странице, список постов юзера пуст, потому что юзер еще не создавал постов, т.п.)
Есть другой вариант перехода между страницами на этапе рендеринга - throw redirect() - делает в принципе то же самое, но меняет url на указанный в аргументе. Подходит для случаев, когда в процессе рендеринга нам нужно, например, отправить пользователя на страницу авторизации.
Последний способ программно перевести юзера на другую страницу - navigate() - но нужна эта функция только на клиенте, и делает она простую вещь - редиректит пользователя на указанный url.
Стоит рассмотреть хуки +Head, +title и +description.
В файле +Head должен находиться компонент, описывающий содержимое тега <head>. При необходимости, мы можем переопределять данный компонент на более вложенных уровнях.
+title и +description создают соответствующие теги внутри <head> с описанным в них содержимым. В качестве таковых может выступать как строка, так и функция от pageContext, возвращающая строку. Можно сгенерировать title и description на базе данных, полученных в +data, например, для страницы с карточками товаров по конкретной категории.
Теперь мы подходим к самому интересному - +Layout. Из названия становится понятно, что это что-то вроде родительского компонента, hoc-а, применяемого сразу к нескольким страницам. В разрабатываемом мною продукте здесь описаны header, footer страницы, а так же некоторые дополнительные оболочки. На мой взгляд +Layout переживает не лучшие времена. Дело в том, что еще в версии 0.4.171 лайауты можно было переопределять на более низких уровнях вложенности. Это позволяло задать 1 глобальный лайаут, но по необходимости удалить его на других страницах. В более поздних версиях лайауты сделали наследуемыми, убрав полностью возможность переопределить вышестоящий и убив обратную совместимость. Надеюсь, мои комментарии в соответствующем ишью смогут привести к результату)
Итого
Выше я постарался раскрыть основные положения vike, которых достаточно для 90% всех задач. Более подробно о фреймворке вы можете почитать у них в документации. Пишите свои вопросы, я обязательно отвечу! Если наберется достаточно вопросов, я выпущу статью, где постараюсь подробно раскрыть интересующие вас аспекты:)
Читайте также моего коллегу:
Сага о внедрении DDD на Fastify в двух частях
Паттерн «Интерпретатор»: что такое и как использовать
Автор: bighorik