插槽

插槽允许组件将组件的 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,而无需下载和重新渲染父组件。

为了使两个组件具有独立的生命周期,投影需要是声明式的。这样,父组件或子组件都可以更改投影的内容或投影方式,而不会强制另一个组件重新渲染。

投影与 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