Qwik 简介
组件
Qwik 组件与 React 组件非常相似。它们是返回 JSX 的函数。但是,需要使用 component$(...)
,事件处理程序必须具有 $
后缀,状态使用 useSignal()
创建,使用 class
而不是 className
,以及其他一些差异。
import { component$, Slot } from '@builder.io/qwik';
import type { ClassList } from '@builder.io/qwik'
export const MyOtherComponent = component$((props: { class?: ClassList }) => { // ✅
return <div class={class}><Slot /></div>;
});
import { component$, useSignal } from '@builder.io/qwik';
// Other components can be imported and used in JSX.
import { MyOtherComponent } from './my-other-component';
interface MyComponentProps {
step: number;
}
// Components are always declared with the `component$` function.
export const MyComponent = component$((props: MyComponentProps) => {
// Components use the `useSignal` hook to create reactive state.
const count = useSignal(0); // { value: 0 }
return (
<>
<button
onClick$={() => {
// Event handlers have the `$` suffix.
count.value = count.value + props.step;
}}
>
Increment by {props.step}
</button>
<main
class={{
even: count.value % 2 === 0,
odd: count.value % 2 === 1,
}}
>
<h1>Count: {count.value}</h1>
<MyOtherComponent class="correct-way"> {/* ✅ */}
{count.value > 10 && <p>Count is greater than 10</p>}
{count.value > 10 ? <p>Count is greater than 10</p> : <p>Count is less than 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;
}
// Components are always declared with the `component$` function.
export const MyComponent = component$(() => {
const count = useSignal(0);
// Notice the `$(...)` around the event handler function.
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
属性投影到其中。
// File: 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>
Hello world
<span q:slot="end">🟩</span>
</Button>
);
});
使用钩子的规则
以 use
开头的函数在 Qwik 中是特殊的,例如 useSignal()
、useStore()
、useOn()
、useTask$()
、useLocation()
等等。与 React 钩子非常相似。
- 它们只能在组件 $ 中调用。
- 它们只能从组件 $ 的顶层调用,不能在条件语句或循环中调用。
样式
Qwik 支持开箱即用的 CSS 模块,甚至 Tailwind、全局 CSS 导入和使用 useStylesScoped$()
延迟加载作用域 CSS。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}>Hello world</div>;
});
请记住,Qwik 使用 class
而不是 className
来表示 CSS 类。
$(...) 规则
$(...)
函数和任何以 $
结尾的函数在 Qwik 中都是特殊的,例如:$()
、useTask$()
、useVisibleTask$()
... 结尾的 $
代表延迟加载边界。任何 $
函数的第一个参数都有一些规则适用。它与 jQuery 完全无关。
- 第一个参数必须是导入的变量。
- 第一个参数必须是在同一模块的顶层声明的变量。
- 第一个参数必须是任何变量的表达式。
- 如果第一个参数是函数,它只能捕获在同一模块的顶层声明的变量,或者其值是可序列化的。可序列化值包括:
string
、number
、boolean
、null
、undefined
、Array
、Object
、Date
、RegExp
、Map
、Set
、BigInt
、Promise
、Error
、JSX 节点
、Signal
、Store
甚至 HTMLElements。
// Valid examples of `$` functions.
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedFunction } from './my-other-module';
export function exportedFunction() {
console.log('exported function');
}
export default component$(() => {
// The first argument is a function.
const valid1 = $((event, elem) => {
console.log(event.type, elem.value);
});
// The first argument is an imported identifier.
const valid2 = $(importedFunction);
// The first argument is an identifier declared at the top level of the same module.
const valid3 = $(exportedFunction);
// The first argument is an expression without local variables.
const valid4 = $([1, 2, { a: 'hello' }]);
// The first argument is a function that captures a local variable.
const localVariable = 1;
const valid5 = $((event) => {
console.log('local variable', localVariable);
});
});
以下是一些无效 $
函数的示例。
// Invalid examples of `$` functions.
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedVariable } from './my-other-module';
export default component$(() => {
const unserializable = new CustomClass();
const localVariable = 1;
// The first argument is a local variable.
const invalid1 = $(localVariable);
// The first argument is a function that captures an unserializable local variable.
const invalid2 = $((event) => {
console.log('custom class', unserializable);
});
// The first argument is an expression that uses a local variable.
const invalid3 = $(localVariable + 1);
// The first argument is an expression that uses an imported variable.
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$(() => {
// The `useStore` hook is used to create a reactive store.
const todoList = useStore(
{
array: [],
},
{ deep: true }
);
// todoList.array is a reactive array, so we can push to it and the component will re-render.
return (
<>
<h1>Todo List</h1>
<ul>
{todoList.array.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onInput$={() => {
// todoList is a reactive store
// because we used `deep: true`, the `todo` object is also reactive.
// so we can change the `completed` property and the component will re-render.
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([]);
// The `useTask$` hook is used to create a task.
useTask$(() => {
// The task is executed before the first render.
console.log('Task executed before first render');
});
// You can create multiple tasks, and they can be async.
useTask$(async (taskContext) => {
// Since we want to re-run the task whenever the `page` changes,
// we need to track it.
taskContext.track(() => page.value);
console.log('Task executed before the first render AND when page changes');
console.log('Current page:', page.value);
// Tasks can run async code, such as fetching data.
const res = await fetch(`https://api.randomuser.me/?page=${page.value}`);
const json = await res.json();
// Assigning to a signal will trigger a re-render.
listOfUsers.value = json.results;
});
return (
<>
<h1>Page {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++}>Next Page</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);
// The `useVisibleTask$` hook is used to create a task that runs eagerly on the client.
useVisibleTask$((taskContext) => {
// Since this VisibleTask is not tracking any signals, it will only run once.
const interval = setInterval(() => {
time.value = new Date();
}, 1000);
// The `cleanup` function is called when the component is unmounted, or when the task is re-run.
taskContext.cleanup(() => clearInterval(interval));
});
return (
<>
<h1>Clock</h1>
<h1>Seconds passed: {time.value}</h1>
</>
);
});
由于 Qwik 在用户交互之前不会在浏览器上执行任何 Javascript,因此 useVisibleTask$()
是唯一一个将在客户端急切运行的 API,这就是为什么它是一个执行以下操作的好地方:
- 访问 DOM API
- 初始化仅浏览器库
- 运行分析代码
- 启动动画或计时器。
请注意,useVisibleTask$()
不应用于获取数据,因为它不会在服务器上运行。相反,您应该使用 useTask$()
获取数据,然后使用 useVisibleTask$()
执行诸如启动动画之类的操作。滥用 useVisibleTask$()
会导致性能下降。
路由
Qwik 带有一个基于文件的路由器,它类似于 Next.js,但有一些区别。路由器基于文件系统,具体来说是在 src/routes/
中。在 src/routes/
下的文件夹中创建一个新的 index.tsx
文件将创建一个新的路由。例如,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/
上创建一个路由。为了访问路由参数,您可以使用从 @builder.io/qwik-city
导出的 useLocation
钩子。
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const loc = useLocation();
return (
<main>
{loc.isNavigating && <p>Loading...</p>}
<h1>User: {loc.params.userID}</h1>
<p>Current URL: {loc.url.href}</p>
</main>
);
});
useLocation()
返回一个反应式 RouteLocation
对象,这意味着它将在路由发生变化时重新渲染。RouteLocation
对象具有以下属性
/**
* The current route location returned by `useLocation()`.
*/
export interface RouteLocation {
readonly params: Readonly<Record<string, string>>;
readonly url: URL;
readonly isNavigating: boolean;
}
链接到其他路由
要链接到其他路由,您可以使用从 @builder.io/qwik-city
导出的 Link
组件。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 navigate to /about/</Link>
<a href="/about/">Full page navigate to /about/</a>
</>
);
});
获取/加载数据
从服务器加载数据的推荐方法是使用从 @builder.io/qwik-city
导出的 routeLoader$()
函数。routeLoader$()
函数用于创建一个数据加载器,该加载器将在渲染路由之前在服务器上执行。routeLoader$()
的返回值必须作为命名导出从路由文件导出,即它只能在 src/routes/
内的 index.tsx
中使用。
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
// The `routeLoader$()` function is used to create a data loader that will be executed on the server before the route is rendered.
// The return of `routeLoader$()` is a custom use hook, which can be used to access the data returned from `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$(() => {
// The `useUserData` hook will return a `Signal` containing the data returned from `routeLoader$()`, which will re-render the component, whenever the navigation changes, and the routeLoader$() is re-run.
const userData = useUserData();
return (
<main>
<h1>User data</h1>
<p>User name: {userData.value.name}</p>
<p>User email: {userData.value.email}</p>
</main>
);
});
// Exported `head` function is used to set the document head for the route.
export const head: DocumentHead = ({resolveValue}) => {
// It can use the `resolveValue()` method to resolve the value from `routeLoader$()`.
const user = resolveValue(useUserData);
return {
title: `User: "${user.name}"`,
meta: [
{
name: 'description',
content: 'User page',
},
],
};
};
routeLoader$()
函数接受一个返回 Promise 的函数。Promise 在服务器上解析,解析后的值传递给 useCustomLoader$()
钩子。useCustomLoader$()
钩子是由 routeLoader$()
函数创建的自定义钩子。useCustomLoader$()
钩子返回一个 Signal
,其中包含从 routeLoader$()
函数返回的 Promise 的解析后的值。每当路由发生变化时,useCustomLoader$()
钩子将重新渲染组件,并且 routeLoader$()
函数将重新运行。
处理表单提交
Qwik 提供了从 @builder.io/qwik-city
导出的 routeAction$()
API 来处理服务器上的表单请求。routeAction$()
仅在提交表单时在服务器上执行。
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
// The `routeAction$()` function is used to create a data loader that will be executed on the server when the form is submitted.
// The return of `routeAction$()` is a custom use hook, which can be used to access the data returned from `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$(() => {
// The `useUserUpdate` hook will return an `ActionStore<T>` containing the `value` returned from `routeAction$()`, and some other properties, such as `submit()`, which is used to submit the form programmatically, and `isRunning`. All of these properties are reactive, and will re-render the component whenever they change.
const userData = useUserUpdate();
// userData.value is the value returned from `routeAction$()`, which is `undefined` before the form is submitted.
// userData.formData is the form data that was submitted, it is `undefined` before the form is submitted.
// userData.isRunning is a boolean that is true when the form is being submitted.
// userData.submit() is a function that can be used to submit the form programmatically.
// userData.actionPath is the path to the action, which is used to submit the form.
return (
<main>
<h1>User data</h1>
<Form action={userData}>
<div>
<label>User name: <input name="name" defaultValue={userData.formData?.get('name')} /></label>
</div>
<div>
<label>User email: <input name="email" defaultValue={userData.formData?.get('email')} /></label>
</div>
<button type="submit">Update</button>
</Form>
</main>
);
});
routeAction$()
与从 @builder.io/qwik-city
导出的 Form
组件配对使用。Form
组件是围绕原生 HTML <form>
元素的包装器。Form
组件将 ActionStore<T>
作为 action
属性。ActionStore<T>
是 routeAction$()
函数的返回值。
仅在浏览器中运行代码
由于 Qwik 在服务器和浏览器上执行相同的代码,因此您无法在代码中使用 window
或其他浏览器 API,因为它们在服务器上执行代码时不存在。
如果您想访问浏览器 API,例如 window
、document
、localStorage
、sessionStorage
、webgl
等,您需要在访问浏览器 API 之前检查代码是否在浏览器中运行。
import { component$, useTask$, useVisibleTask$, useSignal } from '@builder.io/qwik';
import { isBrowser } from '@builder.io/qwik/build';
export default component$(() => {
const ref = useSignal<Element>();
// useVisibleTask$ will only run in the browser
useVisibleTask$(() => {
// No need to check for `isBrowser` before accessing the DOM, because useVisibleTask$ will only run in the browser
ref.value?.focus();
document.title = 'Hello world';
});
// useTask might run on the server, so you need to check for `isBrowser` before accessing the DOM
useTask$(() => {
if (isBrowser) {
// This code will only run in the browser only when the component is first rendered there
ref.value?.focus();
document.title = 'Hello world';
}
});
return (
<button
ref={ref}
onClick$={() => {
// All event handlers are only executed in the browser, so it's safe to access the DOM
ref.value?.focus();
document.title = 'Hello world';
}}
>
Click me
</button>
);
});
useVisibleTask$(() => { ... })
此 API 将声明一个 VisibleTask,确保它仅在客户端/浏览器上运行。它永远不会在服务器上运行。
JSX 事件处理程序
JSX 处理程序(如 onClick$
和 onInput$
)仅在客户端执行。这是因为它们是 DOM 事件,由于服务器上没有 DOM,因此它们不会在服务器上执行。
仅在服务器上运行代码
有时您需要仅在服务器上运行代码,例如获取数据或访问数据库。为了解决这个问题,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) => {
// This code will only run on the server
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` is the `requestEvent
const secretKey = this.env.get('SECRET_KEY');
const encryptedMessage = encrypt(message, secretKey);
return encryptedMessage;
});
export default component$(() => {
useTask$(() => {
if () {
// This code will only run on the server only when the component is first rendered in the server
}
});
return (
<>
<button
onClick$={server$(() => {
// This code will only run on the server when the button is clicked
})}
>
Click me
</button>
<button
onClick$={() => {
// This code will call the server function, and wait for the result
const encrypted = await encryptOnServer('Hello world');
console.log(encrypted);
}}
>
Click me
</button>
</>
);
});
routeAction$()
routeAction$()
是一个特殊的组件,它仅在服务器上执行。它用于处理表单提交和其他操作。例如,您可以使用它将用户添加到数据库,然后重定向到用户个人资料页面。
routeLoader$()
routeLoader$()
是一个特殊的组件,它仅在服务器上执行。它用于获取数据,然后渲染页面。例如,您可以使用它从 API 获取数据,然后使用数据渲染页面。
server$((...args) => { ... })
server$()
是一种特殊的方式来声明仅在服务器上运行的函数。如果从客户端调用,它们的行为将类似于 RPC 调用,并且将在服务器上执行。它们可以接受任何可序列化的参数,并返回任何可序列化的值。
isServer
& isBrowser
条件
建议使用从 @builder.io/qwik/build
导出的 isServer
和 isBrowser
布尔帮助程序,而不是 if(typeof window !== 'undefined')
,以确保您的代码仅在浏览器中运行。它们包含稍微更健壮的检查,以更好地检测浏览器环境。
以下是供参考的源代码
export const isBrowser: boolean = /*#__PURE__*/ (() =>
typeof window !== 'undefined' &&
typeof HTMLElement !== 'undefined' &&
!!window.document &&
String(HTMLElement).includes('[native code]'))();
export const isServer: boolean = !isBrowser;
以下是供参考的导入方法
import {isServer, isBrowser} from '@builder.io/qwik/build';
// inside component$
useTask$(({ track }) => {
track(() => interactionSig.value) <-- tracks on the client when a signal has changed.
// server code
if (isServer) return;
// client code here
});
//