Начало работы с Qwik

Qwik - это новый вид фреймворка, который является возобновляемым (без JS и гидратации), современным и знакомым для разработчиков на React.

Чтобы попробовать его прямо сейчас, попробуйте наши песочницы в браузере:

Подготовка

Чтобы начать работу с 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, который использует маршрутизацию на основе директорий. Чтобы начать:

  1. Создайте в директории routes своего проекте новый каталог joke, содержащий файл index.tsx.

  2. Файл index.tsx каждого маршрута должен содержать export default component$(...), чтобы Qwikcity знал, какой контент обслуживать. Вставьте следующее содержимое в src/routes/joke/index.tsx:

src/routes/joke/index.tsx
import { component$ } from "@builder.io/qwik";
 
export default component$(() => {
  return <section class="section bright">Шутка!</section>;
});
  1. Перейдите по адресу http://127.0.0.1:5173/joke/, чтобы проверить, что ваша новая страница работает.

ПРИМЕЧАНИЕ:

  • Ваш компонент по умолчанию шуточного маршрута вложен в существующий макет. Подробнее о том, что такое макеты и как с ними работать, смотрите в разделе Макет.
  • Подробнее о том, как создавать компоненты, смотрите в разделе Component API.

2. Загрузка данных

Мы будем использовать внешний JSON API по адресу https://icanhazdadjoke.com для загрузки случайных шуток. Мы будем использовать загрузчики маршрута, чтобы загрузить эти данные на сервер и затем отобразить их в компоненте.

  1. Откройте файл src/routes/joke/index.tsx и добавьте этот код:
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>
  );
});
  1. Теперь по адресу http://127.0.0.1:5173/ браузер показывает случайную шутку.

Пояснение к коду:

  • Функция, переданная в routeLoader$, немедленно вызывается на сервере перед рендером любого компонента и отвечает за загрузку данных;
  • routeLoader$ возвращает use-хук useDadJoke(), который может быть использован в компоненте для получения данных сервера.

ПРИМЕЧАНИЕ:

  • Функция routeLoader$ немедленно вызывается на сервере перед рендером любого компонента, даже если ее use-хук не вызывается ни в одном компоненте;
  • Тип возврата routeLoader$ вычисляется в компоненте без необходимости получения дополнительной информации о типе.

3. Отправка данных на сервер

Ранее мы использовали routeLoader$ для отправки данных с сервера на клиент. Чтобы отправить данные с клиента обратно на сервер, мы используем routeAction$.

ПРИМЕЧАНИЕ: routeAction$ - это предпочтительный способ отправки данных на сервер, поскольку он использует нативный API формы браузера, который работает даже при отключенном JavaScript.

Для объявления действия добавьте следующий код:

src/routes/joke/index.tsx
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
 
export const useJokeVoteAction = routeAction$((props) => {
  // Предоставляем читателю реализовать это самостоятельно в качестве упражнения.
  console.log('VOTE', props);
});
  1. Обновите export default компонента для использования хука useJokeVoteAction с элементом <Form>.
src/routes/joke/index.tsx
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>
  );
});
  1. Теперь по адресу http://localhost:5173/joke/ отображаются кнопки, и если нажать на них, их значение логируется в консоль.

Объяснение кода:

  • routeAction$ получает данные.
    • Функция, переданная в routeAction$, вызывается на сервере каждый раз при отправке формы.
    • Функция routeAction$ возвращает use-хук useJokeVoteAction, который вы можете использовать в компоненте для отправки данных формы.
  • Form - это удобный компонент, который оборачивает нативный элемент <form> браузера.

Обратите внимание:

  • Для валидации используется zod validation.
  • Функция routeAction$ работает, даже если JavaScript отключен.
  • Если JavaScript включён, компонент Form не позволит браузеру отправить форму, а вместо этого отправит данные с помощью JavaScript и будет эмулировать поведение нативной формы браузера без полного обновления.

Для справки, полный фрагмент кода для этого раздела выглядит следующим образом:

src/routes/joke/index.tsx
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 для отслеживания состояния приложения. Для получения дополнительной информации смотрите управление состоянием.

