Новый фреимворк в экосистеме React - Remix

Новый фреимворк в экосистеме React - Remix

В экосистеме React.js особенно сильно зарекомендовал себя Next.js, как основа создания динамических сайтов. Не важно, к какой сфере относится сайт - блог, электронная коммерция, развлекательный портал: еcли он написан на React, то, скорее всего, под капотом используется Next.js. В конце 2021 года на рынке появился новый фреймворк от создателей React Router: Remix. Я сделаю краткий обзор Remix и сравню его с Next.js.

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

Remix можно установить с помощью npm, как это обычно делается с любым JavaScript-фреймворком. С помощью npx create-remix просто создается новый проект. Скрипт спрашивает пользователя о предпочтениях, таких как использовать TypeScript или нет, предпочтительная реализация сервера (Express, Vercel, Netlify и т.д.), а также создает минимальный каркас проекта. Remix также предлагает более сложные шаблоны, в которых присутствуют преднастроенные стеки с решением для работы с базами данных, интеграционными и модульными тестами, конвейером развертывания и интеграция с различными хостинг-провайдерами.

Рендеринг на стороне сервера

В основе Remix стоит Server-Side Rendering. Но что это значит на самом деле? Под рендерингом понимается наполнение HTML-документа содержимым. В основном различают рендеринг на стороне клиента и на стороне сервера.

Например, если с помощью create-react-app создать одностраничное приложение React и сделать его доступным на хостинге, то при обращении к сайту будет загружен HTML-документ. Однако содержимое этого документа ограничивается ссылками на стили и скрипты, необходимыми для создания сайта. HTML-структуры строятся только в браузере, поэтому можно говорить о сайте с клиентским рендерингом (CSR). Важным моментом здесь является то, что обычно все маршруты сайта на основе CSR определяются одним и тем же HTML-файлом. А это означает, что для поисковых роботов видны одни и те же метаданные для всех маршрутов. Это плохо, например, в случае интернет-магазина, так как для разных страниц товаров не создаются отдельные отображения.

Если веб-сервер наполняет HTML-документ содержимым перед отображением пользователю, то это называется рендерингом на стороне сервера (SSR). Поскольку документы создаются динамически, вышеупомянутая проблема с метаданными не возникает. Однако веб-сайт отвечает дольше, поскольку серверу приходится сначала собрать ответ и он не может просто взять его напрямую из памяти, как в случае со статическим хостером.

Помимо CSR и SSR существует также Static Site Generation (SSG). В этом случае все маршруты сайта предварительно рендерятся, а HTML-документы загружаются на хост. Такой подход может быть интересен, если сайт содержит мало статических маршрутов, например, портфолио или сайт документации. Для интернет-магазина с сотнями продуктов, которые можно редактировать "на лету", такой метод не подходит.

Решения от Remix

Remix, как фреймворк, запускает сервер, обеспечивающий работу React-приложения. При вызове маршрута сервер Remix рендерит HTML-документ перед его передачей пользователю. Таким образом, браузер может сразу отобразить сайт. JavaScript выполняется в фоновом режиме, фактически превращая сайт в одностраничное приложение на стороне клиента. Этап, при котором клиент берет на себя рендеринг, называется гидрацией. Это дает Remix преимущества SSR, такие как динамические метаданные и более быстрое отображение содержимого в браузере, а также преимущества CSR - внесение изменений на сайт без перезагрузки HTML-документа.

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

Роутинг

Для определения маршрутов Remix использует файлы и каталоги. Все файлы модулей JavaScript, хранящиеся в каталоге app/pages, интерпретируются Remix как компоненты маршрута. Имя файла определяет сегменты маршрута. Например, модуль в каталоге app/pages/products.tsx определяет маршрут /products. Для более глубоких маршрутов могут быть созданы подпапки. Или в новых версиях - имена файлов, в которых пути разделены точками. Переменные могут быть определены в имени файла с помощью знака $. Например, файл, определенный в app/pages/products.$productId.tsx, включается во все маршруты products/*.

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

Интересной особенностью роутинга в Remix является вложенность. В Remix предполагается, что сайты обычно иерархичны. Например, все маршруты сайта имеют одинаковую панель навигации, настройки учетной записи имеют поднавигацию, или все страницы товаров имеют единый макет. Поэтому роуты, хранящиеся в разделе app/pages, определяют не все маршруты, а только их сегменты, как показано на рисунке к посту. Здесь показан сайт продукта, собранный из трех сегментов (корневой компонент, $product-Id.tsx и $variantId.tsx), и то, как вызываемый URL сопоставляется с компонентами маршрута.

Вложенность обеспечивается компонентом <Outlet/>, предоставляемым Remix. Он может быть включен в функцию рендеринга сегмента. При сборке сегментов в него включаются подсегменты.

При визуализации маршрута Remix начинает с корня в app/root.tsx и привязывает следующий сегмент маршрута к компоненту <Outlet/>. Затем следующий сегмент включается в предыдущий компонент. Так продолжается до тех пор, пока не будет назначен весь URL.

Теперь может возникнуть вопрос, как маршрутизация работает с индексными страницами. Если, например, нужно перечислить все категории в маршруте /categories, то очевидной мыслью будет поместить этот список в app/routes/categories.tsx. Однако это приведет к тому, что более специфические маршруты, такие как /categories/123, также будут отображать этот список. В Remix эта проблема решается с помощью индексных сегментов. В папке app/routes можно создать файл с именем categories._index.tsx. Тогда Remix будет отдавать предпочтение этому индексному сегменту, если он является последним элементом пути.

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

Как и в случае экспорта по умолчанию, функции являются компонентами React. При возникновении ошибки вместо реального содержимого отображается содержимое этих React-функций. Поскольку сегменты независимы друг от друга, всё что над ними остаётся интерактивным. Если errorBoundary не определён, то Remix поднимается по сегментам к корню до тех пор, пока не будет найдена функция errorBoundary.

Поток данных

Важной частью любого сайта являются данные. Веб-магазин, блог или новостной сайт без данных... ну, в общем, не стоит читать. В Remix реализован поток данных, интегрированный с маршрутизацией. Каждый сегмент может содержать loader и action. Эти функции, в отличие от errorBoundary, не являются JSX-компонентами, а возвращают Response. Loader и action можно сравнить с запросом и мутацией, которые известны из GraphQL.

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

Поток данных можно представить в виде треугольника. Углы представляют собой функции render, loader и action. Когда пользователь вызывает страницу, загрузчик сначала предоставляет необходимые данные. Эти данные передаются в функцию render. Затем эта функция может определить форму, в которой вызывается действие. Оно изменяет состояние на сервере, которое также изменяет состояние загрузчика, что, в свою очередь, приводит к изменению отрисованного содержимого.

Важно, что загрузчик и actions не включены в клиентский JS-бандл. Каждый вызов этих функций происходит на сервере Remix. Это также означает, что функция не имеет доступа к API браузера, таким как localStorage или sessionStorage. Если необходимо сохранить данные между браузером и сервером, рекомендуется использовать сессионный cookie. Remix предлагает реализацию cookie, в которой данные хранятся в виде base64-кодированной строки.

Loader должен возвращать объект response. Response является частью API Node.js и описывает HTTP-ответ. Response позволяет возвращать любое содержимое и задавать все заголовки. С помощью этих заголовков Remix также позволяет, например, осуществлять перенаправление или устанавливать сессионные cookies.

Remix предлагает несколько вспомогательных функций, абстрагирующих Response, например, функцию json. Данные, возвращаемые функцией-загрузчиком, могут быть использованы в функции рендеринга с помощью хука useLoaderData.

 

export const loader = () => {

  return json({status: "ok"})

}

 

Эта функция идентична функции ниже:

 

export const loader = () => {

  return new Response(JSON.stringify({status: "ok"}), headers: {"Content-Type": "application/json; charset=utf-8"})

}

 

Загрузчик можно использовать следующим образом:

 

export default function ExampleSegment() {

  const {status} = useLoaderData();

  return <div>{{status}}</div>

}

 

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

 

export const action = () => {

  return json({errors: ["user", "email"]})

}

export default function GettingUsers() {

  const {errors} = useActionData() ?? {errors: []};

  return <div>

    {errors.map( e => <p>{e}</p>)}

  </div>

}

 

Как уже упоминалось в начале статьи, в основе Remix лежат HTML-формы для потока данных. Соответственно, действия могут вызываться с помощью HTML-форм. Remix предоставляет собственный компонент формы, который предварительно рендерится как обычная HTML-форма и заменяется после гидратации. Таким образом, страница остается пригодной для использования, даже если она еще не гидрирована или в браузере отключен JavaScript.

Передаваемые значения задаются в элементах <input>. Если необходимо определить несколько действий в одном сегменте, то в Remix существует понятие "Intents". Здесь скрытое поле ввода или значение, заданное в кнопке используется для определения имени действия, которое также передается при отправке формы. На сервере поле ввода может быть прочитано и выполнена соответствующая логика. Сегмент входа в систему может выглядеть так, как показано в коде ниже. В нём определены две интенции - login и logout. Затем в действии выполняется соответствующая ветвь. Прелесть этой модели заключается, с одной стороны, в разделении логики и визуализации, а с другой - в возможности автоматической ревалидации.

 

import { ActionFunction, LoaderFunction, json } from "@remix-run/node";

import { Form, useActionData, useLoaderData } from "@remix-run/react";

import {getSession, commitSession, destroySession} from "../sessions"

import { userService } from "~/userService.server";

 

export const loader: LoaderFunction = async({request}) => {

  const session = await getSession(request.headers.get("Cookie"))

  if(session.has("user")) {

    return json({user: session.get("user")})

  }

  return json({status: "OK"})

}

 

export const action: ActionFunction = async({request}) => {

  const form = await request.formData();

  const intent = form.get("intent")?.toString()

  if(intent === "login") {

    const username = form.get("username")

    const password = form.get("password")

    if(!username || !password) {

      return json({error: "Username and password is required"})

    }

    const isAuthenticated = userService.verifyAuth(username.toString(), password.toString())

    if(!isAuthenticated) {

      return json({error: "Username and password is wrong"})

    }

  

    const session = await getSession(request.headers.get("Cookie"))

    session.set("user", username)

    return json({status: "ok"}, {headers: {

      "Set-Cookie": await commitSession(session)

    }})

  } else if(intent === "logout") {

    return json({status: "ok"}, {headers: {"Set-Cookie": await destroySession(await getSession(request.headers.get("Cookie")))

    }})

  } else {

    return json({error: "Operation unknown"})

  }

}

export default function Index() {

  const {user} = useLoaderData<{user: string}>()

  const {error} = useActionData<{error: string}>() ?? {}

  return <div>

    { !!error && <p><b>{error}</b></p> }

    { user ? <p>Logged in as {user}</p> : <p>Not logged in.</p> }

    { user ? <LogoutForm/> : <LoginForm/>}

  </div>

}

function LoginForm() {

  return <Form method="post">

    <input type="username" required name="username" placeholder="Enter username"/>

    <input type="password" required name="password" placeholder="Enter Password" minLength={4}/>

    <button type="submit" name="intent" value="login">Login</button>

  </Form>

}

 

function LogoutForm() {

  return <Form method="post">

    <button type="submit" name="intent" value="logout">Logout</button>

  </Form>

}

 

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

Поток данных и гидратация

В фоновом режиме Remix создает для каждого action и loader конечную точку, через которую происходит вызов JSON-данных при гидрации приложения. Это означает, что для новых данных передаётся не весь HTML-документ, а только соответствующие данные в формате JSON. Если приложение еще не гидрированно, то загрузчик вызывается изнутри, и HTML-документ возвращается с измененным состоянием. Для action Remix ожидает POST-запросы, содержащих данные формы, и отвечает модифицированным HTML-документом.

Progressive Enhancement

Remix, используя поток данных, заставляет разрабатывать сайт таким образом, чтобы он хорошо работал без JavaScript на стороне клиента, но мог быть улучшен с помощью JavaScript. Помимо потока данных, эту концепцию можно встретить, например, в маршрутизации. Здесь после гидрирования вместо целого HTML-документа запрашиваются только новые данные в формате JSON, которые уже предварительно загружены перед вызовом страницы благодаря событию hover.

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

Remix и Next.js

Для того чтобы сравнить Remix и Next.js, необходимо уточнить, какая версия или какая стратегия Next.js имеется в виду. Это связано с тем, что, начиная с версии 13, Next.js предлагает новую систему маршрутизации, которая во многом напоминает роутинг Remix. До версии 13 Next.js предлагал только "классическую" маршрутизацию. В Next.js она называлась "маршрутизатором страниц". Страницы могли храниться в папке pages, каждая из которых определяла всю страницу. Next.js предлагает аналогичную систему маршрутизации страниц, как и в Remix. Вместо загрузчика в Next.js есть функции getServerSideProps и getStaticProps. Обе функции определяют данные, которые могут быть прочитаны и использованы в рендеринге через параметр функции.

Для маршрутизации страниц Next.js не предусмотрено никаких actions, как в Remix. Вы можете определить API-маршруты, которые могут быть вызваны в функции рендеринга, например, с помощью fetch. Таким образом, поток данных менее интегрирован фреймворком. Разработчику приходится заботиться о ревалидации, для которой необходима отдельная конечная точка API, поскольку, хотя Next.js автоматически создает конечную точку API для getServerSideProps, URL маршрута меняется со случайной выборкой в каждом процессе сборки.

Начиная с версии 13, появилась новая маршрутизация приложений. Вместо папки pages используется папка app. Маршрутизация основана на вложенности и, как и в Remix, позволяет использовать такие преимущества, как распараллеливание, "близость данных", меньшее дублирование кода и т.д. Новая маршрутизация приложений также привносит свою концепцию данных с actions и ревалидацией, но имеет другой синтаксис и подход.

Заключение

Remix - это новый фреймворк React, специализирующийся на рендеринге сайтов на стороне сервера. Он пытается как можно ближе подойти к API HTML5, создавая концепцию данных поверх API форм; эта стратегия настолько стара, что кажется новой. Несмотря на инновационные концепции, которые предлагает Remix, он вряд ли сможет превалировать над Next.js в качестве фреймворка для сайтов с серверным рендерингом. Next.js просто слишком распространен и устоялся для этого. Для Next.js уже существует большая и стабильная экосистема инструментов, ресурсов, библиотек и разработчиков. С этим всем Remix не может конкурировать.

Так неужели Remix потерпел неудачу? Нет, совсем наоборот! Даже если он и не станет основным фреймворком, Remix смог проиллюстрировать преимущества таких парадигм, как вложенная маршрутизация или Progressive Enhancement. Таким образом, очень вероятно, что он косвенно способствовал созданию более совершенной концепции маршрутизации для Next.js и, возможно, других фреймворков.

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

Популярное

Самые популярные посты

Как быть максимально продуктивным на удалённой работе?
Business

Как быть максимально продуктивным на удалённой работе?

Я запустил собственный бизнес и намеренно сделал всё возможное, чтобы работать из любой точки мира. Иногда я сижу с своём кабинете с большим 27-дюймовым монитором в своей квартире в г. Чебоксары. Иногда я нахожусь в офисе или в каком-нибудь кафе в другом городе.

Привет! Меня зовут Сергей Емельянов и я трудоголик
Business PHP

Привет! Меня зовут Сергей Емельянов и я трудоголик

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

Акция! Профессиональный разработчик CRM за 2000 руб. в час

Выделю время под ваш проект. Знания технологий Vtiger CRM, SuiteCRM, Laravel, Vue.js, Golang, React.js. Предлагаю варианты сотрудничества, которые помогут вам воспользоваться преимуществами внешнего опыта, оптимизировать затраты и снизить риски. Полная прозрачность всех этапов работы и учёт временных затрат. Оплачивайте только рабочие часы разработки после приемки задачи. Экономьте на платежах по его содержанию разработчика в штате. Возможно заключение договора по ИП. С чего начать, чтобы нанять профессионального разработчика на full-time? Просто заполните форму!

Telegram
@sergeyem
Telephone
+4915211100235