Remix
Remix - фреймворк для создания клиент-серверных веб-приложений на JavaScript (React) со встроенной поддержкой TypeScript.
Remix позволяет разрабатывать так называемые PESPA (Progressive Enhancement Single Page Apps - одностраничные приложения с возможностью прогрессивного улучшения). Это означает следующее:
- почти весь код приложения "живет" на сервере;
- приложение остается функциональным даже при отсутствии JS;
- JS используется только для прогрессивного улучшения UX (User Experience - пользовательский опыт).
Подробнее о PESPA и других архитектурах веб-приложений можно почитать здесь.
Маршрутизация / 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 👈 не является вложенным
└── sales.jsx
Пути example.com/sales/invoices/123/edit
будет соответствовать такая иерархия компонентов:
<Root>
<EditInvoice />
</Root>
А пути example.com/sales/invoices/123
(без /edit
) такая:
<Root>
<Sales>
<Invoices>
<InvoiceId />
</Invoices>
</Sales>
</Root>
Макеты без пути
Иногда, наоборот, может потребоваться добавить компонент в иерархию UI без добавления сегментов в URL. Для этого используются макеты без пути.
Предположим, что мы хотим получить такую иерархию UI:
<Root>
<Auth>
<Login />
</Auth>
</Root>
В данном случае иерархия роутов может выглядеть так:
app
├── root.jsx
└── routes
├── auth
│ ├── login.jsx
│ ├── logout.jsx
│ └── signup.jsx
└── auth.jsx
У нас имеется правильная иерархия UI, но, возможно, мы не хотим, чтобы каждый URL содержал префикс /auth
, например, вместо /auth/login
мы хотим видеть просто /login
.
Для удаления вложенности URL при сохранении вложенности UI достаточно добавить к названиям роута и дир ектории 2 нижних подчеркивания:
app
├── root.jsx
└── routes
├── __auth
│ ├── login.jsx
│ ├── logout.jsx
│ └── signup.jsx
└── __auth.jsx
Динамические сегменты
Если название файла содержит префикс $
, соответствующая часть пути роута становится динамическим сегментом. Это означает, что любое значение в URL для данного сегмента будет извлекаться из URL и передаваться в приложение.
Рассмотрим роут $invoiceId.jsx
. При переходе по адресу /sales/invoices/123
строка 123
будет извлечена из URL и передана в loader
, action
и компонент в виде одноименного свойства объекта params
:
import { useParams } from "@remix-run/react";
export async function loader({ params }) {
const id = params.invoiceId;
}
export async function action({ params }) {
const id = params.invoiceId;
}
export default function Invoice() {
const params = useParams();
const id = params.invoiceId;
}
Роуты могут содержать несколько параметров. Параметры могут быть директориями.
app
├── root.jsx
└── routes
├── projects
│ ├── $projectId
│ │ └── $taskId.jsx
│ └── $projectId.jsx
└── projects.jsx
В данном случае при переходе по адресу /projects/123/abc
параметры буду следующими:
params.projectId; // "123"
params.taskId; // "abc"
Сплаты
Если название файла содержит только $
($.jsx
), то такой роут является сплатом. С таким роутом совп адает любое значение URL для оставшейся части URL до конца. В отличие от динамических сегментов, сплат не останавливается при достижении следующего /
.
Рассмотрим такую иерархию роутов:
app
├── root.jsx
└── routes
├── files
│ ├── $.jsx
│ ├── mine.jsx
│ └── recent.jsx
└── files.jsx
При переходе по адресу example.com/files/images/work/flyer.jpg
, сегменты URL, начиная с files
будут перехвачены сплатом и доступны в приложении через params["*"]
:
export async function loader({ params }) {
params["*"]; // "images/work/flyer.jpg"
}
Сплаты могут использоваться для реализации кастомных страниц 404 с данными из loader
(при отсутствии кастомной страницы 404 рендерится корневой CatchBoundary
, не позволяющий загружать данные для страницы при отсутствии совпадения с роутами).
Ресурсные роуты / Resource Routes
Ресурсные роуты не являются частью UI, но являются частью приложения. Они могут возвращать любые ответы.