元件
元件是 Qwik 應用程式的基本建構模組。它們是可重複使用的程式碼片段,可用於建置 UI。
Qwik 元件的獨特之處在於
- Qwik 元件會被 優化器 自動分解成延遲載入的區塊。
- 它們是 可恢復的(元件可以在伺服器上建立並在客戶端繼續執行)。
- 它們是反應式 (reactive)的,並且獨立於頁面上的其他組件進行渲染。請參閱渲染 (rendering)。
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
,它聲明了可選的 name
、quantity
、description
和 price
屬性。
在第一個範例中,原始資料類型通過屬性傳遞。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 組件是反應式的。這意味著它們會在狀態更改時自動更新。有兩種更新:
- 狀態綁定到 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>
投影內容。
顧名思義,內嵌組件最適合少量使用於輕量級的標記,因為它們提供了與父組件捆綁在一起的便利性。
多態組件
當您想根據 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 概覽
狀態
useSignal(initialState)
- 創建一個響應式值useStore(initialStateObject)
- 創建一個可用於存儲狀態的響應式對象createContextId(contextName)
- 創建一個上下文引用useContextProvider()
- 為給定上下文提供一個值useContext()
- 讀取當前上下文的值
樣式
useStylesScoped$()
- 將作用域樣式附加到組件useStyles$()
- 將非作用域樣式附加到組件
事件
useOn()
- 以編程方式將偵聽器附加到當前組件useOnWindow()
- 以編程方式將偵聽器附加到 window 對象useOnDocument()
- 以編程方式將偵聽器附加到 document 對象
任務/生命週期
useTask$()
- 定義一個在渲染之前和/或在觀察到的存儲更改時將被調用的回調useVisibleTask$()
- 定義一個僅在客戶端(瀏覽器)中渲染後將被調用的回調useResource$()
- 創建一個資源來異步加載數據
其他
$()
- 創建一個 QRLnoSerialize()
useErrorBoundary()
組件
<Slot>
- 聲明一個內容投影槽<SSRStreamBlock>
- 聲明一個流塊<SSRStream>
- 聲明一個流<Fragment>
- 聲明一個 JSX 片段