Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
![](https://habrastorage.org/r/w1560/webt/kg/nf/k1/kgnfk1erl4lfx35dipzjer7psn8.png)
Привет, друзья!
В этой серии статей я расскажу вам о Remix — новом фреймворке для создания клиент-серверных веб-приложений на JavaScript (точнее, на React) со встроенной поддержкой TypeScript.
Remix позволяет разрабатывать так называемые PESPA (Progressive Enhancement Single Page Apps — одностраничные приложения с возможностью прогрессивного улучшения). Это означает следующее:
- почти весь код приложения "живет" на сервере;
- приложение остается функциональным даже при отсутствии JS;
- JS используется только для прогрессивного улучшения UX (User Experience — пользовательский опыт).
Подробнее о PESPA и других архитектурах веб-приложений можно почитать здесь.
Очевидно, что разработчики Remix вдохновлялись Next.js и Svelte.
К слову, здесь вы найдете полное руководство по Next.js.
В первой части мы пройдемся по руководствам из официальной документации, во второй — более подробно рассмотрим возможности, предоставляемые Remix, в третьей — разработаем что-нибудь интересное.
Это часть номер раз.
Содержание
- Маршрутизация / Routing
- Ресурсные роуты / Resource Routes
- Интерфейс роутов / API Routes
- Бэкенд для фронтенда / Backend for Frontend
- Загрузка данных / Data Loading
- Запись данных / Data Writes
- Оптимистичное обновление UI / Optimistic UI
- Ограничения модулей / Module Constraints
- Обработка ошибок / Error Handling
- Обработка ошибки 404 / Not Found Handling
- Переменные среды окружения / Environment Variables
- Стилизация / Styling
- TypeScript
Маршрутизация / Routing
Маршрутизация (роутинг) — это, пожалуй, самая важная концепция в Remix. Все начинается с маршрутов (роутов): компилятор, первоначальный запрос документа и почти все последующие действия пользователя.
Начнем с определения понятий:
- вложенные роуты (nested routes) — связь (map) роутов с сегментами URL обеспечивает соответствие URL определенным компонентам и данным, известным перед рендерингом страницы;
- URL — полный путь в поисковой строке браузера пользователя. Один URL может совпадать (соответствовать — match) с несколькими роутами. Обратите внимание: роут и URL — разные вещи в Remix;
- роут (route) или модуль роута (route module) — модуль JS с определенными экспортами (
loader
,action
,default function
(компонент) и др.), который соответствует одному или нескольким сегментам URL. Поскольку роут соответствует сегменту URL, по одному пути могут рендерится несколько модулей. Иерархия компонентов соответствует сегментам URL (в основном); - путь (path) или путь роута (route path) — сегмент URL, которому соответствует отдельный модуль. Путь роута определяется названием файла в директории
app/routes
; - родительский макет (parent layout route) или родительский роут (parent route) — модуль, который рендерит макет для дочерних компонентов через компонент
Outlet
; - макет без пути (pathless layout route) или роут без пути (pathless route) — модуль, который не добавляет сегменты к URL, но добавляет компонент в иерархию UI (User Interface — пользовательский интерфейс) при совпадении его дочерних роутов;
- дочерний роут (child route) — модуль, который рендерится внутри родительского
Outlet
при совпадении его пути с URL; - индексный роут (index route) — модуль, который имеет такой же путь, как его родительский роут, но рендерится в качестве дефолтного дочернего роута внутри
Outlet
; - динамический сегмент (dynamic segment) — сегмент пути роута, извлекаемый из URL и передаваемый в приложение, такой как идентификатор (ID) записи или слаг (slug) поста;
- сплат (splat) — замыкающая звездочка (trailing wildcard) в пути роута, который благодаря этому совпадает со всеми сегментами URL (включая последующий
/
); - аутлет (outlet) — компонент, который рендерится внутри родительского модуля, предназначенный для рендеринга дочерних модулей. Другими словами, аутлет определяет локацию дочерних роутов.
Вложенный роутинг
Вложенный роутинг — это связь между сегментами URL и иерархией компонентов в UI. Сегменты URL определяют:
- макеты, формирующие страницу;
- загрузку JS-кода, используемого на странице;
- загрузку данных, используемых на странице.
Определение роутов
Роуты определяются посредством создания файлов в директории app/routes
. Вот как может выглядеть иерархия роутов приложения:
app
├── root.jsx
└── routes
├── accounts.jsx
├── dashboard.jsx
├── expenses.jsx
├── index.jsx
├── reports.jsx
├── sales
│ ├── customers.jsx
│ ├── deposits.jsx
│ ├── index.jsx
│ ├── invoices
│ │ ├── $invoiceId.jsx
│ │ └── index.jsx
│ ├── invoices.jsx
│ └── subscriptions.jsx
└── sales.jsx
root.jsx
— это корневой роут, служащий макетом для всего приложения. Другие роуты рендерятся внутри егоOutlet
;- обратите внимание на файлы, названия которых совпадают с названиями директорий, в которых эти файлы находятся. Эти файлы предназначены для формирования иерархии макетов компонентов. Например,
sales.jsx
— это родительский роут для всех дочерних роутов внутри директорииapp/routes/sales
. При совпадении с URL любого роута из этой директории, он будет рендерится внутриOutlet
модуляsales.jsx
; - роут
index.jsx
будет рендерится внутриOutlet
при совпадении URL с путем директории (например, путиexample.com/sales
соответствует роутapp/routes/sales/index.jsx
).
Рендеринг иерархии макета роута
Предположим, что URL имеет вид /sales/invoices/123
. С этим URL будут совпадать следующие роуты:
root.jsx
;routes/sales.jsx
;routes/sales/invoices.jsx
;routes/sales/invoices/$invoiceId.jsx
.
При посещении этой страницы пользователем Remix отрендерит такую иерархию компонентов:
<Root>
<Sales>
<Invoices>
<InvoiceId />
</Invoices>
</Sales>
</Root>
Иерархия компонентов полностью соответствует иерархии файлов в директории app/routes
:
app
├── root.jsx
└── routes
├── sales
│ ├── invoices
│ │ └── $invoiceId.jsx
│ └── invoices.jsx
├── sales.jsx
└── accounts.jsx
Иерархия компонентов для URL /accounts
будет такой:
<Root>
<Accounts />
</Root>
Для рендеринга дочерних роутов внутри родительского используется Outlet
. root.jsx
рендерит основной макет, боковую панель и аутлет для дочерних роутов:
import { Outlet } from "@remix-run/react";
export default function Root() {
return (
<Document>
<Sidebar />
{/* ! */}
<Outlet />
</Document>
);
}
В свою очередь, sales.jsx
рендерит аутлет для всех его дочерних роутов (app/routes/sales/*
):
import { Outlet } from "@remix-run/react";
export default function Sales() {
return (
<div>
<h1>Sales</h1>
<SalesNav />
{/* ! */}
<Outlet />
</div>
);
}
Индексные роуты
Индексный роут — это дефолтный дочерний роут. При отсутствии других дочерних роутов рендерится индексный модуль. Например, URL exmaple.com/sales
соответствует индексный роут app/routes/sales/index.jsx
.
Индексные роуты не должны рендерить дочерние модули, они являются тупиком для URL. Например, вместо рендеринга глобальной панели навигации в app/routes/index.jsx
, ее следует рендерить в app/root.jsx
.
Параметр строки запроса ?index
Данный параметр позволяет отличать индексные роуты от их родительский модулей, которые рендерят макеты. Предположим, что у нас имеется такая иерархия роутов:
└── app
├── root.jsx
└── routes
├── sales
│ ├── invoices
│ │ └── index.jsx
│ └── invoices.jsx
Какому роуту будет соответствовать путь /sales/invoices
? Роуту /sales/invoices.jsx
или роуту /sales/invoices/index.jsx
? Ответ: роуту /sales/invoices.jsx
. Для совпадения с роутом /sales/invoices/index.jsx
путь должен заканчиваться параметром строки запроса ?index
:
└── app
├── root.jsx
└── routes
├── sales
│ ├── invoices
│ │ └── index.jsx <-- /sales/invoices?index
│ └── invoices.jsx <-- /sales/invoices
В некоторых случаях добавление ?index
происходит автоматически (например, при отправке формы в индексном или его родительском роуте), в других — это делается вручную (например, при использовании fetcher.submit()
или fetcher.load()
).
Вложенные URL без вложенных макетов
Иногда может потребоваться добавить вложенный URL без добавления компонента в иерархию UI. Рассмотрим страницу редактирования счета:
- мы хотим, чтобы URL имел вид
/sales/invoices/$invoiceId/edit
; - мы хотим, чтобы соответствующий компонент был прямым потомком корневого компонента.
Другими словами, мы не хотим этого:
<Root>
<Sales>
<Invoices>
<InvoiceId>
<EditInvoice />
</InvoiceId>
</Invoices>
</Sales>
</Root>
Мы хотим это:
<Root>
<EditInvoice />
</Root>
Для создания плоской иерархии UI используется плоское название файла — использование .
в названии файла позволяет добавлять сегменты URL без добавления компонентов в иерархию UI:
└── app
├── root.jsx
└── routes
├── sales
│ ├── invoices
│ │ └── $invoiceId.jsx
│ └── invoices.jsx
├── sales.invoices.$invoiceId.edit.jsx