组件基础 #

一、什么是 Astro 组件? #

Astro 组件是 Astro 项目的基本构建块。每个 .astro 文件都是一个独立的组件,包含 HTML、CSS 和 JavaScript。

组件特点 #

text
┌─────────────────────────────────────────────────────┐
│               Astro 组件特点                         │
├─────────────────────────────────────────────────────┤
│                                                     │
│  📦 独立封装   - HTML、CSS、JS 封装在一起            │
│                                                     │
│  🔄 可复用     - 在多个页面重复使用                  │
│                                                     │
│  📥 可配置     - 通过属性传递数据                    │
│                                                     │
│  🎨 样式隔离   - 组件样式默认作用域隔离              │
│                                                     │
│  ⚡ 零 JS      - 默认不发送 JavaScript 到客户端      │
│                                                     │
└─────────────────────────────────────────────────────┘

二、组件结构 #

2.1 基本结构 #

一个 Astro 组件由两个主要部分组成:

astro
---
// 组件脚本(Frontmatter)
// 这里写 JavaScript/TypeScript 代码
const greeting = "Hello";
---

<!-- 组件模板 -->
<!-- 这里写 HTML 模板 -->
<h1>{greeting}, World!</h1>

2.2 完整组件示例 #

astro
---
// src/components/Card.astro

// 定义 Props 类型
interface Props {
  title: string;
  description: string;
  image?: string;
}

// 解构获取属性
const { title, description, image = '/default.jpg' } = Astro.props;

// 可以在这里执行任意 JavaScript
const formattedTitle = title.toUpperCase();
---

<div class="card">
  <img src={image} alt={title} class="card-image" />
  <div class="card-content">
    <h3 class="card-title">{formattedTitle}</h3>
    <p class="card-description">{description}</p>
  </div>
</div>

<style>
  .card {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    overflow: hidden;
    transition: transform 0.2s;
  }

  .card:hover {
    transform: translateY(-4px);
  }

  .card-image {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }

  .card-content {
    padding: 1rem;
  }

  .card-title {
    font-size: 1.25rem;
    margin-bottom: 0.5rem;
  }

  .card-description {
    color: #6b7280;
    line-height: 1.6;
  }
</style>

<script>
  // 客户端 JavaScript(打包后发送到客户端)
  console.log('Card component loaded');
</script>

三、组件属性(Props) #

3.1 定义和接收属性 #

astro
---
// src/components/Button.astro

interface Props {
  text: string;
  type?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  onClick?: string;
}

const { 
  text, 
  type = 'primary', 
  disabled = false,
  onClick 
} = Astro.props;
---

<button 
  class={`btn btn-${type}`} 
  disabled={disabled}
  onclick={onClick}
>
  {text}
</button>

<style>
  .btn {
    padding: 0.5rem 1rem;
    border-radius: 4px;
    border: none;
    cursor: pointer;
    font-weight: 500;
  }

  .btn-primary {
    background: #2563eb;
    color: white;
  }

  .btn-secondary {
    background: #6b7280;
    color: white;
  }

  .btn-danger {
    background: #dc2626;
    color: white;
  }

  .btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
</style>

3.2 使用组件 #

astro
---
import Button from '../components/Button.astro';
---

<!-- 基本使用 -->
<Button text="点击我" />

<!-- 带属性使用 -->
<Button text="提交" type="primary" />
<Button text="取消" type="secondary" />
<Button text="删除" type="danger" />

<!-- 禁用状态 -->
<Button text="不可点击" disabled={true} />

3.3 属性类型验证 #

使用 TypeScript 进行类型验证:

astro
---
// src/components/UserCard.astro

interface Props {
  name: string;
  age: number;
  email: string;
  avatar?: string;
  role: 'admin' | 'user' | 'guest';
  skills: string[];
  metadata: {
    createdAt: Date;
    updatedAt: Date;
  };
}

const { 
  name, 
  age, 
  email, 
  avatar,
  role,
  skills,
  metadata 
} = Astro.props;
---

<div class="user-card">
  <img src={avatar || '/default-avatar.png'} alt={name} />
  <h2>{name}</h2>
  <p>年龄: {age}</p>
  <p>邮箱: {email}</p>
  <p>角色: {role}</p>
  <div class="skills">
    {skills.map(skill => <span class="skill">{skill}</span>)}
  </div>
</div>

四、插槽(Slots) #

4.1 基本插槽 #

astro
---
// src/components/Container.astro
---

<div class="container">
  <slot />
</div>

<style>
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 1rem;
  }
</style>

使用:

astro
---
import Container from '../components/Container.astro';
---

<Container>
  <h1>标题</h1>
  <p>内容</p>
</Container>

4.2 具名插槽 #

astro
---
// src/components/Card.astro
---

<div class="card">
  <div class="card-header">
    <slot name="header" />
  </div>
  <div class="card-body">
    <slot />
  </div>
  <div class="card-footer">
    <slot name="footer" />
  </div>
</div>

使用:

astro
---
import Card from '../components/Card.astro';
---

<Card>
  <h2 slot="header">卡片标题</h2>
  
  <p>这是卡片的主要内容。</p>
  <p>可以包含多个元素。</p>
  
  <div slot="footer">
    <button>确定</button>
    <button>取消</button>
  </div>
