Qwik в двух словах
Компоненты
Компоненты Qwik очень похожи на компоненты React. Это функции, которые возвращают JSX. Однако, необходимо использовать component$(...)
, обработчики событий должны иметь суффикс $
, состояние создается с помощью useSignal()
, class
используется вместо className
и некоторые другие отличия.
- Компоненты всегда объявляются с помощью функции
component$
; - Компоненты могут использовать хук
useSignal
для создания реактивного состояния; - Обработчики событий объявляются с суффиксом
$
; - Для
<input>
событиеonChange
в Qwik называетсяonInput$
; - JSX предпочитает атрибуты HTML:
class
вместоclassName
,for
вместоhtmlFor
; - Условный рендер осуществляется с помощью тернарного оператора
?
и оператора&&
, как и в React;
import { component$, Slot } from '@builder.io/qwik';
import type { ClassList } from '@builder.io/qwik'
export component$((props: { class?: ClassList }) => { // ✅
return <div class={class}><Slot /></div>;
});
import { component$, useSignal } from '@builder.io/qwik';
// Другие компоненты могут быть импортированы и использованы в JSX.
import { MyOtherComponent } from './my-other-component';
interface MyComponentProps {
step: number;
}
// Компоненты всегда объявляются с помощью функции `component$`.
export const MyComponent = component$((props: MyComponentProps) => {
// Компоненты используют хук `useSignal` для создания реактивного состояния.
const count = useSignal(0); // { value: 0 }
return (
<>
<button
onClick$={() => {
// Обработчики событий имеют суффикс `$`.
count.value = count.value + props.step;
}}
>
Увеличить на {props.step}
</button>
<main
class={{
even: count.value % 2 === 0,
odd: count.value % 2 === 1,
}}
>
<h1>Счёт: {count.value}</h1>
<MyOtherComponent class="correct-way" /> {/* ✅ */}
{count.value > 10 && <p>Счёт больше 10</p>}
{count.value > 10 ? <p>Счёт больше 10</p> : <p>Счёт меньше 10</p>}
</MyOtherComponent>
</main>
</>
);
});
Вывод списка элементов
Как и в React, вы можете вывести список элементов с помощью функции map
, однако каждый элемент в списке должен иметь уникальное свойство key
. Ключ key
должен быть строкой или числом, и должен быть уникальным в пределах списка.
import { component$, useSignal } from '@builder.io/qwik';
import { US_PRESIDENTS } from './presidents';
export const PresidentsList = component$(() => {
return (
<ul>
{US_PRESIDENTS.map((president) => (
<li key={president.number}>
<h2>{president.name}</h2>
<p>{president.description}</p>
</li>
))}
</ul>
);
});
Переиспользование обработчиков событий
Обработчики событий в элементах JSX можно переиспользовать. Это делается путём создания обработчика с помощью $(...handler...)
.
import { $, component$, useSignal } from '@builder.io/qwik';
interface MyComponentProps {
step: number;
}
// Компоненты всегда объявляются с помощью функции `component$`.
export const MyComponent = component$(() => {
const count = useSignal(0);
// Обратите внимание на `$(...)` вокруг функции обработчика события.
const inputHandler = $((event, elem) => {
console.log(event.type, elem.value);
});
return (
<>
<input name="name" onInput$={inputHandler} />
<input
name="password"
onInput$={inputHandler}
/>
</>
);
});
Проекция содержимого
Проекция содержимого осуществляется компонентом <Slot/>
, который экспортируется из @builder.io/qwik
. Слотам можно присваивать имена, и они будут спроецированы с помощью атрибута q:slot
.
// Файл: src/components/Button/Button.tsx
import { component$, Slot } from '@builder.io/qwik';
import styles from './Button.module.css';
export const Button = component$(() => {
return (
<button class={styles.button}>
<div class={styles.start}>
<Slot name="start" />
</div>
<Slot />
<div class={styles.end}>
<Slot name="end" />
</div>
</button>
);
});
export default component$(() => {
return (
<Button>
<span q:slot="start">📩</span>
Привет, мир
<span q:slot="end">🟩</span>
</Button>
);
});
Правила использования хуков
Методы, начинающиеся с use
, являются специальными в Qwik, например, useSignal()
, useStore()
, useOn()
, useTask$()
, useLocation()
и т.д. Очень похоже на React-хуки.
- Они могут быть вызваны только внутри
component$
. - Они могут быть вызваны только на верхнем уровне
component$
-а, но не внутри условия или цикла.
Стилизация
Qwik из коробки поддерживает CSS-модули, или даже tailwind, глобальный импорт CSS и ленивую загрузку локальных стилей с помощью useStylesScoped$()
. Модули CSS - это рекомендуемый способ стилизации компонентов Qwik.
CSS-модули
Чтобы использовать CSS-модули, просто создайте файл .module.css
. Например, src/components/MyComponent/MyComponent.module.css
.
.container {
background-color: red;
}
Затем импортируйте CSS-модуль в свой компонент.
import { component$ } from '@builder.io/qwik';
import styles from './MyComponent.module.css';
export default component$(() => {
return (
<div class={styles.container}>Привет, мир</div>
);
});
Помните, что Qwik использует class
вместо className
для классов CSS.
$(...) rules
Функция $(...)
и любые функции, заканчивающиеся на $
, являются специальными в Qwik, например: $()
, useTask$()
, useVisibleTask$()
... Символ $
в конце обозначает границу ленивой загрузки. Существуют некоторые правила, которые применяются к первому аргументу любой функции $
. Это вообще НЕ связано с jQuery.
- Первый аргумент должен быть импортированной переменной.
- Первый аргумент должен быть объявленной переменной на верхнем уровне того же модуля.
- Первый аргумент должен быть выражением любых значений.
- Если первым аргументом является функция, то она может захватывать только переменные, которые объявлены на верхнем уровне того же модуля или значение которых сериализуемо. Сериализуемые значения включают:
string
,number
,boolean
,null
,undefined
,Array
,Object
,Date
,RegExp
,Map
,Set
,BigInt
,Promise
,Error
,JSX nodes
,Signal
,Store
и даже HTML-элементы.
// Допустимые примеры `$`-функций.
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedFunction } from './my-other-module';
export function exportedFunction() {
console.log('exported function');
}
export default component$(() => {
// Первый аргумент - функция.
const valid1 = $((event, elem) => {
console.log(event.type, elem.value);
});
// Первым аргументом является импортированный идентификатор.
const valid2 = $(importedFunction);
// Первым аргументом является идентификатор, объявленный на верхнем уровне того же модуля.
const valid3 = $(exportedFunction);
// Первый аргумент - выражение без локальных переменных.
const valid4 = $([1, 2, { a: 'hello' }]);
// Первый аргумент - это функция, которая захватывает локальную переменную.
const localVariable = 1;
const valid5 = $((event) => {
console.log('local variable', localVariable);
});
});
Вот несколько примеров недопустимых $
-функций.
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedVariable } from './my-other-module';
export default component$(() => {
const unserializable = new CustomClass();
const localVariable = 1;
// Первый аргумент - локальная переменная.
const invalid1 = $(localVariable);
// Первый аргумент - это функция, которая захватывает несериализуемую локальную переменную.
const invalid2 = $((event) => {
console.log('custom class', unserializable);
});
// Первый аргумент - это выражение, которое использует локальную переменную.
const invalid3 = $(localVariable + 1);
// Первый аргумент - это выражение, которое использует импортированную переменную.
const invalid4 = $(importedVariable + 'hello');
});
Реактивное состояние
useSignal(initialValue?)
useSignal()
является основным способом создания реактивного состояния. Сигналы могут быть общими для всех компонентов, и любой компонент или задача, считывающая сигнал (выполняющая: signal.value
) будет рендериться при изменении сигнала.
// Typescript definition for `Signal<T>` and `useSignal<T>`
export interface Signal<T> {
value: T;
}
export const useSignal: <T>(value?: T | (() => T)): Signal<T>;
useSignal(initialValue?)
принимает необязательное начальное значение и возвращает объект Signal<T>
. Объект Signal<T>
имеет свойство value
, которое можно читать и записывать. Когда компонент или задача обращается к свойству value
, то автоматически создаётся подписка, поэтому при изменении value
каждый компонент, задача или другой вычисляемый сигнал, который читает value
, будет вычислен заново.
useStore(initialValue?)
useStore(initialValue?)
похож на useSignal
, за исключением того, что он создаёт реактивный javascript-объект, делая каждое свойство объекта реактивным, как и value
сигнала. Под капотом useStore
реализуется с помощью объекта Proxy
, который перехватывает все обращения к свойствам, делая их реактивными.
// Typescript definition `useStore<T>`
// The `Reactive<T>` is a reactive version of the `T` type, every property of `T` behaves like a `Signal<T>`.
export interface Reactive<T extends Record<string, any>> extends T {}
export interface StoreOptions {
// If `deep` is true, then nested property of the store will be wrapped in a `Signal<T>`.
deep?: boolean;
}
export const useStore: <T>(value?: T | (() => T), options?: StoreOptions): Reactive<T>;
На практике useSignal
и useStore
очень похожи -- useSignal(0) === useStore({ value: 0 })
-- но useSignal
в большинстве случаев предпочтительнее. Некоторые случаи использования useStore
:
- Когда вам нужна реактивность в массиве.
- Когда вам нужен реактивный объект, к которому можно легко добавлять свойства.
import { component$, useStore } from '@builder.io/qwik';
export const Counter = component$(() => {
// Хук `useStore` используется для создания реактивного состояния.
const todoList = useStore(
{
array: [],
},
{ deep: true }
);
// todoList.array - это реактивный массив, поэтому при его изменении компонент перерендерится.
return (
<>
<h1>Todo List</h1>
<ul>
{todoList.array.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onInput$={() => {
// todoList - реактивное состояние
// из-за использования `deep: true`, `todo`-объект тоже реактивен,
// поэтому мы можем изменить свойство `completed`, и компонент будет перерисован.
todo.completed = !todo.completed;
}}
/>
{todo.text}
</li>
))}
</ul>
</>
);
});
useTask$(() => { ... })
useTask$
используется для создания асинхронной задачи. Задачи полезны для реализации побочных эффектов, выполнения тяжёлых вычислений и асинхронного кода в рамках жизненного цикла рендера. Задачи useTask$
выполняются перед первым рендером, и в последствии, когда отслеживаемый сигнал или состояние изменяются, задача выполняется заново.
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
export const Counter = component$(() => {
const page = useSignal(0);
const listOfUsers = useSignal([]);
// Хук `useTask$` используется для создания задачи.
useTask$(() => {
// Задача выполняется перед первым рендером.
console.log('Задача, выполняемая перед первым рендером');
});
// Вы можете создавать несколько задач, и они могут быть асинхронными.
useTask$(async (taskContext) => {
// Поскольку мы хотим повторно запускать задачу всякий раз, когда меняется `page`,
// нам нужно его отслеживать.
taskContext.track(() => page.value);
console.log('Задача выполняется перед первым рендерингом И при изменении страницы');
console.log('Текущая страница:', page.value);
// Задачи могут выполнять асинхронный код, например, получать данные.
const res = await fetch(`https://api.randomuser.me/?page=${page.value}`);
const json = await res.json();
// Изменение значения сигнала вызовет повторный рендер.
listOfUsers.value = json.results;
});
return (
<>
<h1>Страница {page.value}</h1>
<ul>
{listOfUsers.value.map((user) => (
<li key={user.login.uuid}>
{user.name.first} {user.name.last}
</li>
))}
</ul>
<button onClick$={() => page.value++}>Следующая страница</button>
</>
);
});
useTask$()
будет выполняться на сервере в процессе SSR, и в браузере, если компонент изначально монтируется на клиенте. Из-за этого не стоит обращаться к DOM API в задаче, так как они не будут доступны на сервере. Вместо этого следует использовать обработчики событий или useVisibleTask$()
для запуска задачи только на клиенте/браузере.
useVisibleTask$(() => { ... })
useVisibleTask$
используется для создания задачи, которая выполняется сразу после того, как компонент впервые установлен в DOM. Он похож на useTask$
, за исключением того, что он запускается только на клиенте, и выполняется после первого рендера. Поскольку он выполняется после рендера, можно обращаться к DOM или использовать API браузера.
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
export const Clock = component$(() => {
const time = useSignal(0);
// Хук `useVisibleTask$` используется для создания задачи, которая сразу запускается на клиенте.
useVisibleTask$((taskContext) => {
// Поскольку useVisibleTask не отслеживает никаких сигналов, она будет запущена только один раз.
const interval = setInterval(() => {
time.value = new Date();
}, 1000);
// Функция `cleanup` вызывается при удалении компонента или при повторном запуске задачи.
taskContext.cleanup(() => clearInterval(interval));
});
return (
<>
<h1>Часы</h1>
<h1>Прошло секунд: {time.value}</h1>
</>
);
});
Поскольку Qwik НЕ выполняет Javascript в браузере до взаимодействия с пользователем, useVisibleTask$()
- это единственный API, который будет запускать выполнение сразу на клиенте, вот почему это хорошее место для таких вещей, как:
- Обращение к DOM API;
- Инициализация библиотек, предназначенных только для браузера;
- Запуск кода аналитик;
- Запуск анимации или таймера.
Обратите внимание, что useVisibleTask$()
не следует использовать для получения данных, поскольку она не будет выполняться на сервере. Вместо этого для получения данных следует использовать useTask$()
, а затем использовать useVisibleTask$()
для выполнения таких действий, как запуск анимации. Злоупотребление useVisibleTask$()
может привести к низкой производительности.
Маршрутизация
Qwik поставляется с маршрутизатором на основе файлов, который похож на Next.js, но имеет несколько отличий. Маршрутизатор основан на файловой системе, точнее, на src/routes/
. Создание нового файла index.tsx
в src/routes/
приведет к созданию нового маршрута. Например, src/routes/home/index.tsx
создаст маршрут по адресу /home/
.
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <h1>Home</h1>;
});
Очень важно экспортировать компонент по умолчанию, иначе маршрутизатор не сможет его найти.
Параметры маршрута
Вы можете создавать динамические маршруты, добавляя папку с [param]
в путь маршрута. Например, src/routes/user/[id]/index.tsx
создаст маршрут по адресу /user/:id/
. Для доступа к параметру маршрута можно использовать хук useLocation
, экспортированный из @builder.io/qwik-city
.
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const loc = useLocation();
return (
<main>
{loc.isNavigating && <p>Загрузка...</p>}
<h1>Пользователь: {loc.params.userID}</h1>
<p>Текущий URL: {loc.url.href}</p>
</main>
);
});
useLocation()
возвращает реактивный объект RouteLocation
, что означает, что он будет ререндериться каждый раз при изменении маршрута. Объект RouteLocation
имеет следующие свойства:
/**
* Текущее местоположение маршрута, возвращаемое функцией `useLocation()`.
*/
export interface RouteLocation {
readonly params: Readonly<Record<string, string>>;
readonly url: URL;
readonly isNavigating: boolean;
}
Связь с другими маршрутами
Для связи с другими маршрутами можно использовать компонент Link
, экспортированный из @builder.io/qwik-city
. Компонент Link
принимает все свойства элемента <a>
HTMLAnchorElement. Единственное отличие заключается в том, что он будет использовать маршрутизатор Qwik для SPA-навигации к маршруту вместо перехода на страницу.
import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';
export default component$(() => {
return (
<>
<h1>Home</h1>
<Link href="/about/">SPA-навигация в раздел /about/</Link>
<a href="/about/">Переход на страницу /about/</a>
</>
);
});
Получение / загрузка данных
Рекомендуемый способ загрузки данных с сервера - использовать функцию routeLoader$()
, экспортированную из @builder.io/qwik-city
. Функция routeLoader$()
используется для создания загрузчика данных, который будет выполняться на сервере перед рендером маршрута. Возврат routeLoader$()
должен быть экспортирован как именованный экспорт из файла маршрута, т.е. он может быть использован только в файле index.tsx
, внутри src/routes/
.
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
// Функция `routeLoader$()` используется для создания загрузчика данных, который будет выполняться на сервере перед рендером маршрута.
// Возврат `routeLoader$()` - это пользовательский хук use, который можно использовать для доступа к данным, возвращаемым из `routeLoader$()`.
export const useUserData = routeLoader$(async (requestContext) => {
const user = await db.table('users').get(requestContext.params.userID);
return {
name: user.name,
email: user.email,
};
});
export default component$(() => {
// Хук `useUserData` будет возвращать `Signal`, содержащий данные, возвращённые из `routeLoader$()`, который будет ререндерить компонент, когда навигация изменится, и routeLoader$() будет повторно запущен.
const userData = useUserData();
return (
<main>
<h1>Данные пользователя</h1>
<p>Имя пользователя: {userData.value.name}</p>
<p>Email пользователя: {userData.value.email}</p>
</main>
);
});
// Экспортируемая функция `head` используется для установки заголовка документа для маршрута.
export const head: DocumentHead = ({resolveValue}) => {
// Она может использовать метод `resolveValue()` для разрешения значения из `routeLoader$()`.
const user = resolveValue(useUserData);
return {
title: `User: "${user.name}"`,
meta: [
{
name: 'description',
content: 'User page',
},
],
};
};
Функция routeLoader$()
принимает функцию, которая возвращает промис. Промис разрешается на сервере, и разрешённое значение передается в хук useCustomLoader$()
. Хук useCustomLoader$()
- это пользовательский хук, который создается функцией routeLoader$()
. Хук useCustomLoader$()
возвращает Signal
, который содержит разрешённое значение промиса, возвращённого из функции routeLoader$()
. Хук useCustomLoader$()
будет рендерить компонент при каждом изменении маршрута и повторном запуске функции routeLoader$()
.
Обработка отправленных форм
Qwik предоставляет API routeAction$()
, экспортированный из @builder.io/qwik-city
для обработки запросов формы на сервере. routeAction$()
выполняется на сервере только в момент отправки формы.
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
// Функция `routeAction$()` используется для создания загрузчика данных, который будет выполняться на сервере при отправке формы.
// `routeAction$()` возвращает пользовательский хук, который можно использовать для доступа к данным, возвращаемым из `routeAction$()`.
export const useUserUpdate = routeAction$(async (data, requestContext) => {
const user = await db.table('users').get(requestContext.params.userID);
user.name = data.name;
user.email = data.email;
await db.table('users').put(user);
return {
user,
};
}, zod$({
name: z.string(),
email: z.string(),
}));
export default component$(() => {
// Хук `useUserUpdate` вернёт `ActionStore<T>`, содержащий `value`, возвращённое из `routeAction$()`, и некоторые другие свойства, такие как `submit()`, которое используется для программной отправки формы, и свойство `isRunning`. Все эти свойства являются реактивными и будут перерисовывать компонент всякий раз, когда они изменяются.
const userData = useUserUpdate();
// userData.value — это значение, возвращаемое функцией `routeAction$()`, которое является неопределённым перед отправкой формы.
// userData.formData — это данные формы, которые были отправлены, они `undefined` перед отправкой формы.
// userData.isRunning — это логическое значение, которое принимает значение `true` при отправке формы.
// userData.submit() — это функция, которую можно использовать для отправки формы программно.
// userData.actionPath — это путь к действию, которое используется для отправки формы.
return (
<main>
<h1>Данные пользователя</h1>
<Form action={userData}>
<div>
<label>Имя пользователя: <input name="name" defaultValue={userData.formData?.get('name')} /></label>
</div>
<div>
<label>Электронная почта: <input name="email" defaultValue={userData.formData?.get('email')} /></label>
</div>
<button type="submit">Обновить</button>
</Form>
</main>
);
});
routeAction$()
используется в паре с компонентом Form
, экспортированным из @builder.io/qwik-city
. Компонент Form
представляет собой оболочку вокруг HTML-элемента <form>
. Компонент Form
принимает ActionStore<T>
в качестве свойства action
. ActionStore<T>
— это возвращаемое значение функции routeAction$()
.
Запуск кода только в браузере
Поскольку Qwik выполняет один и тот же код на сервере и в браузере, вы не можете использовать window
или другие API браузера в своём коде, поскольку они не существуют, когда код выполняется на сервере.
Если вы хотите получить доступ к API браузера, таким как window
, document
, localStorage
, sessionStorage
, webgl
и т.д., вам необходимо проверить, запущен ли код в браузере, прежде чем обращаться к ним.
import { component$, useTask$, useVisibleTask$, useSignal } from '@builder.io/qwik';
import { isBrowser } from '@builder.io/qwik/build';
export default component$(() => {
const ref = useSignal<Element>();
// useVisibleTask$ будет выполняться только в браузере.
useVisibleTask$(() => {
// Нет необходимости проверять `isBrowser` перед доступом к DOM, потому что `useVisibleTask$` будет выполняться только в браузере.
ref.value?.focus();
document.title = 'Привет, мир';
});
// useTask может выполняться на сервере, поэтому вам необходимо проверить `isBrowser` перед доступом к DOM.
useTask$(() => {
if (isBrowser) {
// Этот код будет выполняться в браузере только при первом отображении компонента.
ref.value?.focus();
document.title = 'Привет, мир';
}
});
return (
<button
ref={ref}
onClick$={() => {
// Все обработчики событий выполняются только в браузере, поэтому доступ к DOM безопасен.
ref.value?.focus();
document.title = 'Привет, мир';
}}
>
Нажми меня
</button>
);
});
useVisibleTask$(() => { ... })
Этот API объявит функцию VisibleTask, которая будет выполняться только на клиенте/браузере. Она никогда не будет запущена на сервере.
Обработчики событий JSX
Обработчики JSX, такие как onClick$
и onInput$
, выполняются только на клиенте. Это потому, что они являются событиями DOM, поскольку на сервере нет DOM, они не будут выполняться на сервере.
isBrowser
Условное выражение Qwik предоставляет логическое значение isBrowser
, экспортируемое из @builder.io/qwik/build
. Вы можете использовать его, чтобы убедиться, что код работает только в браузере.
Запуск кода только на сервере
Иногда вам нужно выполнять код только на сервере, например, получать данные или обращаться к базе данных. Для решения этой проблемы Qwik предоставляет несколько API для запуска кода только на сервере.
import { component$, useTask$ } from '@builder.io/qwik';
import { server$, routeLoader$ } from '@builder.io/qwik/qwik-city';
import { isServer } from '@builder.io/qwik/build';
export const useGetProducts = routeLoader$((requestEvent) => {
// Этот код будет работать только на сервере.
const db = await openDB(requestEvent.env.get('DB_PRIVATE_KEY'));
const product = await db.table('products').select();
return product;
});
const encryptOnServer = server$(function(message: string) {
// `this` - это `requestEvent`
const secretKey = this.env.get('SECRET_KEY');
const encryptedMessage = encrypt(message, secretKey);
return encryptedMessage;
});
export default component$(() => {
useTask$(() => {
if (isServer) {
// Этот код будет работать только на сервере и только тогда, когда компонент будет впервые отрендерен на сервере.
}
});
return (
<>
<button
onClick$={server$(() => {
// Этот код будет выполняться на сервере только при нажатии кнопки.
})}
>
Нажми меня
</button>
<button
onClick$={() => {
// Этот код вызовет серверную функцию и дождётся результата.
const encrypted = await encryptOnServer('Привет, мир');
console.log(encrypted);
}}
>
Нажми меня
</button>
</>
);
});
routeAction$()
routeAction$()
- это специальный компонент, который выполняется только на сервере. Он используется для обработки отправки форм и других действий. Например, вы можете использовать его для добавления пользователя в базу данных, а затем перенаправить на страницу профиля пользователя.
routeLoader$()
routeLoader$()
— это специальный компонент, который выполняется только на сервере. Он используется для получения данных, а затем рендера страницы. Например, вы можете использовать его для получения данных из API, а затем рендерить страницу с данными.
server$((...args) => { ... })
server$()
— это особый способ объявления функций, которые выполняются только на сервере. При вызове с клиента они будут вести себя как RPC-вызов и будут выполняться на сервере. Они могут принимать любые сериализуемые аргументы и возвращать любое сериализуемое значение.
isServer
Условное выражение Qwik предоставляет логическое значение isServer
, экспортированное из @builder.io/qwik/build
. Вы можете использовать его, чтобы убедиться, что код работает только на сервере.