Slots

Slots 允许组件将组件的 JSX 子元素作为一种输入形式,并将这些子元素投射到组件的 DOM 树中。

这个概念在不同的框架中有不同的名称:

  • 在 Angular 中称为 Content Projection
  • 在 React 中,它是 propschildren
  • 在 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 中的 Slots 是声明式的,允许 Qwik 独立地渲染父组件和子组件。由于 Slots 是声明式的,组件无法读取或转换子元素。

为什么?因为 Qwik 需要能够独立地渲染父/子组件。使用命令式(children)的方法,子组件可以以无数种方式修改 children。如果子组件依赖于 children,那么每当父组件重新渲染以将命令式转换应用于 children 时,子组件将被迫重新渲染。额外的渲染明显违反了 Qwik 组件独立渲染的目标。

命名 Slots

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> 组件时,我们可以传递子元素,并指定它们应该放置在哪个 Slot 中,使用 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 方法。

这两种方法可以最好地描述为声明式 vs 命令式。它们都有各自的优点和缺点。

Qwik 使用了声明式投射方法。原因是 Qwik 需要能够独立地渲染父/子组件。使用命令式(children)方法,子组件可以以无数种方式修改 children。如果子组件依赖于 children,那么每当父组件重新渲染以将命令式转换应用于 children 时,子组件将被迫重新渲染。额外的渲染明显违反了 Qwik 组件独立渲染的目标。

注意:由于 Qwik 中的 Slots 是声明式的,使用 <Slot> 进行投射只有在父组件包装在 component$() 中时才起作用。如果父组件没有包装在 component$() 中,它被视为内联组件<Slot> 将不起作用。

高级:Slots 和上下文

投射的组件可以访问其父组件的上下文,即使它们没有被投射。此外,如果父组件在另一个组件内投射 <Slot />,则投射的组件也可以访问该更深层次组件的上下文。

然而,如果一个组件还没有被投射,因为 <Slot /> 是有条件地渲染的,那么就无法知道更深层次的上下文,那么投射的组件只能看到直接父组件的上下文。

因此,最安全的做法是避免这些情况;如果您提供了上下文,请不要有条件地渲染您的 <Slot />

Contributors

Thanks to all the contributors who have helped make this documentation better!

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