Маршрутизация

Маршрутизация в Qwik City основана на файловой системе, как в Next.js, SvelteKit, SolidStart или Remix. Файлы и каталоги в src/routes играют определенную роль в маршрутизации вашего приложения.

  • 📂 Директории: Определяют сегменты URL, которые будут соответствовать маршрутизатору.
  • 📄 index.tsx/mdx файлы: Определяют страницу.
  • 📄 index.ts файлы: Определяют конечную точку.
  • 🖼️ layout.tsx файлы: Определяют вложенные макеты и/или промежуточное ПО.

Маршрутизация на основе каталогов

Для сопоставления входящих запросов к страницам/конечным точкам/промежуточному ПО используются только имена каталогов.

Например, если у вас есть файл по пути src/routes/some/path/index.tsx, он будет сопоставлен с URL-адресом https://example.com/some/path.

Структура каталога
src/
└── routes/
    ├── contact/
       └── index.mdx         # https://example.com/contact
    ├── about/
       └── index.md          # https://example.com/about
    ├── docs/
       └── [id]/
           └── index.ts      # https://example.com/docs/1234
                             # https://example.com/docs/anything
    ├── [...catchall]/
       └── index.tsx         # https://example.com/anything/else/that/didnt/match
    
    └── layout.tsx            # This layout is used for all pages
  • [id] — это каталог, представляющий сегмент динамического маршрута, в этом примере id — это параметр с типом строки, доступный с помощью useLocation().params.id;
  • [...catchall] — это каталог, представляющий динамический универсальный маршрут, в этом примере catchall — параметр с типом строки, доступный с помощью useLocation().params.catchall;
  • Файлы index.tsx|mdx являются страницами/конечными точками/промежуточным ПО.
  • Файлы layout.tsx являются макетами.

Сегменты динамического маршрута

Специальные именованные каталоги с квадратными скобками, такие как [paramName] и [...catchAll], могут использоваться для сопоставления сегментов маршрута, которые являются динамическими:

