canvas-component-composability
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePrefer small, focused components over monolithic ones with many props. When
a component starts accumulating many unrelated props, it's often a sign that it
should be decomposed into smaller, composable pieces.
优先选择小巧、功能单一的组件,而非拥有大量props的单体组件。 当一个组件开始积累大量不相关的props时,通常意味着它应该被分解为更小、可组合的组件片段。
Signs a component should be decomposed
组件需要分解的信号
Consider breaking up a component when it has:
- More than 6-8 props that serve distinct purposes
- Props for elements that make sense as standalone components (breadcrumbs, titles, metadata, navigation)
- Built-in layout assumptions that limit where the component can be used
- Multiple distinct visual sections that could be reused independently
当组件出现以下情况时,考虑将其拆分:
- 超过6-8个用途不同的props
- 包含可作为独立组件的元素props(如面包屑、标题、元数据、导航)
- 内置布局假设,限制了组件的使用场景
- 多个可独立复用的不同视觉区域
Use slots for flexible composition
使用插槽实现灵活组合
Slots are the primary mechanism for composability. Instead of passing
complex data through props, use slots to let parent components accept child
components. This matches how Canvas users build pages—by placing components
inside other components.
插槽是实现组件组合性的核心机制。 不要通过props传递复杂数据,而是使用插槽让父组件接收子组件。这与Canvas用户构建页面的方式一致——将组件嵌套在其他组件内部。
Prefer slots over complex props
优先使用插槽而非复杂props
When a component needs to render variable content, use a slot instead of props
with complex structures:
jsx
// Wrong
const ResourceDetail = ({
metadata: [
{ label: "Type", value: "Report" },
{ label: "Author", value: "UNICEF" },
],
}) => (
<div>
{metadata.map((item) => (
<MetadataItem key={item.label} {...item} />
))}
</div>
);
// Correct
const ResourceMetadata = ({ items }) => (
<div className="flex flex-col gap-2">{items}</div>
);
// Usage: pass MetadataItem components through the slot
<ResourceMetadata
items={
<>
<MetadataItem label="Type" value="Report" />
<MetadataItem label="Author" value="UNICEF" />
</>
}
/>;当组件需要渲染可变内容时,使用插槽替代结构复杂的props:
jsx
// 错误示例
const ResourceDetail = ({
metadata: [
{ label: "Type", value: "Report" },
{ label: "Author", value: "UNICEF" },
],
}) => (
<div>
{metadata.map((item) => (
<MetadataItem key={item.label} {...item} />
))}
</div>
);
// 正确示例
const ResourceMetadata = ({ items }) => (
<div className="flex flex-col gap-2">{items}</div>
);
// 使用方式:通过插槽传递MetadataItem组件
<ResourceMetadata
items={
<>
<MetadataItem label="Type" value="Report" />
<MetadataItem label="Author" value="UNICEF" />
</>
}
/>;Slots enable Canvas compatibility
插槽确保Canvas兼容性
In Drupal Canvas, users compose pages by dragging components into slots. When
you design components with slots:
- Users can add, remove, or reorder child components freely
- Each child component's props can be edited independently
- The parent component doesn't need to know about child component types
在Drupal Canvas中,用户通过将组件拖入插槽来组合页面。当你使用插槽设计组件时:
- 用户可以自由添加、移除或重新排序子组件
- 每个子组件的props可独立编辑
- 父组件无需了解子组件的类型
When to use slots vs props
何时使用插槽vs props
| Use slots for | Use props for |
|---|---|
| Variable number of child components | Single, required values (text, URL) |
| Content that users should compose | Configuration options (size, color) |
| Complex nested structures | Simple data (strings, booleans) |
| Content that varies between instances | Content consistent across instances |
| 适合使用插槽的场景 | 适合使用props的场景 |
|---|---|
| 子组件数量可变 | 单一必填值(文本、URL) |
| 用户需要自行组合的内容 | 配置选项(尺寸、颜色) |
| 复杂的嵌套结构 | 简单数据(字符串、布尔值) |
| 不同实例间内容有差异的场景 | 所有实例内容一致的场景 |
Declare slots in component.yml
在component.yml中声明插槽
Every slot must be declared in the component's :
component.ymlyaml
slots:
content:
title: Content
sidebar:
title: SidebarIn the JSX, slots are received as props and rendered directly:
jsx
const TwoColumnLayout = ({ content, sidebar }) => (
<div className="grid grid-cols-[1fr_300px] gap-8">
<div>{content}</div>
<aside>{sidebar}</aside>
</div>
);每个插槽必须在组件的中声明:
component.ymlyaml
slots:
content:
title: Content
sidebar:
title: Sidebar在JSX中,插槽会作为props接收并直接渲染:
jsx
const TwoColumnLayout = ({ content, sidebar }) => (
<div className="grid grid-cols-[1fr_300px] gap-8">
<div>{content}</div>
<aside>{sidebar}</aside>
</div>
);Common decomposition patterns
常见的组件分解模式
Page-level elements should be separate components
页面级元素应作为独立组件
Elements that appear on many pages but aren't always needed together should be
separate components:
jsx
// Wrong
const ResourceDetail = ({
breadcrumbItems,
title,
date,
taxonomyTag,
coverImage,
downloadButtonUrl,
metadata,
description,
}) => (
<div>
<Breadcrumb items={breadcrumbItems} />
<Heading text={title} element="h1" />
{/* ... */}
</div>
);
// Correct
const ResourceDetailPage = () => (
<PageLayout>
<Section width="wide" content={<Breadcrumb items={breadcrumbItems} />} />
<Section width="wide" content={<Heading text={title} element="h1" />} />
<Section width="wide" content={<ArticleMeta date="May 2023" taxonomyTag="Climate" />} />
<Section width="wide" content={/* ... */} />
</PageLayout>
);出现在多个页面但并非总是同时需要的元素,应作为独立组件:
jsx
// 错误示例
const ResourceDetail = ({
breadcrumbItems,
title,
date,
taxonomyTag,
coverImage,
downloadButtonUrl,
metadata,
description,
}) => (
<div>
<Breadcrumb items={breadcrumbItems} />
<Heading text={title} element="h1" />
{/* ... */}
</div>
);
// 正确示例
const ResourceDetailPage = () => (
<PageLayout>
<Section width="wide" content={<Breadcrumb items={breadcrumbItems} />} />
<Section width="wide" content={<Heading text={title} element="h1" />} />
<Section width="wide" content={<ArticleMeta date="May 2023" taxonomyTag="Climate" />} />
<Section width="wide" content={/* ... */} />
</PageLayout>
);Extract repeated patterns into small components
将重复模式提取为小型组件
When you see the same combination of elements repeated, extract them:
| Pattern | Extract to component |
|---|---|
| Date + category/tag | |
| Cover image + download button | |
| Label + value pairs | |
| Icon + text link | |
当看到相同的元素组合重复出现时,将其提取为独立组件:
| 重复模式 | 提取为组件 |
|---|---|
| 日期 + 分类/标签 | |
| 封面图 + 下载按钮 | |
| 标签 + 值对 | |
| 图标 + 文本链接 | |
Use layout components instead of built-in layouts
使用布局组件而非内置布局
Don't build two-column or grid layouts into content components. Use layout
components like and compose content into them:
grid-containerjsx
// Wrong
const ResourceDetail = ({ leftContent, rightContent }) => (
<div className="flex gap-10">
<div className="w-[300px]">{leftContent}</div>
<div className="flex-1">{rightContent}</div>
</div>
);
// Correct
<GridContainer
layout="25-75"
gap="extra_large"
content={
<>
<ResourceCover image={coverImage} />
<div className="flex flex-col gap-5">
<ResourceMetadata items={metadata} />
<Text text={description} />
</div>
</>
}
/>;不要在内容组件中内置两栏或网格布局,应使用等布局组件,将内容组合到其中:
grid-containerjsx
// 错误示例
const ResourceDetail = ({ leftContent, rightContent }) => (
<div className="flex gap-10">
<div className="w-[300px]">{leftContent}</div>
<div className="flex-1">{rightContent}</div>
</div>
);
// 正确示例
<GridContainer
layout="25-75"
gap="extra_large"
content={
<>
<ResourceCover image={coverImage} />
<div className="flex flex-col gap-5">
<ResourceMetadata items={metadata} />
<Text text={description} />
</div>
</>
}
/>;When NOT to decompose
无需分解组件的场景
Keep components together when:
- They always appear together and never make sense separately
- They share significant internal state that would be awkward to lift up
- The visual design tightly couples them (e.g., overlapping elements, shared backgrounds)
- Decomposition would create components with only 1-2 props that aren't useful elsewhere
在以下情况下,应保持组件的完整性:
- 组件总是同时出现,单独存在没有意义
- 组件共享大量内部状态,提升状态会导致代码繁琐
- 视觉设计紧密耦合(如重叠元素、共享背景)
- 分解后会生成仅含1-2个props的组件,且无法在其他场景复用