组件是 Qwik 应用程序的基本构建块。它们是可重用的代码片段,用于构建用户界面。

Qwik 组件具有以下独特之处:

  • Qwik 组件会被优化器自动拆分为延迟加载的块。
  • 它们是可恢复的(一个组件可以在服务器上创建,并在客户端上继续执行)。
  • 它们是响应式的,并且独立于页面上的其他组件进行渲染。请参阅渲染。

component$()

Qwik 组件是一个返回 JSX 的函数,其中包装在 component$ 调用中。

  1. import { component$ } from '@builder.io/qwik';
  2. export default component$(() => {
  3. return <div>Hello World!</div>;
  4. });

概览 - 图1

component$ 的原因在于尾部的 $ 允许优化器将组件拆分成一个独立的块,以便每个块可以独立加载(或者如果不需要则不加载)。如果没有 $,则如果需要加载父组件,该组件将始终被加载。

组合组件

组件可以组合在一起,以创建更复杂的组件。

  1. import { component$ } from '@builder.io/qwik';
  2. export default component$(() => {
  3. return (
  4. <>
  5. <p>父组件文本</p>
  6. <Child />
  7. </>
  8. );
  9. });
  10. const Child = component$(() => {
  11. return <p>子组件文本</p>;
  12. });

这个例子中,父组件中使用了 <Child />,将子组件嵌套在父组件中,实现了组件的复合。

概览 - 图2

请注意,由于使用了 $ 符号,Qwik 组件已经具备懒加载的特性。这意味着您无需手动动态导入子组件,Qwik 将为您处理这一切。

计数器示例

一个稍微复杂的计数器示例。

  1. import { component$, useSignal } from '@builder.io/qwik';
  2. export default component$(() => {
  3. const count = useSignal(0);
  4. return (
  5. <>
  6. <p>计数: {count.value}</p>
  7. <button onClick$={() => count.value++}>增加</button>
  8. </>
  9. );
  10. });

在这个示例中,使用了 useSignal 来跟踪计数的状态,并通过按钮点击事件来增加计数。这是一个更具互动性的组件示例。

概览 - 图3

Props

Props 用于从父组件传递数据到子组件。Props 可以通过 component$ 函数的 props 参数进行访问。

在这个示例中,一个名为 Item 的组件声明了可选的 name、quantity、description 和 price。

  1. import { component$ } from '@builder.io/qwik';
  2. interface ItemProps {
  3. name?: string;
  4. quantity?: number;
  5. description?: string;
  6. price?: number;
  7. }
  8. export const Item = component$<ItemProps>((props) => {
  9. return (
  10. <ul>
  11. <li>名称: {props.name}</li>
  12. <li>数量: {props.quantity}</li>
  13. <li>描述: {props.description}</li>
  14. <li>价格: {props.price}</li>
  15. </ul>
  16. );
  17. });
  18. export default component$(() => {
  19. return (
  20. <>
  21. <h1>Props</h1>
  22. <Item name="锤子" price={9.99} />
  23. </>
  24. );
  25. });

在这个示例中,Item 组件通过 props 接收数据,并根据这些数据渲染了一个项目列表。在调用 Item 组件时,传递了相应的 name 和 price 属性。

概览 - 图4

在上面的例子中,我们使用了 component$<ItemProps> 来为 props 明确指定类型。这是可选的,但它允许 TypeScript 编译器检查 props 是否被正确使用。

默认 Props

您可以使用解构模式与 props 一起提供默认值。

  1. interface Props {
  2. enabled?: boolean;
  3. placeholder?: string;
  4. }
  5. // 我们可以使用 JS 的解构 props 提供默认值。
  6. export default component$<Props>(({enabled = true, placeholder = ''}) => {
  7. return (
  8. <input disabled={!enabled} placeholder={placeholder} />
  9. );
  10. });

在上面的例子中,通过解构 props 的方式,我们为 enabledplaceholder 提供了默认值。这样,如果调用组件时没有传递相应的属性,将使用默认值。

响应式渲染

Qwik 组件是响应式的,这意味着它们会在状态发生变化时自动更新。有两种类型的更新:

  1. 将状态绑定到 DOM 文本或属性。这种变化通常直接更新 DOM,不需要重新执行组件函数。
  2. 将状态导致对 DOM 的结构性更改(元素被创建或删除)。这种变化需要重新执行组件函数。

