组件

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

Qwik 组件的独特之处在于:

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

component$()

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

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

component$ 的原因是尾部的 $ 允许 Optimizer 将组件拆分为应用程序树的单独块,以便可以独立加载每个块(如果不需要则不加载)。如果没有 $,则父组件需要加载时,组件将始终加载。

组合组件

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

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>
    </>
  );
});

属性

属性用于从父组件传递数据到子组件。属性可以通过 props 参数访问。

在此示例中,组件 Item 声明了可选的 namequantitydescriptionprice

import { component$ } from '@builder.io/qwik';
 
interface ItemProps {
  name?: string;
  quantity?: number;
  description?: string;
  price?: number;
}
 
export const Item = component$<ItemProps>((props) => {
  return (
    <ul>
      <li>name: {props.name}</li>
      <li>quantity: {props.quantity}</li>
      <li>description: {props.description}</li>
      <li>price: {props.price}</li>
    </ul>
  );
});
 
export default component$(() => {
  return (
    <>
      <h1>Props</h1>
      <Item name="hammer" price={9.99} />
    </>
  );
});

在上面的示例中,我们使用 component$<ItemProps> 为属性提供了一个明确的类型。这是可选的,但它允许 TypeScript 编译器检查属性的正确使用方式。

默认属性

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

interface Props {
  enabled?: boolean;
  placeholder?: string;
}
 
// 我们可以使用 JS 的解构来为属性提供默认值。
export default component$<Props>(({enabled = true, placeholder = ''}) => {
  return (
    <input disabled={!enabled} placeholder={placeholder} />
  );
});

响应式渲染

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

  1. 将状态绑定到 DOM 文本或属性。此类更改通常直接更新 DOM,不需要重新执行组件函数。
  2. 状态导致 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)和客户端操作中保持一致。当服务器渲染的组件需要客户端脚本时,这非常重要,例如:

  1. 动画引擎
  2. 可访问性增强
  3. 其他客户端库

在多个片段同时运行的微前端设置中,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。) 这些传递依赖关系是一个问题,因为这意味着引用根组件将传递地引用应用程序的其余部分,这是 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;

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

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>
);

注意 为简单起见,未显示所有转换;为简洁起见,所有生成的符号都保留在同一个文件中。

请注意,Optimizer 转换代码后,Parent 不再直接引用 Child。这很重要,因为它允许打包工具(和树摇器)自由地将符号移动到不同的块中,而不会将应用程序的其余部分一同移动。

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

<main>
  <section>
    <!--qv--><!--/qv-->
  </section>
</main>

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

内联组件

除了具有所有懒加载属性的标准 component$(),Qwik 还支持更像传统框架中的组件的轻量级(内联)组件。

import { component$ } from '@builder.io/qwik';
 
// 内联组件:使用标准函数声明。
export const MyButton = (props: { text: string }) => {
  return <button>{props.text}</button>;
};
 
// 组件:使用 `component$()` 声明。
export default component$(() => {
  return (
    <p>
      Some text:
      <MyButton text="Click me" />
    </p>
  );
});

在上面的示例中,MyButton 是一个内联组件。 与标准的 component$() 不同,内联组件不能单独进行懒加载;而是与其父组件一起打包。在这种情况下:

  • MyButton 将与 default 组件一起打包。
  • 每当渲染 default 组件时,它也会确保 MyButton 被渲染。

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

限制

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

  • 不能使用 use* 方法,如 useSignaluseStore
  • 不能使用 <Slot> 进行内容投影。

顾名思义,内联组件最适合用于轻量级的标记片段,因为它们提供了与父组件一起打包的便利性。

API 概述

状态

样式

事件

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

任务/生命周期

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

其他

组件

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

参见

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • RATIU5
  • leifermendez
  • manucorporat
  • adamdbradley
  • cunzaizhuyi
  • shairez
  • the-r3aper7
  • zanettin
  • Craiqser
  • steve8708
  • mforncro
  • georgeiliadis91
  • leader22
  • almilo
  • estherbrunner
  • kumarasinghe
  • mhevery
  • AnthonyPAlicea
  • khalilou88
  • n8sabes