组件

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

Qwik 组件的独特之处在于

  • 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,它声明了可选的 namequantitydescriptionprice 属性。

在第一个示例中,原始数据类型通过属性传递。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 组件是反应式的。这意味着它们会在状态更改时自动更新。有两种类型的更新

  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。(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* 方法,例如 useSignaluseStore
  • 不能使用 <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 概述

状态

样式

事件

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

任务/生命周期

  • useTask$() - 定义一个回调函数,该函数将在渲染之前和/或观察到的存储发生更改时调用
  • useVisibleTask$() - 定义一个回调函数,该函数将在客户端(浏览器)中渲染后调用
  • useResource$() - 创建一个资源以异步加载数据

其他

组件

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

另请参阅

贡献者

感谢所有帮助改进此文档的贡献者!

  • RATIU5
  • leifermendez
  • manucorporat
  • adamdbradley
  • cunzaizhuyi
  • shairez
  • the-r3aper7
  • zanettin
  • Craiqser
  • steve8708
  • mforncro
  • georgeiliadis91
  • leader22
  • almilo
  • estherbrunner
  • kumarasinghe
  • mhevery
  • AnthonyPAlicea
  • khalilou88
  • n8sabes
  • fabian-hiller
  • gioboa
  • mrhoodz
  • eecopa
  • drumnistnakano
  • maiieul
  • wmertens
  • aendel
  • jemsco