需要记住的是,当状态发生变化时,您的组件函数可能会执行零次或多次,这取决于状态绑定的是什么。因此,函数应该是幂等的,并且不应该依赖于执行的次数。

状态更改会导致组件失效。当组件失效时,它们将被添加到失效队列中,该队列在下一个 requestAnimationFrame 中被刷新(渲染)。这充当了组件渲染的一种合并形式。

获取 DOM 元素引用

使用 ref 来获取 DOM 元素引用。创建一个信号来存储 DOM 元素,然后将该信号传递给 JSX 的 ref 属性。

获取对 DOM 元素的引用可能对计算元素大小(getBoundingClientRect)、计算样式、初始化 WebGL 画布,甚至连接与 DOM 元素直接交互的某些第三方库非常有用。

  1. import { component$, useVisibleTask$, useSignal } from '@builder.io/qwik';
  2. export default component$(() => {
  3. const width = useSignal(0);
  4. const height = useSignal(0);
  5. const outputRef = useSignal<Element>();
  6. useVisibleTask$(() => {
  7. if (outputRef.value) {
  8. const rect = outputRef.value.getBoundingClientRect();
  9. width.value = Math.round(rect.width);
  10. height.value = Math.round(rect.height);
  11. }
  12. });
  13. return (
  14. <section>
  15. <article
  16. ref={outputRef}
  17. style={{ border: '1px solid red', width: '100px' }}
  18. >
  19. Change text value here to stretch the box.
  20. </article>
  21. <p>
  22. The above red box is {height.value} pixels high and {width.value}{' '}
  23. pixels wide.
  24. </p>
  25. </section>
  26. );
  27. });

概览 - 图5

跨服务器和客户端环境访问元素的 id

在服务器和客户端环境中,有时需要通过它们的 id 访问元素。使用 useId() 函数可以获取当前组件的唯一标识符,该标识符在服务器端渲染 (SSR) 和客户端操作中保持一致。这在需要客户端脚本的服务器渲染组件中非常关键,例如:

  • 动画引擎
  • 辅助功能增强
  • 其他客户端库

在多个片段同时运行的微前端设置中,useId() 确保在执行环境中具有唯一且一致的 ID,消除了冲突。

  1. import {
  2. component$,
  3. useId,
  4. useSignal,
  5. useVisibleTask$,
  6. } from '@builder.io/qwik';
  7. export default component$(() => {
  8. const elemIdSignal = useSignal<string | null>(null);
  9. const id = useId();
  10. const elemId = `${id}-example`;
  11. console.log('server-side id:', elemId);
  12. useVisibleTask$(() => {
  13. const elem = document.getElementById(elemId);
  14. elemIdSignal.value = elem?.getAttribute('id') || null;
  15. console.log('client-side id:', elemIdSignal.value);
  16. });
  17. return (
  18. <section>
  19. <div id={elemId}>
  20. 服务器端和客户端的控制台应该匹配这个 id
  21. <br />
  22. <b>{elemIdSignal.value || null}</b>
  23. </div>
  24. </section>
  25. );
  26. });

在这个示例中,useId() 用于获取组件的唯一标识符,然后通过该标识符构建一个唯一的元素 id(elemId)。在服务器端,该 id 将打印到控制台,然后在客户端使用 useVisibleTask$ 进行访问,以确保在两个环境中都具有相同的 id。这对于需要在服务器和客户端之间共享标识符的场景非常有用。

概览 - 图6

懒加载

组件在为捆绑目的打破父子关系时也起着重要的作用。

  1. export const Child = () => <span>child</span>;
  2. const Parent = () => (
  3. <section>
  4. <Child />
  5. </section>
  6. );

在上面的例子中,引用 Parent 组件意味着对 Child 组件的传递引用。当捆绑器创建一个块时,对 Parent 的引用需要将 Child 一同捆绑(因为 Parent 内部引用了 Child)。这些传递依赖关系是一个问题,因为这意味着对根组件的引用将传递地引用整个应用程序,这是 Qwik 明确试图避免的。

为了避免上述问题,我们不直接引用组件,而是引用懒加载包装器。这是由 component$() 函数自动创建的。

  1. import { component$ } from '@builder.io/qwik';
  2. export const Child = component$(() => {
  3. return <p>child</p>;
  4. });
  5. export const Parent = component$(() => {
  6. return (
  7. <section>
  8. <Child />
  9. </section>
  10. );
  11. });
  12. export default Parent;