</Card>

4.3 插槽默认内容 #

astro
---
// src/components/Alert.astro
---

<div class="alert">
  <slot>
    <p>这是默认内容,当没有提供内容时显示。</p>
  </slot>
</div>

4.4 检查插槽内容 #

astro
---
// src/components/Modal.astro

const hasHeader = Astro.slots.has('header');
const hasFooter = Astro.slots.has('footer');
---

<div class="modal">
  {hasHeader && (
    <div class="modal-header">
      <slot name="header" />
    </div>
  )}
  
  <div class="modal-body">
    <slot />
  </div>
  
  {hasFooter && (
    <div class="modal-footer">
      <slot name="footer" />
    </div>
  )}
</div>

五、组件样式 #

5.1 Scoped 样式(默认) #

组件内的 <style> 标签默认是作用域隔离的:

astro
---
// src/components/Button.astro
---

<button class="btn">
  <slot />
</button>

<style>
  /* 只影响当前组件的 .btn */
  .btn {
    padding: 0.5rem 1rem;
    background: blue;
    color: white;
  }
</style>

5.2 全局样式 #

使用 :global()is:global

astro
---
// src/components/Button.astro
---

<button class="btn">
  <slot />
</button>

<style>
  .btn {
    /* scoped 样式 */
  }

  /* 全局样式 */
  :global(.btn-primary) {
    background: blue;
  }
</style>

<style is:global>
  /* 整个样式块都是全局的 */
  .btn-secondary {
    background: gray;
  }
</style>

5.3 CSS 变量 #

astro
---
// src/components/Button.astro
---

<button class="btn">
  <slot />
</button>

<style>
  .btn {
    --btn-bg: #2563eb;
    --btn-color: white;
    --btn-padding: 0.5rem 1rem;
    
    padding: var(--btn-padding);
    background: var(--btn-bg);
    color: var(--btn-color);
  }
</style>

使用时覆盖变量:

astro
---
import Button from '../components/Button.astro';
---

<Button style="--btn-bg: #dc2626;">删除</Button>

5.4 条件样式 #

astro
---
// src/components/Button.astro

interface Props {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
}

const { variant = 'primary', size = 'md' } = Astro.props;
---

<button class={`btn btn-${variant} btn-${size}`}>
  <slot />
</button>

<style>
  .btn {
    border: none;
    cursor: pointer;
    border-radius: 4px;
  }

  .btn-primary { background: #2563eb; color: white; }
  .btn-secondary { background: #6b7280; color: white; }
  .btn-danger { background: #dc2626; color: white; }

  .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
  .btn-md { padding: 0.5rem 1rem; font-size: 1rem; }
  .btn-lg { padding: 0.75rem 1.5rem; font-size: 1.125rem; }
</style>

六、组件组合 #

6.1 组件嵌套 #

astro
---
// src/components/Card.astro
import Button from './Button.astro';

interface Props {
  title: string;
  description: string;
}

const { title, description } = Astro.props;
---

<div class="card">
  <h3>{title}</h3>
  <p>{description}</p>
  <Button>了解更多</Button>
</div>

6.2 组件作为属性 #

astro
---
// src/components/Layout.astro
import Header from './Header.astro';
import Footer from './Footer.astro';

interface Props {
  title: string;
  header?: any;
}

const { title, header = Header } = Astro.props;
---

<!DOCTYPE html>
<html>
  <head>
    <title>{title}</title>
  </head>
  <body>
    {header()}
    <main>
      <slot />
    </main>
    <Footer />
  </body>
</html>

七、组件最佳实践 #

7.1 单一职责 #

每个组件只做一件事:

text
✅ 好的做法:
├── Button.astro       # 只负责按钮样式
├── Icon.astro         # 只负责图标显示
└── Card.astro         # 组合 Button 和 Icon

❌ 不好的做法:
└── ButtonWithIconAndCard.astro  # 做太多事情

7.2 Props 设计 #

astro
---
// ✅ 好的 Props 设计
interface Props {
  user: {
    name: string;
    email: string;
  };
  onEdit?: () => void;
}

// ❌ 不好的 Props 设计
interface Props {
  userName: string;
  userEmail: string;
  editHandler: any;
}

7.3 组件命名 #

text
✅ 好的命名:
├── UserCard.astro
├── NavigationBar.astro
├── BlogPost.astro
└── SearchInput.astro

❌ 不好的命名:
├── card.astro
├── nav.astro
├── post.astro
└── input.astro

八、总结 #

Astro 组件核心要点:

text
┌─────────────────────────────────────────────────────┐
│                 组件核心要点                         │
├─────────────────────────────────────────────────────┤
│                                                     │
│  📝 结构     Frontmatter + Template                │
│                                                     │
│  📥 Props    类型定义 + 默认值                      │
│                                                     │
│  🔲 Slots    内容分发 + 具名插槽                    │
│                                                     │
│  🎨 Styles   Scoped 默认 + 全局可选                │
│                                                     │
│  🔄 组合     组件嵌套 + 组件复用                    │
│                                                     │
└─────────────────────────────────────────────────────┘

下一步,让我们学习 页面与路由,掌握 Astro 的路由系统!

最后更新:2026-03-28