Маршрутизация
Маршрутизация в 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]
.
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-ответ в виде веб-страницы.
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-метода.
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.
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
будут выполняться в следующем порядке:
onRequest
в файлеsrc/routes/layout.tsx
;onRequest
в файлеsrc/routes/admin/layout.tsx
;onRequest
в файлеsrc/routes/admin/index.tsx
;- Компонент файла
src/routes/admin/index.tsx
.
Обработчик onRequest
в файле src/routes/index.tsx
не выполняется.
Вложенные макеты
Макеты также обеспечивают возможность добавления общего пользовательского интерфейса на отображаемую страницу. Например, если мы хотим добавить общий заголовок ко всем маршрутам, мы можем добавить компонент Header
в корневой макет.
Для данного примера компоненты Qwik будут рендериться в следующем порядке:
- Компонент файла
src/routes/layout.tsx
; - Компонент файла
src/routes/admin/layout.tsx
; - Компонент файла
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>
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
до тех пор, пока страница не будет полностью перерендерена.
<Link prefetch>
Свойство 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 также поддерживает:
Они рассматриваются дальше.