在这个例子中,Child 和 Parent 组件都被 component$ 包装,使它们成为懒加载的组件。这有助于避免捆绑器在创建块时引入不必要的传递依赖关系。

概览 - 图7

在上面的例子中,Optimizer 将上述代码转换为:

  1. const Child = componentQrl(qrl('./chunk-a', 'Child_onMount'));
  2. const Parent = componentQrl(qrl('./chunk-b', 'Parent_onMount'));
  3. const Parent_onMount = () => qrl('./chunk-c', 'Parent_onRender');
  4. const Parent_onRender = () => (
  5. <section>
  6. <Child />
  7. </section>
  8. );

注意,在 Optimizer 转换代码之后,Parent 不再直接引用 Child。这是重要的,因为它允许捆绑器(和 tree shakers)自由地将符号移动到不同的块中,而不会将整个应用程序一起拉过来。

那么当 Parent 组件需要渲染一个尚未下载的 Child 组件时会发生什么呢?首先,Parent 组件会像下面这样渲染其 DOM。

  1. <main>
  2. <section>
  3. <!--qv--><!--/qv-->
  4. </section>
  5. </main>

如上例所示,<!--qv--> 作为标记,表示 Child 组件将在懒加载后插入的位置。

内联组件

除了具有全部懒加载特性的标准 component$() 外,Qwik 还支持轻量级(内联)组件,更类似于传统框架中的组件。

  1. import { component$ } from '@builder.io/qwik';
  2. // 内联组件:使用标准函数声明。
  3. export const MyButton = (props: { text: string }) => {
  4. return <button>{props.text}</button>;
  5. };
  6. // 组件:使用 `component$()` 声明。
  7. export default component$(() => {
  8. return (
  9. <p>
  10. 一些文本:
  11. <MyButton text="点击我" />
  12. </p>
  13. );
  14. });

在上面的例子中,MyButton 是一个内联组件,使用标准函数声明。它接收一个 text 属性,并渲染一个包含该属性文本的按钮。在默认导出的组件中,使用了 component$() 声明,它包含了一个 <p> 元素和一个嵌套的 MyButton 组件。这两种方式都可以在 Qwik 中创建组件。

概览 - 图8

在上面的例子中,MyButton 是一个内联组件。与标准的 component$() 不同,内联组件无法单独进行懒加载;相反,它们会与其父组件一起捆绑。在这种情况下:

  • MyButton 将与默认组件一起捆绑。
  • 每当默认组件被渲染时,也会确保 MyButton 被渲染。

可以将内联组件视为嵌入到它们被实例化的组件中。

限制

内联组件有一些标准 component$() 不具备的限制。内联组件:

  • 不能使用像 useSignaluseStore 这样的 use* 方法。
  • 不能使用 <Slot> 进行内容复制。

顾名思义,内联组件最好仅在需要轻量级标记的情况下适度使用,因为它们提供了与父组件捆绑的便利。

API 概览

状态

  • useSignal(initialState) - 创建一个响应式值
  • useStore(initialStateObject) - 创建一个可用于存储状态的响应式对象
  • createContextId(contextName) - 创建一个上下文引用
  • useContextProvider() - 为给定的上下文提供一个值
  • useContext() - 读取当前上下文的值

样式

  • useStylesScoped$() - 将作用域样式附加到组件
  • useStyles$() - 将非作用域样式附加到组件

事件

  • useOn() - 以编程方式将监听器附加到当前组件
  • useOnWindow() - 以编程方式将监听器附加到 window 对象
  • useOnDocument() - 以编程方式将监听器附加到 document 对象

任务/生命周期

  • useTask$() - 定义一个在渲染之前和/或在监视的存储更改时将被调用的回调
  • useVisibleTask$() - 定义一个仅在客户端渲染后(浏览器)调用的回调
  • useResource$() - 创建一个资源以异步加载数据

其他

  • $('') - 创建一个 QRL
  • noSerialize()
  • useErrorBoundary()

组件

  • <Slot> - 声明内容投影插槽
  • <SSRStreamBlock> - 声明流块
  • <SSRStream> - 声明流
  • <Fragment> - 声明 JSX 片段