插槽

插槽允許元件將其 JSX 子元件視為一種輸入形式,並將這些子元件投影到元件的 DOM 樹中。

這個概念在不同的框架中有不同的名稱

  • 在 Angular 中稱為內容投影
  • 在 React 中,這是 props 的 children
  • 在 Web 元件中,它也是 <slot>

實現此目標的主要 API 是 <Slot> 元件,在 @builder.io/qwik 中匯出

import { Slot, component$ } from '@builder.io/qwik';
 
const Button = component$(() => {
  return (
    <button>
      Content: <Slot />
    </button>
  );
});
 
export default component$(() => {
  return (
    <Button>
      This goes inside {'<Button>'} component marked by{`<Slot>`}
    </Button>
  );
});

<Slot> 元件是元件子項的佔位符。 <Slot> 元件將在渲染期間被元件的子項替換。

注意:Qwik 中的插槽是宣告式的,允許 Qwik 單獨渲染父元件和子元件。因為插槽是宣告式的,所以子元件不能被元件讀取或轉換。

Qwik 中的插槽就像內容的指定佔位符,允許元件保持獨立並避免不必要的重新渲染。這種設定保持了靈活性和易於管理,與直接子項方法不同,在直接子項方法中,父元件的更改可能會導致頻繁且複雜的子元件更新。插槽有助於維護順暢且高效的元件結構。

在一些特殊情況下,需要根據子元件完成一些工作,並且了解父元件和子元件一起懶載入的缺點,那麼內嵌元件是另一種選擇。

命名插槽

只要 Slot 元件具有不同的 name 屬性,就可以在同一個元件中多次使用它

import { Slot, component$, useStylesScoped$ } from '@builder.io/qwik';
import CSS from './index.css?inline';
 
const Tab = component$(() => {
  useStylesScoped$(CSS);
  return (
    <section>
      <h2>
        <Slot name="title" />
      </h2>
      <div>
        <Slot /> {/* default slot */}
        <div>
          <Slot name="footer" />
        </div>
      </div>
    </section>
  );
});
 
export default component$(() => {
  return (
    <Tab>
      <div q:slot="title">Qwik</div>
      <div>A resumable framework for building instant web applications</div>
      <span q:slot="footer">made with ❤️ by </span>
      <a q:slot="footer" href="https://builder.io">
        builder.io
      </a>
    </Tab>
  );
});

現在,當使用 <Tab> 元件時,我們可以傳遞子元件並使用 q:slot 屬性指定它們應該放置在哪個插槽中

請注意

  • 如果未指定 q:slot 或它是空字串,則內容將被投影到預設 <Slot> 中,即沒有 name 屬性的 <Slot>
  • 多個 q:slot="footer" 屬性在內容投影中合併在一起。

未投影的內容

Qwik 會保留所有內容,即使未投影也是如此。這是因為內容可能會在未來投影。當投影的內容與任何 <Slot> 元件不匹配時,內容將被移動到惰性 <q:template> 元素中。

import { Slot, component$, useSignal } from '@builder.io/qwik';
 
const Accordion = component$(() => {
  const isOpen = useSignal(false);
  return (
    <div>
      <h1 onClick$={() => (isOpen.value = !isOpen.value)}>
        {isOpen.value ? '▼' : '▶︎'}
      </h1>
      {isOpen.value && <Slot />}
    </div>
  );
});
 
export default component$(() => {
  return (
    <Accordion>
      I am pre-rendered on the Server and hidden until needed.
    </Accordion>
  );
});

結果是

<div>
  <h1>▶︎</h1>
</div>
<q:template q:slot hidden aria-hidden="true">
  I am pre-rendered on the Server and hidden until needed.
</q:template>

請注意,未投影的內容已移至惰性 <q:template> 中。這樣做是為了防止 Accordion 元件在 <Slot> 中重新渲染。在這種情況下,我們避免了僅僅為了生成投影內容而重新渲染父元件。通過在初始渲染父元件時保留未投影的內容,兩個元件的渲染可以保持獨立。

