Qwik简介
组件
Qwik组件与React组件非常相似。它们是返回JSX的函数。然而,需要使用component$(...)
,事件处理程序必须具有$
后缀,使用useSignal()
创建状态,使用class
代替className
等等。
- 组件总是使用
component$
函数声明。 - 组件可以使用
useSignal
钩子创建响应式状态。 - 事件处理程序使用
$
后缀声明。 - 对于
<input>
,在Qwik中,onChange
事件称为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;
}}
>
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;
}
// 组件总是使用`component$`函数声明。
export const MyComponent = component$(() => {
const count = useSignal(0);
// 注意事件处理程序函数周围的`$(...)`。
const inputHandler = $((event) => {
console.log('input', event.target.name, event.target.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>
Hello world
<span q:slot="end">🟩</span>
</Button>
);
});
使用钩子的规则
以use
开头的方法在Qwik中是特殊的,例如useSignal()
、useStore()
、useOn()
、useTask$()
、useLocation()
等。与React钩子非常相似。
- 它们只能在
component$
内部调用。 - 它们只能在
component$
的顶层调用,不能在条件或循环内部调用。
样式
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。
// `$`函数的有效示例。
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) => {
console.log('input', event.target.name, event.target.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<T>`和`useSignal<T>`的TypeScript定义
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
对象来使属性具有响应性。
// `useStore<T>`的TypeScript定义
// `Reactive<T>`是`T`类型的响应式版本,`T`的每个属性的行为都像`Signal<T>`一样。
export interface Reactive<T extends Record<string, any>> extends T {}
export interface StoreOptions {
// 如果`deep`为true,则存储的嵌套属性将包装在`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('Task executed before first render');
});
// 您可以创建多个任务,并且它们可以是异步的。
useTask$(async (taskContext) => {
// 由于我们希望在`page`更改时重新运行任务,
// 我们需要跟踪它。
taskContext.track(() => page.value);
console.log('Task executed before the first render AND when page changes');
console.log('Current page:', 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 {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);
// 使用`useVisibleTask$`钩子创建一个任务,它在客户端上急切地运行。
useVisibleTask$((taskContext) => {
// 由于此VisibleTask没有跟踪任何信号,它只会运行一次。
const interval = setInterval(() => {
time.value = new Date();
}, 1000);
// 当组件卸载或任务重新运行时,将调用`cleanup`函数。
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
对象具有以下属性:
/**
* `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$()
的返回值必须作为命名导出从路由文件中导出,即只能在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`钩子将返回一个包含从`routeLoader$()`返回的数据的`Signal`,当导航更改并且重新运行`routeLoader$()`时,它将重新渲染组件。
const userData = useUserData();
return (
<main>
<h1>User data</h1>
<p>User name: {userData.value.name}</p>
<p>User 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$()
函数接受一个返回Promise的函数。该Promise在服务器上解析,解析后的值传递给useCustomLoader$()
钩子。useCustomLoader$()
钩子是由routeLoader$()
函数创建的自定义钩子。useCustomLoader$()
钩子返回一个包含从routeLoader$()
函数返回的Promise的Signal
。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';
// `routeAction$()`函数用于创建在表单提交时在服务器上执行的数据加载程序。
// `routeAction$()`的返回值是一个自定义的use钩子,可以用于访问从`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`钩子将返回一个包含从`routeAction$()`返回的`value`的`ActionStore<T>`,以及其他一些属性,例如`submit()`,用于以编程方式提交表单,和`isRunning`。所有这些属性都是响应式的,当它们更改时,将重新渲染组件。
const userData = useUserUpdate();
// userData.value是从`routeAction$()`返回的值,在提交表单之前为`undefined`。
// userData.formData是提交的表单数据,在提交表单之前为`undefined`。
// userData.isRunning是一个布尔值,当表单正在提交时为true。
// userData.submit()是一个函数,可用于以编程方式提交表单。
// userData.actionPath是操作的路径,用于提交表单。
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$()
用于处理表单提交和其他操作的特殊组件,仅在服务器上执行。例如,您可以使用它将用户添加到数据库,然后重定向到用户配置文件页面。
仅在浏览器中运行代码
由于 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,因此它们不会在服务器上执行。
isServer
条件
Qwik提供了一个从@builder.io/qwik/build
导出的isServer
布尔值。您可以使用它来确保代码仅在服务器上运行。
仅在服务器上运行代码
有时您需要仅在服务器上运行代码,例如获取数据或访问数据库。为了解决这个问题,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$(() => {
// 此代码仅在服务器上运行,仅在单击按钮时运行
})}
>
Click me
</button>
<button
onClick$={() => {
// 此代码将调用服务器函数,并等待结果
const encrypted = await encryptOnServer('Hello world');
console.log(encrypted);
}}
>
Click me
</button>
</>
);
});
routeAction$()
routeAction$()
是仅在服务器上执行的特殊组件。它用于处理表单提交和其他操作。例如,您可以使用它将用户添加到数据库,然后重定向到用户配置文件页面。
routeLoader$()
routeLoader$()
是仅在服务器上执行的特殊组件。它用于获取数据,然后渲染页面。例如,您可以使用它从API获取数据,然后使用数据渲染页面。
server$((...args) => { ... })
server$()
是一种特殊的方式来声明仅在服务器上运行的函数。如果从客户端调用它们,它们将像RPC调用一样在服务器上执行。它们可以接受任何可序列化的参数,并返回任何可序列化的值。
isServer
条件
Qwik提供了一个从@builder.io/qwik/build
导出的isServer
布尔值。您可以使用它来确保代码仅在服务器上运行。