Для объявления состояния:

  1. Импортируйте useSignal из qwik.
    import { component$, useSignal } from "@builder.io/qwik";
  2. Объявите состояние компонента с помощью useSignal().
    const isFavoriteSignal = useSignal(false);
  3. После закрывающего тега Form добавьте в компонент кнопку для изменения состояния.
    <button
     onClick$={() => {
       isFavoriteSignal.value = !isFavoriteSignal.value;
     }}>
      {isFavoriteSignal.value ? '❤️' : '🤍'}
    </button>

ПРИМЕЧАНИЕ: Нажатие на кнопку обновляет состояние, которое, в свою очередь, обновляет пользовательский интерфейс.

Для справки, полный фрагмент кода для этого раздела выглядит следующим образом:

src/routes/joke/index.tsx
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" в других фреймворках). В этом примере мы будем использовать задачу для вызова кода на сервере.

  1. Импортируйте useTask$ из qwik.
    import { component$, useSignal, useTask$ } from "@builder.io/qwik";
  2. Создайте новую задачу, которая отслеживает состояние isFavoriteSignal:
    useTask$(({ track }) => {});
  3. Добавьте вызов track для повторного выполнения задачи при изменении состояния isFavoriteSignal:
    useTask$(({ track }) => {
      track(() => isFavoriteSignal.value);
    });
  4. Добавьте код, который вы хотите выполнить при изменении состояния:
    useTask$(({ track }) => {
      track(() => isFavoriteSignal.value);
      console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
    });
  5. Если вы хотите, чтобы работа происходила только на сервере, оберните её в 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.

Для справки, полный фрагмент кода для этого раздела выглядит следующим образом:

src/routes/joke/index.tsx
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 предоставляет способ ассоциировать и ограничить стили вашим компонентом.

Для добавления стилей:

  1. Создайте новый файл src/routes/joke/index.css:

    p {
      font-weight: bold;
    }
     
    form {
      float: right;
    }
  2. Импортируйте стили в src/routes/joke/index.tsx:

     import styles from "./index.css?inline";
  3. Импортируйте useStylesScoped$ из qwik.

    import { component$, useSignal, useStylesScoped$, useTask$ } from "@builder.io/qwik";
  4. Сообщите компоненту о необходимости загрузить стили:

    useStylesScoped$(styles);

Объяснение кода:

  • Параметр запроса ?inline указывает Vite на вставку стилей в компонент;
  • Вызов useStylesScoped$ указывает Qwik ассоциировать стили только с этим компонентом;
  • Стили загружаются только в том случае, если они ещё не вставлены SSR, и только для первого компонента.

Для справки, полный фрагмент кода для этого раздела выглядит следующим образом:

src/routes/joke/index.tsx
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) для непрерывного обновления приложения при изменении кода.

В режиме разработки:

  • Каждый файл загружается отдельно, что может вызвать водопад на вкладке сети.
  • Спекулятивная загрузка бандлов отсутствует, поэтому при первом взаимодействии может возникнуть задержка.

Давайте создадим производственную сборку, которая устранит эти проблемы.

Чтобы создать сборку для предварительного просмотра:

  1. Запустите npm run preview для создания сборки для рабочей среды.

ПРИМЕЧАНИЕ:

  • Теперь ваше приложение собрано для рабочей среды и будет работать на другом порту.
  • Если вы сейчас будете взаимодействовать с приложением, то на вкладке Network в инструментах разработчика должно быть видно, что бандлы мгновенно доставляются из кэша сервис-воркера.

Обзор

Поздравляю! Вы узнали много нового о Qwik! Чтобы узнать больше о том, как много можно достичь с помощью Qwik, мы рекомендуем прочитать специальную документацию по каждой из тем, затронутых в этом руководстве:

Участники

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

  • manucorporat
  • jesperp
  • adamdbradley
  • steve8708
  • cunzaizhuyi
  • mousaAM
  • zanettin
  • Craiqser
  • MyltsinVV
  • literalpie
  • colynyu
  • the-r3aper7
  • ahmadalfy
  • renomureza
  • mhevery
  • AnthonyPAlicea
  • kapunahelewong
  • kushalmahajan
  • sreeisalso
  • dustinsgoodman
  • nsdonato
  • seqshem
  • ryo-manba
  • EamonHeffernan
  • DKozachenko
  • mrhoodz
  • moinulmoin
  • lanc3lo1
  • johnrackles
  • kushalvmahajan
  • daniela-bonvini