Slots
Slots 允许组件将组件的 JSX 子元素作为一种输入形式,并将这些子元素投射到组件的 DOM 树中。
这个概念在不同的框架中有不同的名称:
- 在 Angular 中称为 Content Projection
- 在 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 中的 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.isOpen
为 true
时才会显示文本的主体。
此外,投射内容的 title
和 description
是可编辑的。
- 父组件需要能够在不强制
Collapsible
组件重新渲染的情况下更改内容。 - 子组件需要在不重新渲染父组件的情况下更改投射的内容。在我们的例子中,
Collapsible
应该能够显示/隐藏默认的q:slot
,而不需要下载和重新渲染父组件。
为了使这两个组件具有独立的生命周期,投射需要是声明式的。这样,父组件或子组件都可以更改投射的内容或投射的方式,而不会强制重新渲染另一个组件。
children
投射 vs 所有框架都需要一种方式让组件以条件方式包装其复杂内容。这个问题有很多不同的解决方法,但有两种主要的方法:
- 投射:投射是一种声明式的方式,描述了内容如何从父模板传递到需要投射的位置。
children
:children
是将内容视为另一种输入的 vDOM 方法。
这两种方法可以最好地描述为声明式 vs 命令式。它们都有各自的优点和缺点。
Qwik 使用了声明式投射方法。原因是 Qwik 需要能够独立地渲染父/子组件。使用命令式(children
)方法,子组件可以以无数种方式修改 children
。如果子组件依赖于 children
,那么每当父组件重新渲染以将命令式转换应用于 children
时,子组件将被迫重新渲染。额外的渲染明显违反了 Qwik 组件独立渲染的目标。
注意:由于 Qwik 中的 Slots 是声明式的,使用
<Slot>
进行投射只有在父组件包装在component$()
中时才起作用。如果父组件没有包装在component$()
中,它被视为内联组件,<Slot>
将不起作用。
高级:Slots 和上下文
投射的组件可以访问其父组件的上下文,即使它们没有被投射。此外,如果父组件在另一个组件内投射 <Slot />
,则投射的组件也可以访问该更深层次组件的上下文。
然而,如果一个组件还没有被投射,因为 <Slot />
是有条件地渲染的,那么就无法知道更深层次的上下文,那么投射的组件只能看到直接父组件的上下文。
因此,最安全的做法是避免这些情况;如果您提供了上下文,请不要有条件地渲染您的 <Slot />
。