Структура каталога
src/routes/blog/index.tsx  /blog
src/routes/user/[username]/index.tsx  /user/:username (/user/foo)
src/routes/post/[...all]/index.tsx  /post/* (/post/2020/id/title)
Структура каталога
src/
└── routes/
    ├── blog/
       └── index.tsx         # https://example.com/blog
    ├── post/
       └── [...all]/
           └── index.tsx     # https://example.com/post/2020/id/title
    └── user/
        └── [username]/
            └── index.tsx     # https://example.com/user/foo

Папка [username] может быть любым из тысячи пользователей, которые есть в вашей базе данных. Было бы нецелесообразно создавать маршрут для каждого пользователя. Вместо этого нам нужно определить параметр маршрута (часть URL-адреса), который будет использоваться для извлечения [username].

src/routes/user/[username]/index.tsx
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
 
export default component$(() => {
  const loc = useLocation();
  return <div>Привет, {loc.params.username}!</div>;
});

index. -файлы

Внутри каталога src/routes все файлы с именем index считаются страницами/конечными точками/промежуточным ПО. Qwik поддерживает следующие расширения: .ts, .tsx, .md и .mdx.

Страницы/конечные точки/промежуточное ПО — это конечные узлы дерева маршрутизации, т. е. модули, которые будут обрабатывать запрос и возвращать ответ HTTP.

Страница index.tsx

Когда index.tsx или index.ts экспортирует компонент Qwik в качестве экспорта по умолчанию, Qwik City рендерит компонент и возвращает HTML-ответ в виде веб-страницы.

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return <h1>Привет, мир</h1>;
});

Конечная точка index.ts

index.ts может напрямую обращаться к HTTP-запросу и возвращать необработанный HTTP-ответ без участия какого-либо компонента Qwik. Это делается путём экспорта метода onRequest или onGet, onPost, onPut, onDelete в зависимости от того, хотите ли вы обработать только конкретный запрос с учётом его HTTP-метода.

src/routes/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = ({ json }) => {
  json(200, { message: 'Привет, мир' });
};

Обратите внимание, что в последнем примере нет экспорта по умолчанию. Это связано с тем, что мы не рендерим компонент Qwik, а обрабатываем запрос напрямую и возвращаем ответ в формате JSON. Это полезно для реализации RESTful API или любого другого типа конечной точки HTTP.

Страница + Конечная точка

Как видите, в Qwik City нет чёткого разделения между страницами и конечными точками, в обоих случаях это файл index.tsx, который экспортирует компонент Qwik или метод onRequest. Однако можно комбинировать оба подхода. Например, вы можете экспортировать метод onRequest, который будет обрабатывать запрос, а затем рендерить компонент Qwik.

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = ({ headers, query, json }) => {
  headers.set('Cache-Control', 'private');
  if (query.get('format') === 'json') {
    json(200, { message: 'Привет, мир' });
  }
};
 
export default component$(() => {
  return <h1>Привет, мир</h1>;
});

В этом примере обработчик запроса всегда будет устанавливать для заголовка Cache-Control значение private, а страница будет рендериться как HTML-страница, но если запрос содержит параметр format=json, конечная точка будет вместо страницы возвращать ответ в формате JSON.

Файлы layout.tsx

Модули макета очень похожи на файлы index, оба могут обрабатывать запросы и рендерить компоненты Qwik, однако макеты предназначены для работы в качестве слоя промежуточного программного обеспечения, позволяя совместно использовать пользовательский интерфейс и обработку запросов (промежуточное ПО) для набора маршрутов.

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

  • Общая обработка запросов: Куки запроса должны быть проверены до рендера страницы, в противном случае рендерится пустая страница 401;
  • Общий пользовательский интерфейс: Все страницы имеют общий заголовок, показывающий имя пользователя и изображение профиля.

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

Возьмите этот каталог src/routes в качестве примера:

Структура каталога
src/
└── routes/
    ├── admin/
       ├── layout.tsx  <-- Этот макет используется для всех страниц в /admin/*
       └── index.tsx
    ├── layout.tsx      <-- Этот макет используется для всех страниц
    └── index.tsx

Макеты промежуточного ПО

Поскольку макеты могут реализовывать обработку запросов с помощью onRequest или onGet, onPost, onPut, onDelete, их можно использовать для реализации промежуточного ПО, например, для проверки куки-файлов в запросе перед рендером страницы.

Для маршрута https://example.com/admin методы onRequest будут выполняться в следующем порядке:

  1. onRequest в файле src/routes/layout.tsx;
  2. onRequest в файле src/routes/admin/layout.tsx;
  3. onRequest в файле src/routes/admin/index.tsx;
  4. Компонент файла src/routes/admin/index.tsx.

Обработчик onRequest в файле src/routes/index.tsx не выполняется.

Вложенные макеты

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

Для данного примера компоненты Qwik будут рендериться в следующем порядке:

  1. Компонент файла src/routes/layout.tsx;
  2. Компонент файла src/routes/admin/layout.tsx;
  3. Компонент файла src/routes/admin/index.tsx.
<RootLayout>
  <AdminLayout>
    <AdminPage />
  </AdminLayout>
</RootLayout>

SPA-навигация

С Qwik исчезает различие между MPA и SPA; каждое приложение может быть одновременно и тем, и другим. Этот выбор больше не является архитектурным решением, определяемым в начале проекта, теперь такое решение может быть принято для каждого маршрута.

Qwik предоставляет компонент <Link> и хук useNavigate(). Их можно использовать для инициирования обновления SPA или навигации между страницами.

Link является рекомендуемым способом навигации, поскольку он использует тег HTML <a>, который является наиболее доступным способом перехода между страницами. Однако, если вам необходимо осуществлять навигацию программно, вы можете использовать хук useNavigate().

import { component$ } from '@builder.io/qwik';
import { Link, useNavigate } from '@builder.io/qwik-city';
 
export default component$(() => {
  const nav = useNavigate();
  return (
    <div>
      <Link href="/about">Описание (предпочтительно)</Link>
      <button onClick$={() => nav('/about')}>Описание</button>
    </div>
  );
});

Компонент Link использует хук useNavigate() под капотом.

Link с параметром reload может быть использован для обновления текущей страницы. Вы также можете вызвать функцию nav() из хука useNavigate() без аргументов.

import { component$ } from '@builder.io/qwik';
import { Link, routeLoader$, useNavigate } from '@builder.io/qwik-city';
 
export const useServerTime = routeLoader$(() => {
  // Это будет выполняться на сервере каждый раз при обновлении страницы.
  return Date.now();
});
 
export default component$(() => {
  const nav = useNavigate();
  const serverTime = useServerTime();
 
  return (
    <div>
      <Link reload>Обновить (лучшая доступность)</Link>
      <button onClick$={() => nav()}>Обновить</button>
      <p>Серверное время: {serverTime.value}</p>
    </div>
  );
});

Когда страница обновится, все соответствующие routeLoader$ и серверные обработчики (onRequest) будут повторно выполнены на сервере, и UI будет перерисован соответствующим образом.

При обновлении страницы свойство isNavigating из useLocation() будет в значении true до тех пор, пока страница не будет полностью перерендерена.

Свойство prefetch компонента Link может быть использовано для повышения воспринимаемой производительности приложения. Хотя страницы Qwik отлично справляются с ленивой загрузкой javascript, эта функция может пригодиться для страниц с большим объёмом контента или SSR-страниц, которым необходимо ожидать вызова базы данных или API.

<Link prefetch href="/about">About</Link>

Просто добавив параметр prefetch, ваш Link-компонент начнёт предварительную выборку страницы, как только пользователь наведёт курсор на ссылку. Если приложение выполняет предварительную выборку, когда пользователь щёлкает на ссылке, то следующая страница появляется мгновенно.

Восстановление прокрутки

Qwik обеспечивает лучшее в своём классе восстановление прокрутки для SPA, которое в точности имитирует работу в родном браузере. Ваши пользователи должны получать точно такой же опыт, который они привыкли ожидать от MPA, но со всеми дополнительными преимуществами SPA.

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

Если пользователь затем нажмёт на обычный тег <a>, он выполнит обычную навигацию. Эта новая страница не будет иметь контекста SPA, и фактически понижается до уровня MPA. Вы можете менять их местами по своему усмотрению, и опыт пользователя будет плавно переключаться между MPA и SPA, как будто это одно и то же.

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

Сценарий, необходимый для обеспечения этого опыта, никогда не загружается и даже не отправляется в браузер пользователя, если только запись истории не имела контекста SPA. Чистые страницы MPA никогда не загружают этот скрипт. В этом и заключается магия Qwik.

Восстановление прокрутки в Qwik всегда происходит синхронно с рендером. В сочетании с возобновляемостью Qwik и природой SSR/MPA, пользователь никогда не должен испытывать мигание прокрутки.

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

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

Примечания по использованию pushState() и replaceState() во время SPA:.

На странице с контекстом SPA Qwik исправляет функции pushState() и replaceState() на глобальный history. Это необходимо для того, чтобы все пользовательские состояния, которые вы добавляете как разработчик, также получали контекст SPA.

Пока они исправлены, состояния, которые вы push или replace, всегда должны быть фактическим типом Object. Это связано с тем, что Qwik должен иметь возможность автоматически добавлять контекст SPA к состоянию в качестве свойства.

Если вы предоставите значение, которое не является объектом, Qwik создаст новый объект для состояния и добавит предоставленное вами значение. значение для нового ключа: { _data: <ваше_ значение> }

Qwik также предупредит вас в консоли браузера в режиме dev, когда это произойдет.

Событие запроса

Каждый обработчик запросов, таких как onRequest, onGet, onPost и т.д., передаёт обработчику в качестве первого аргумента объект RequestEvent. Объект RequestEvent содержит служебные функции и свойства для получения и установки значений запроса и ответа сервера. Этот объект содержит следующие свойства:

  • basePathname: Базовый путь запроса, который может быть настроен во время сборки. По умолчанию используется /.
  • cacheControl: Удобная функция для установки в ответе заголовка Cache-Control.
  • cookie: cookies HTTP-запроса и ответа. Используйте метод get() для получения из запроса значения куки. Используйте метод set() для установки значения куки ответа.
  • env: Переменные окружения, предоставляемые платформой.
  • error: При вызове метода ответ немедленно завершается с заданным кодом состояния. Это может быть полезно для завершения ответа кодом 404 и использования 404-хэндлера в каталоге routes. Смотрите Коды состояния, чтобы знать, какой код состояния следует использовать.
  • getWritableStream: Низкоуровневый доступ для записи в поток ответов HTTP. После вызова getWritableStream() состояние и заголовки больше не могут быть изменены и будут отправлены по сети.
  • headers: HTTP заголовки ответов.
  • html: Удобный метод для отправки ответа в виде тела HTML. В ответе будет автоматически установлен заголовок Content-Type в значение text/html; charset=utf-8. Метод html() в ответе может быть вызван только один раз.
  • json: Удобный метод для конвертации данных в JSON и отправки их в ответ. В ответе будет автоматически установлен заголовок Content-Type со значением application/json; charset=utf-8. Метод json() в ответе может быть вызван только один раз.
  • locale: В какой локали находится содержимое. Значение локали может быть получено из выбранных методов с помощью getLocale().
  • method: Значение метода HTTP-запроса.
  • next: Вызывает следующий обработчик запроса. Используется в ПО промежуточного слоя.
  • params: Параметры пути URL, которые были извлечены из сегментов текущего пути. Если вам нужно получить поисковые параметры строки запроса, то используйте query.
  • parseBody: Этот метод проверяет заголовки запроса на наличие заголовка Content-Type и соответствующим образом разбирает тело запроса. Он поддерживает типы содержимого application/json, application/x-www-form-urlencoded и multipart/form-data. Если заголовок Content-Type не установлен, возвращается null.
  • pathname: Значение пути из URL. Не включает протокол, домен, строку запроса (параметры поиска) или хэш.
  • platform: Данные и функции, специфичные для конкретной платформы.
  • query: Значение строки запроса из URL - URLSearchParams. Используйте params, если вам надо получить параметры маршрута.
  • redirect: URL для перенаправления. При вызове метода ответ будет немедленно завершён с корректным статусом перенаправления и заголовками. Смотрите раздел Перенаправления, чтобы узнать, какой код статуса следует использовать.
  • request: HTTP Запрос.
  • send: Отправляет тело ответа. При использовании send() заголовок ответа Content-Type автоматически не устанавливается и должен быть установлен вручную. Вызов send() может быть сделан только один раз.
  • sharedMap: Общая карта для всех обработчиков запросов. Каждый HTTP-запрос будет получать новый экземпляр общей карты. Общая карта используется для обмена данными между обработчиками запросов.
  • status: HTTP-ответ код состояния. Устанавливает код состояния при вызове с аргументом. Всегда возвращает код состояния, поэтому вызов status() без аргумента может быть использован для возврата текущего кода состояния.
  • text: Удобный метод для отправки текстового тела ответа. В ответе будет автоматически установлен заголовок Content-Type на text/plain; charset=utf-8. Метод text() может быть вызван только один раз.
  • url: URL HTTP-запроса.

Перезапись маршрутов

Вы можете переписать ваши маршруты, чтобы повторно использовать один компонент страницы с его собственными элементами промежуточного ПО и макетами для нескольких страниц. Это может быть полезно для целей SEO или для перевода ваших страниц на разные языки.

Перевод локализованных адресов с префиксом:

В целях локализации вы можете перевести свои маршруты с /products на /it/prodotti или на /fr/produits и /products/product-name на /it/prodotti/nome-prodotto или /fr/produits/nom-du-produit без использования нескольких файлов маршрутов для каждой локали, но с повторным использованием одного и того же компонента страницы, макетов, промежуточного ПО и т.д.

Имя параметра не будет изменено, поэтому, если файл маршрута — /products/[slug]/index.tsx, а URL-адрес – /products/product-name, /it/prodotti/nome-prodotto или /fr/produits/nom-du-produit вы получите тот же параметр пути slug со значениями product-name, nome-prodotto или nom-du-produit.

Перезапись адресов без префикса:

Это редкость, но вы можете захотеть иметь псевдонимы для одного и того же пути. Например, вы можете захотеть, чтобы и /docs, и /documents рендерились одним и тем же компонентом страницы или вы можете захотеть перевести /products в /prodotti без добавления префикса /it.

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

Вы можете установить правила перезаписи в вашем vite.config.ts следующим образом:

import { defineConfig } from 'vite';
import { qwikCity } from '@builder.io/qwik-city/vite';
 
export default defineConfig(async () => {
  return {
    plugins: [
      qwikCity({
        rewriteRoutes: [
            {
              paths: {
                  'docs': 'documentation'
              },
            },
            {
              prefix: 'it',
              paths: {
                'docs': 'documentazione',
                'getting-started': 'per-iniziare',
                'products': 'prodotti',
              },
            },
          ],
      }),
    ],
  };
});

Расширенная маршрутизация

Qwik City также поддерживает:

Они рассматриваются дальше.

Участники

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

  • manucorporat
  • nnelgxorz
  • the-r3aper7
  • Oyemade
  • mhevery
  • adamdbradley
  • wtlin1228
  • AnthonyPAlicea
  • hamatoyogi
  • jakovljevic-mladen
  • claudioshiver
  • maiieul
  • igorbabko
  • jordanw66
  • mrhoodz
  • chsanch