無效的投影

q:slot 屬性必須是元件的直接子項。

import { component$ } from '@builder.io/qwik';
 
export const Project = component$(() => { ... })
 
export const MyApp = component$(() => {
  return (
    <Project>
      <span q:slot="title">ok, direct child of Project</span>
      <div>
        <span q:slot="title">Error, not a direct child of Project</span>
      </div>
    </Project>
  );
});

進階範例

一個可折疊元件的範例,該元件有條件地投影可編輯的內容。

import { Slot, component$, useSignal } from '@builder.io/qwik';
 
export const Collapsible = component$(() => {
  const isOpen = useSignal(true);
 
  return (
    <div>
      <h1 onClick$={() => (isOpen.value = !isOpen.value)}>
        {isOpen.value ? '▼' : '▶︎'}
        <Slot name="title" />
      </h1>
      {isOpen.value && <Slot />}
    </div>
  );
});
 
export default component$(() => {
  const title = useSignal('Qwik');
  const description = useSignal(
    'A resumable framework for building instant web applications'
  );
  return (
    <>
      <label>Title</label>
      <input bind:value={title} type="text" />
      <label>Description</label>
      <textarea bind:value={description} cols={50} />
      <hr />
      <Collapsible>
        <span q:slot="title">{title}</span>
        {description}
      </Collapsible>
    </>
  );
});

Collapsible 元件將始終顯示標題,但只有在 store.isOpentrue 時才會顯示文字主體。

此外,投影內容的 titledescription 是可編輯的。

  • 父元件需要能夠在不強制 Collapsible 元件重新渲染的情況下更改內容。
  • 子元件需要在不重新渲染父元件的情況下更改投影的內容。在我們的例子中,Collapsible 應該能夠顯示/隱藏預設的 q:slot,而無需下載和重新渲染父元件。

為了讓兩個元件具有獨立的生命週期,投影需要是宣告式的。這樣,父元件或子元件都可以更改投影的內容或投影的方式,而無需強制重新渲染另一個元件。

投影 vs children

所有框架都需要一種方法讓組件能夠根據條件包裹其複雜的內容。這個問題可以用許多不同的方式解決,但主要有兩種方法

  • 投影:投影是一種宣告式的方式,用於描述內容如何從父模板到達需要投影的位置。
  • childrenchildren 是指將內容視為另一個輸入的 vDOM 方法。

這兩種方法可以最好地描述為宣告式與指令式。它們都有各自的優缺點。

Qwik 使用宣告式投影方法,因為它需要能夠獨立於彼此渲染父組件和子組件。在指令式方法中,子組件可以通過無數種方式修改 children。如果子組件依賴於其 children,則每次父組件重新渲染以重新應用更改時,它都將被迫重新渲染。這種額外的渲染與 Qwik 的組件隔離渲染目標相矛盾。

注意:請確保在 component$() 函數中使用 <Slot /> 以確保其正常工作。<Slot /> 無法在內嵌組件中運行,內嵌組件類似於普通函數 export const MyInlineComp = () =>

進階:插槽和上下文

插槽組件可以訪問其父組件的上下文,即使它們沒有被投影。此外,如果父組件正在另一個組件內部投影 <Slot />,則插槽組件也將可以訪問該更深層組件的上下文。

但是,如果組件尚未被投影,因為 <Slot /> 是有條件地渲染的,則無法知道更深層的上下文,然後插槽組件將只看到直接父組件的上下文。

因此,最安全的做法是避免這些情況;如果您要提供上下文,請不要有條件地渲染您的 <Slot />

貢獻者

感謝所有幫助改進此文檔的貢獻者!

  • RATIU5
  • manucorporat
  • forresst
  • adamdbradley
  • cunzaizhuyi
  • zanettin
  • lbensaad
  • gabrielgrant
  • mhevery
  • jakovljevic-mladen
  • mrhoodz
  • Jemsco