Начало работы с Qwik
Qwik - это новый вид фреймворка, который является возобновляемым (без JS и гидратации), современным и знакомым для разработчиков на React.
Чтобы попробовать его прямо сейчас, попробуйте наши песочницы в браузере:
- Stackblitz Qwik + Vite
- Песочница с примерами (только Qwik, без маршрутизации)
Подготовка
Чтобы начать работу с Qwik на своём компьютере, вам понадобится следующее:
- Node.js v16.8 или выше
- Ваша любимая IDE (рекомендуется vscode)
- Опционально, прочитайте думай как Qwik
Создание приложения с помощью CLI
Сначала создайте приложение Qwik с помощью Qwik CLI, который генерирует пустой стартовый шаблон, чтобы вы могли быстро ознакомиться с ним. Вы можете использовать эту же команду для создания проекта Qwik или Qwik City.
Запустите Qwik CLI в своём терминале. Qwik поддерживает npm, yarn, pnpm и bun. Выберите предпочитаемый менеджер пакетов и выполните одну из следующих команд:
npm create qwik@latest
pnpm create qwik@latest
yarn create qwik
bun create qwik@latest
CLI проведёт вас через интерактивное меню для задания названия проекта, выбора одного из стартовых шаблонов, и спросит, хотите ли вы установить зависимости. Чтобы узнать больше о создаваемых файлах, ознакомьтесь с документацией по структуре проекта.
Запуск сервера разработки:
npm start
pnpm start
yarn start
bun start
Qwik Joke App
Учебник Qwik Joke App поможет вам создать с помощью Qwik шуточное приложение, охватывая при этом наиболее важные концепции Qwik. Приложение отображает случайную шутку с сайта https://icanhazdadjoke.com и содержит кнопку для получения новой шутки по клику.
1. Создание маршрута
Всё начинается с обслуживания страницы по определённому маршруту. Это базовое приложение показывает случайную шутку по маршруту /joke/
. В этом учебнике используется Qwikcity, мета-фреймворк Qwik, который использует маршрутизацию на основе директорий. Чтобы начать:
-
Создайте в директории
routes
своего проекте новый каталогjoke
, содержащий файлindex.tsx
. -
Файл
index.tsx
каждого маршрута должен содержатьexport default component$(...)
, чтобы Qwikcity знал, какой контент обслуживать. Вставьте следующее содержимое вsrc/routes/joke/index.tsx
:
import { component$ } from "@builder.io/qwik";
export default component$(() => {
return <section class="section bright">Шутка!</section>;
});
- Перейдите по адресу
http://127.0.0.1:5173/joke/
, чтобы проверить, что ваша новая страница работает.
ПРИМЕЧАНИЕ:
- Ваш компонент по умолчанию
шуточного
маршрута вложен в существующий макет. Подробнее о том, что такое макеты и как с ними работать, смотрите в разделе Макет.- Подробнее о том, как создавать компоненты, смотрите в разделе Component API.
2. Загрузка данных
Мы будем использовать внешний JSON API по адресу https://icanhazdadjoke.com для загрузки случайных шуток. Мы будем использовать загрузчики маршрута, чтобы загрузить эти данные на сервер и затем отобразить их в компоненте.
- Откройте файл
src/routes/joke/index.tsx
и добавьте этот код:
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export default component$(() => {
// Вызов нашего хука `useDadJoke` вернёт реактивный сигнал на загруженные данные.
const dadJokeSignal = useDadJoke();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
</section>
);
});
- Теперь по адресу
http://127.0.0.1:5173/
браузер показывает случайную шутку.
Пояснение к коду:
- Функция, переданная в
routeLoader$
, немедленно вызывается на сервере перед рендером любого компонента и отвечает за загрузку данных; routeLoader$
возвращает use-хукuseDadJoke()
, который может быть использован в компоненте для получения данных сервера.
ПРИМЕЧАНИЕ:
- Функция
routeLoader$
немедленно вызывается на сервере перед рендером любого компонента, даже если ее use-хук не вызывается ни в одном компоненте;- Тип возврата
routeLoader$
вычисляется в компоненте без необходимости получения дополнительной информации о типе.
3. Отправка данных на сервер
Ранее мы использовали routeLoader$
для отправки данных с сервера на клиент. Чтобы отправить данные с клиента обратно на сервер, мы используем routeAction$
.
ПРИМЕЧАНИЕ: routeAction$
- это предпочтительный способ отправки данных на сервер, поскольку он использует нативный API формы браузера, который работает даже при отключенном JavaScript.
Для объявления действия добавьте следующий код:
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
export const useJokeVoteAction = routeAction$((props) => {
// Предоставляем читателю реализовать это самостоятельно в качестве упражнения.
console.log('VOTE', props);
});
- Обновите
export default
компонента для использования хукаuseJokeVoteAction
с элементом<Form>
.
export default component$(() => {
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">👍</button>
<button name="vote" value="down">👎</button>
</Form>
</section>
);
});
- Теперь по адресу
http://localhost:5173/joke/
отображаются кнопки, и если нажать на них, их значение логируется в консоль.
Объяснение кода:
routeAction$
получает данные.- Функция, переданная в
routeAction$
, вызывается на сервере каждый раз при отправке формы. - Функция
routeAction$
возвращает use-хукuseJokeVoteAction
, который вы можете использовать в компоненте для отправки данных формы.
- Функция, переданная в
Form
- это удобный компонент, который оборачивает нативный элемент<form>
браузера.
Обратите внимание:
- Для валидации используется zod validation.
- Функция
routeAction$
работает, даже если JavaScript отключен. - Если JavaScript включён, компонент
Form
не позволит браузеру отправить форму, а вместо этого отправит данные с помощью JavaScript и будет эмулировать поведение нативной формы браузера без полного обновления.
Для справки, полный фрагмент кода для этого раздела выглядит следующим образом:
import { component$ } from '@builder.io/qwik';
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
// Вызов нашего хука `useDadJoke`, вернёт реактивный сигнал на загруженные данные.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
</section>
);
});
4. Изменение состояния
Отслеживание состояния и обновление пользовательского интерфейса является основной частью работы приложений. Qwik предоставляет хук useSignal
для отслеживания состояния приложения. Для получения дополнительной информации смотрите управление состоянием.
Для объявления состояния:
- Импортируйте
useSignal
изqwik
.import { component$, useSignal } from "@builder.io/qwik";
- Объявите состояние компонента с помощью
useSignal()
.const isFavoriteSignal = useSignal(false);
- После закрывающего тега
Form
добавьте в компонент кнопку для изменения состояния.<button onClick$={() => { isFavoriteSignal.value = !isFavoriteSignal.value; }}> {isFavoriteSignal.value ? '❤️' : '🤍'} </button>
ПРИМЕЧАНИЕ: Нажатие на кнопку обновляет состояние, которое, в свою очередь, обновляет пользовательский интерфейс.
Для справки, полный фрагмент кода для этого раздела выглядит следующим образом:
import { component$, useSignal } from '@builder.io/qwik';
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
const isFavoriteSignal = useSignal(false);
// Вызов нашего хука `useDadJoke`, вернёт реактивный сигнал на загруженные данные.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
<button
onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
</section>
);
});
5. Задачи и вызов серверного кода
В Qwik задача - это работа, которая должна произойти при изменении состояния (это похоже на хук "effect" в других фреймворках). В этом примере мы будем использовать задачу для вызова кода на сервере.
- Импортируйте
useTask$
изqwik
.import { component$, useSignal, useTask$ } from "@builder.io/qwik";
- Создайте новую задачу, которая отслеживает состояние
isFavoriteSignal
:useTask$(({ track }) => {});
- Добавьте вызов
track
для повторного выполнения задачи при изменении состоянияisFavoriteSignal
:useTask$(({ track }) => { track(() => isFavoriteSignal.value); });
- Добавьте код, который вы хотите выполнить при изменении состояния:
useTask$(({ track }) => { track(() => isFavoriteSignal.value); console.log('FAVORITE (isomorphic)', isFavoriteSignal.value); });
- Если вы хотите, чтобы работа происходила только на сервере, оберните её в
server$()
.useTask$(({ track }) => { track(() => isFavoriteSignal.value); console.log('FAVORITE (isomorphic)', isFavoriteSignal.value); server$(() => { console.log('FAVORITE (server)', isFavoriteSignal.value); })(); });
ПРИМЕЧАНИЕ:
- Тело
useTask$
выполняется как на сервере, так и на клиенте (изоморфно); - Во время SSR сервер выведет в лог
FAVORITE (isomorphic) false
иFAVORITE (server) false
; - Когда пользователь взаимодействует с избранным, клиент выведет
FAVORITE (isomorphic) true
, и сервер выведетFAVORITE (server) true
.
Для справки, полный фрагмент кода для этого раздела выглядит следующим образом:
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import {
routeLoader$,
Form,
routeAction$,
server$,
} from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
const isFavoriteSignal = useSignal(false);
// Вызов нашего хука `useDadJoke`, вернёт реактивный сигнал на загруженные данные.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
server$(() => {
console.log('FAVORITE (server)', isFavoriteSignal.value);
})();
});
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
<button
onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
</section>
);
});
6. Стилизация
Стилизация - важная часть любого приложения. Qwik предоставляет способ ассоциировать и ограничить стили вашим компонентом.
Для добавления стилей:
-
Создайте новый файл
src/routes/joke/index.css
:p { font-weight: bold; } form { float: right; }
-
Импортируйте стили в
src/routes/joke/index.tsx
:import styles from "./index.css?inline";
-
Импортируйте
useStylesScoped$
изqwik
.import { component$, useSignal, useStylesScoped$, useTask$ } from "@builder.io/qwik";
-
Сообщите компоненту о необходимости загрузить стили:
useStylesScoped$(styles);
Объяснение кода:
- Параметр запроса
?inline
указывает Vite на вставку стилей в компонент; - Вызов
useStylesScoped$
указывает Qwik ассоциировать стили только с этим компонентом; - Стили загружаются только в том случае, если они ещё не вставлены SSR, и только для первого компонента.
Для справки, полный фрагмент кода для этого раздела выглядит следующим образом:
import {
component$,
useSignal,
useStylesScoped$,
useTask$,
} from '@builder.io/qwik';
import {
routeLoader$,
Form,
routeAction$,
server$,
} from '@builder.io/qwik-city';
import styles from './index.css?inline';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
useStylesScoped$(styles);
const isFavoriteSignal = useSignal(false);
// Вызов нашего хука `useDadJoke`, вернёт реактивный сигнал на загруженные данные.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
server$(() => {
console.log('FAVORITE (server)', isFavoriteSignal.value);
})();
});
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">👍</button>
<button name="vote" value="down">👎</button>
</Form>
<button
onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
</section>
);
});
7. Предварительный просмотр
Мы создали очень простое приложение, которое дало вам обзор ключевых концепций Qwik и API. Приложение работает в режиме dev, который использует горячую модульную перезагрузку (HMR) для непрерывного обновления приложения при изменении кода.
В режиме разработки:
- Каждый файл загружается отдельно, что может вызвать водопад на вкладке сети.
- Спекулятивная загрузка бандлов отсутствует, поэтому при первом взаимодействии может возникнуть задержка.
Давайте создадим производственную сборку, которая устранит эти проблемы.
Чтобы создать сборку для предварительного просмотра:
- Запустите
npm run preview
для создания сборки для рабочей среды.
ПРИМЕЧАНИЕ:
- Теперь ваше приложение собрано для рабочей среды и будет работать на другом порту.
- Если вы сейчас будете взаимодействовать с приложением, то на вкладке Network в инструментах разработчика должно быть видно, что бандлы мгновенно доставляются из кэша сервис-воркера.
Обзор
Поздравляю! Вы узнали много нового о Qwik! Чтобы узнать больше о том, как много можно достичь с помощью Qwik, мы рекомендуем прочитать специальную документацию по каждой из тем, затронутых в этом руководстве: