组件
组件是 Qwik 应用程序的基本构建块。它们是可重用的代码片段,可用于构建 UI。
Qwik 组件的独特之处在于
component$()
Qwik 组件是一个函数,它返回用 component$
调用包装的 JSX。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <div>Hello World!</div>;
});
component$
函数(以$
结尾)使优化器能够将组件拆分为单独的块。这允许每个块根据需要独立加载,而不是在加载父组件时加载所有组件。
注意:routes 文件夹中的 index.tsx、layout.tsx、root.tsx 和所有入口文件都需要使用 export default。对于其他组件,可以使用 export const 和 export function。
组合组件
组件可以组合在一起以创建更复杂的组件。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return (
<>
<p>Parent Text</p>
<Child />
</>
);
});
const Child = component$(() => {
return <p>Child Text</p>;
});
请注意,由于
$
符号,Qwik 组件已经延迟加载。这意味着您不需要手动动态导入子组件,Qwik 会为您完成。
计数器示例
一个稍微复杂一点的计数器示例。
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<>
<p>Count: {count.value}</p>
<button onClick$={() => count.value++}>Increment</button>
</>
);
});
属性
属性用于将数据从父组件传递到子组件。通过属性传递的数据可以通过 component$
函数的 props
参数访问。
属性是浅不可变的,这意味着原始数据类型(字符串、数字、布尔值)一旦传递就不能更改。但是,引用类型(对象、数组、函数)的内部元素可以更改,尽管引用本身是不可变的。
要在父组件中从子组件更改原始属性数据,请使用信号。在子组件中本地更新数据时,不需要信号,只需解构属性并使用这些值定义新的局部变量。
以下两个示例展示了一个组件 Item
,它声明了可选的 name
、quantity
、description
和 price
属性。
在第一个示例中,原始数据类型通过属性传递。price
属性作为信号传递,可以从父组件更改。quantity
作为数字值传递,用于在 Item
中定义一个新的信号,该信号可以本地反应式更新。或者,如果 quantity
不需要反应式,则可以将其定义为普通变量而不是信号。
import { component$, useSignal } from "@builder.io/qwik";
import type { Signal } from "@builder.io/qwik";
interface ItemProps {
name?: string;
quantity?: number;
description?: string;
price?: Signal<number>;
}
export const Item = component$<ItemProps>((props) => {
const localQuantity = useSignal(props.quantity);
return (
<ul>
<li>name: {props.name}</li>
<li>quantity: {localQuantity}</li>
<li>description: {props.description}</li>
<li>price: {props.price}</li>
</ul>
);
});
export default component$(() => {
const price = useSignal(9.99);
return (
<>
<h1>Props</h1>
<Item name="hammer" price={price} quantity={5} />
</>
);
});
在第二个示例中,属性作为包含数据的单个 details
对象传递,而不是单独的原始值。这允许在不使用信号的情况下对内部数据进行变异。但是,存储数据的 details
对象仍然是不可变的,一旦传递就不能更改。
import { component$ } from "@builder.io/qwik";
interface ItemProps {
details: {
name?: string;
quantity?: number;
description?: string;
price?: number;
};
}
export const Item = component$((props: ItemProps) => {
props.details.price = 4.99;
return (
<ul>
<li>name: {props.details.name}</li>
<li>quantity: {props.details.quantity}</li>
<li>description: {props.details.description}</li>
<li>price: {props.details.price}</li>
</ul>
);
});
export default component$(() => {
return (
<Item
details={{ name: "hammer", quantity: 5, description: "", price: 9.99 }}
/>
);
});
在上面的示例中,我们使用
component$<ItemProps>
为属性提供显式类型。这是可选的,但它允许 TypeScript 编译器检查属性是否被正确使用。
默认属性
您可以使用属性的解构模式来提供默认值。
interface Props {
enabled?: boolean;
placeholder?: string;
}
// We can use JS's destructuring of props to provide a default value.
export default component$<Props>(({enabled = true, placeholder = ''}) => {
return (
<input disabled={!enabled} placeholder={placeholder} />
);
});
在反应式上渲染
Qwik 组件是反应式的。这意味着它们会在状态更改时自动更新。有两种类型的更新
- 状态绑定到 DOM 文本或属性。此类更改通常会直接更新 DOM,并且不需要重新执行组件函数。
- 状态会导致 DOM 的结构性更改(元素被创建或删除)。此类更改需要重新执行组件函数。
需要注意的是,当状态发生变化时,您的组件函数可能会执行零次或多次,具体取决于状态绑定到的内容。因此,该函数应该是幂等的,您不应该依赖于它执行的次数。
状态更改会导致组件失效。当组件失效时,它们会被添加到失效队列中,该队列会在下一个 requestAnimationFrame
上被刷新(渲染)。这充当组件渲染的合并形式。
获取 DOM 元素
使用 ref
获取 DOM 元素。创建一个信号来存储 DOM 元素。然后将信号传递给 JSX 的 ref
属性。
获取对 DOM 元素的引用对于计算元素大小 (
getBoundingClientRect
)、计算样式、初始化 WebGL 画布,甚至连接一些直接与 DOM 元素交互的第三方库非常有用。
import { component$, useVisibleTask$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const width = useSignal(0);
const height = useSignal(0);
const outputRef = useSignal<Element>();
useVisibleTask$(() => {
if (outputRef.value) {
const rect = outputRef.value.getBoundingClientRect();
width.value = Math.round(rect.width);
height.value = Math.round(rect.height);
}
});
return (
<section>
<article
ref={outputRef}
style={{ border: '1px solid red', width: '100px' }}
>
Change text value here to stretch the box.
</article>
<p>
The above red box is {height.value} pixels high and {width.value}{' '}
pixels wide.
</p>
</section>
);
});
id
在服务器和客户端环境中访问元素
通过 在服务器和客户端环境中,有时需要通过其 id
访问元素。使用 useId()
函数获取当前组件的唯一标识符,该标识符在服务器端渲染 (SSR) 和客户端操作中保持一致。当服务器端渲染的组件需要客户端脚本时,这一点至关重要,例如
- 动画引擎
- 辅助功能增强
- 其他客户端库
在微前端设置中,多个片段同时运行,useId()
确保在执行环境中具有唯一且一致的 ID,从而消除冲突。
import {
component$,
useId,
useSignal,
useVisibleTask$,
} from '@builder.io/qwik';
export default component$(() => {
const elemIdSignal = useSignal<string | null>(null);
const id = useId();
const elemId = `${id}-example`;
console.log('server-side id:', elemId);
useVisibleTask$(() => {
const elem = document.getElementById(elemId);
elemIdSignal.value = elem?.getAttribute('id') || null;
console.log('client-side id:', elemIdSignal.value);
});
return (
<section>
<div id={elemId}>
Both server-side and client-side console should match this id:
<br />
<b>{elemIdSignal.value || null}</b>
</div>
</section>
);
});
延迟加载
该组件在将父子关系分解以用于捆绑目的时也起着重要作用。
export const Child = () => <span>child</span>;
const Parent = () => (
<section>
<Child />
</section>
);
在上面的示例中,引用 Parent
组件意味着对 Child
组件的传递引用。当捆绑器创建块时,对 Parent
的引用也需要捆绑 Child
。(Parent
在内部引用 Child
。)这些传递依赖关系是一个问题,因为它意味着对根组件的引用将传递地引用应用程序的其余部分——这是 Qwik 试图明确避免的。
为了避免上述问题,我们不直接引用组件,而是引用延迟包装器。这是由 component$()
函数自动创建的。
import { component$ } from '@builder.io/qwik';
export const Child = component$(() => {
return <p>child</p>;
});
export const Parent = component$(() => {
return (
<section>
<Child />
</section>
);
});
export default Parent;
在上面的示例中,优化器将上述内容转换为
const Child = componentQrl(qrl('./chunk-a', 'Child_onMount'));
const Parent = componentQrl(qrl('./chunk-b', 'Parent_onMount'));
const Parent_onMount = () => qrl('./chunk-c', 'Parent_onRender');
const Parent_onRender = () => (
<section>
<Child />
</section>
);
注意 为了简单起见,并非所有转换都显示出来;所有生成的符号都保留在同一个文件中,以保持简洁。
请注意,在优化器转换代码后,Parent
不再直接引用 Child
。这很重要,因为它允许捆绑器(和树摇器)自由地将符号移动到不同的块中,而不会将应用程序的其余部分也拉进来。
那么,当 Parent
组件需要渲染 Child
组件,但 Child
组件尚未下载时会发生什么?首先,Parent
组件像这样渲染其 DOM。
<main>
<section>
<!--qv--><!--/qv-->
</section>
</main>
如您在上面的示例中看到的,<!--qv-->
充当标记,Child
组件将在延迟加载后插入到该标记中。
内联组件
除了具有所有延迟加载属性的标准 component$()
之外,Qwik 还支持轻量级(内联)组件,这些组件的行为更像传统框架中的组件。
import { component$ } from '@builder.io/qwik';
// Inline component: declared using a standard function.
export const MyButton = (props: { text: string }) => {
return <button>{props.text}</button>;
};
// Component: declared using `component$()`.
export default component$(() => {
return (
<p>
Some text:
<MyButton text="Click me" />
</p>
);
});
在上面的示例中,MyButton
是一个内联组件。与标准 component$()
不同,内联组件不能单独延迟加载;相反,它们与它们的父组件捆绑在一起。在这种情况下
MyButton
将与default
组件捆绑在一起。- 每当渲染
default
时,它也会保证渲染MyButton
。
您可以将内联组件视为内联到实例化它们的组件中。
限制
内联组件有一些标准 component$()
不具备的限制。内联组件
- 不能使用
use*
方法,例如useSignal
或useStore
。 - 不能使用
<Slot>
投影内容。
顾名思义,内联组件最适合谨慎地用于轻量级标记片段,因为它们提供了与父组件捆绑在一起的便利性。
多态组件
当您希望根据道具输出不同类型的元素,但默认使用 <div>
时,您可以使用类似以下内容
const Poly = component$(
<C extends string | FunctionComponent = 'div'>({
as,
...props
}: { as?: C } & PropsOf<string extends C ? 'div' : C>) => {
const Cmp = as || 'div';
return (
<Cmp {...props}>
<Slot />
</Cmp>
);
}
);
export const TestComponent = component$(() => {
// These all work with correct types
return (
<>
<Poly>Hello from a div</Poly>
<Poly as="a" href="/blog">
Blog
</Poly>
<Poly as="input" onInput$={(ev, el) => console.log(el.value)} />
<Poly as={OtherComponent} someProp />
</>
);
});
注意 string extends C
,只有当 TypeScript 无法从 as
道具推断类型时,才能使用此方法,从而允许您指定默认类型。
API 概述
状态
useSignal(initialState)
- 创建一个反应式值useStore(initialStateObject)
- 创建一个反应式对象,可用于存储状态createContextId(contextName)
- 创建一个上下文引用useContextProvider()
- 为给定上下文提供一个值useContext()
- 读取当前上下文的 value
样式
useStylesScoped$()
- 将作用域样式附加到组件useStyles$()
- 将非作用域样式附加到组件
事件
useOn()
- 以编程方式将监听器附加到当前组件useOnWindow()
- 以编程方式将监听器附加到 window 对象useOnDocument()
- 以编程方式将监听器附加到 document 对象
任务/生命周期
useTask$()
- 定义一个回调函数,该函数将在渲染之前和/或观察到的存储发生更改时调用useVisibleTask$()
- 定义一个回调函数,该函数将在客户端(浏览器)中渲染后调用useResource$()
- 创建一个资源以异步加载数据
其他
$()
- 创建一个 QRLnoSerialize()
useErrorBoundary()
组件
<Slot>
- 声明一个内容投影插槽<SSRStreamBlock>
- 声明一个流块<SSRStream>
- 声明一个流<Fragment>
- 声明一个 JSX 片段