元件

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

屬性 (Props)

屬性用於將資料從父組件傳遞到子組件。通過屬性傳遞的資料可以通过 component$ 函數的 props 參數訪問。

屬性是淺層不可變的,這意味著原始資料類型(字串、數字、布林值)在傳遞後不能更改。但是,引用類型(對象、數組、函數)的內部元素可以在引用本身不可變的情況下更改。

要從子組件更改父組件中的原始屬性資料,請使用信號。當在子組件中本地更新資料時,不需要信號,解構屬性並使用這些值來定義新的局部變數。

以下兩個範例顯示了一個組件 Item,它聲明了可選的 namequantitydescriptionprice 屬性。

在第一個範例中,原始資料類型通過屬性傳遞。price 屬性作為信號傳遞,並且可以從父組件更改。quantity 作為一個數值傳遞,用於在 Item 中定義一個新的信號,該信號可以在本地進行反應式更新。或者,如果數量不需要是反應式的,則可以將其定義為普通變數而不是信號。

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

在第二個範例中,屬性作為包含資料的單個詳細信息對象傳遞,而不是單獨的原始值。這允許在不使用信號的情況下對內部資料進行變更。但是,存儲資料的詳細信息對象仍然是不可變的,並且在傳遞後不能更改。

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> 投影內容。

顧名思義,內嵌組件最適合少量使用於輕量級的標記,因為它們提供了與父組件捆綁在一起的便利性。

多態組件

當您想根據 props 輸出不同類型的元素,但默認為 <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 prop 推斷類型時,這才成立,允許您指定默認類型。

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