布局组件 #

一、什么是布局组件? #

布局组件是一种特殊的 Astro 组件,用于定义页面的公共结构和样式。它通常包含:

  • HTML 文档结构
  • 公共头部(导航、样式)
  • 公共底部(页脚、脚本)
  • 插槽用于插入页面内容

布局的作用 #

text
┌─────────────────────────────────────────────────────┐
│                   布局组件作用                       │
├─────────────────────────────────────────────────────┤
│                                                     │
│  🎯 统一结构   - 所有页面共享相同的 HTML 结构        │
│                                                     │
│  🔄 代码复用   - 避免重复编写公共部分                │
│                                                     │
│  🎨 一致样式   - 保持网站视觉一致性                  │
│                                                     │
│  📦 易于维护   - 修改一处,全局生效                  │
│                                                     │
└─────────────────────────────────────────────────────┘

二、基础布局 #

2.1 创建基础布局 #

astro
---
// src/layouts/Layout.astro

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

const { 
  title, 
  description = '默认描述' 
} = Astro.props;
---

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content={description} />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

<style is:global>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  body {
    font-family: system-ui, -apple-system, sans-serif;
    line-height: 1.6;
  }
</style>

2.2 使用布局 #

astro
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
---

<Layout title="首页" description="欢迎来到我的网站">
  <h1>欢迎</h1>
  <p>这是首页内容。</p>
</Layout>

三、完整布局示例 #

3.1 带导航和页脚的布局 #

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

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

const { 
  title, 
  description = '默认描述',
  image = '/og-image.png'
} = Astro.props;

const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
    <!-- SEO Meta -->
    <meta name="description" content={description} />
    <meta name="generator" content={Astro.generator} />
    <link rel="canonical" href={canonicalURL} />
    
    <!-- Open Graph -->
    <meta property="og:type" content="website" />
    <meta property="og:url" content={canonicalURL} />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={image} />
    
    <!-- Twitter Card -->
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content={title} />
    <meta name="twitter:description" content={description} />
    <meta name="twitter:image" content={image} />
    
    <!-- Favicon -->
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    
    <title>{title}</title>
  </head>
  <body>
    <Header />
    <main>
      <slot />
    </main>
    <Footer />
  </body>
</html>

<style is:global>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  html {
    scroll-behavior: smooth;
  }

  body {
    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    line-height: 1.6;
    color: #1f2937;
    background: #fff;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }

  main {
    flex: 1;
  }
</style>

3.2 Header 组件 #

astro
---
// src/components/Header.astro

const navItems = [
  { label: '首页', href: '/' },
  { label: '博客', href: '/blog' },
  { label: '关于', href: '/about' },
];

const currentPath = Astro.url.pathname;
---

<header class="header">
  <nav class="nav container">
    <a href="/" class="logo">
      <span class="logo-text">My Site</span>
    </a>
    
    <ul class="nav-list">
      {navItems.map(item => (
        <li>
          <a 
            href={item.href}
            class:list={['nav-link', { active: currentPath === item.href }]}
          >
            {item.label}
          </a>
        </li>
      ))}
    </ul>
  </nav>
</header>

<style>
  .header {
    background: #fff;
    border-bottom: 1px solid #e5e7eb;
    position: sticky;
    top: 0;
    z-index: 100;
  }

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

  .nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 64px;
  }

  .logo {
    font-size: 1.5rem;
    font-weight: bold;
    color: #2563eb;
    text-decoration: none;
  }

  .nav-list {
    display: flex;
    list-style: none;
    gap: 1.5rem;
  }

  .nav-link {
    padding: 0.5rem 1rem;
    border-radius: 6px;
    text-decoration: none;
    color: #4b5563;
    transition: all 0.2s;
  }

  .nav-link:hover {
    background: #f3f4f6;
    color: #1f2937;
  }

  .nav-link.active {
    background: #eff6ff;
    color: #2563eb;
  }
</style>
astro
---
// src/components/Footer.astro

const year = new Date().getFullYear();

const links = [
  { label: 'GitHub', href: 'https://github.com' },
  { label: 'Twitter', href: 'https://twitter.com' },
  { label: 'Email', href: 'mailto:hello@example.com' },
];
---

<footer class="footer">
  <div class="container">
    <div class="footer-content">
      <div class="footer-info">
        <p class="copyright">&copy; {year} My Site. All rights reserved.</p>
        <p class="built-with">Built with Astro</p>
      </div>
      
      <div class="footer-links">
        {links.map(link => (
          <a 
            href={link.href}
            target="_blank"
            rel="noopener noreferrer"
            class="footer-link"
          >
            {link.label}
          </a>
        ))}
      </div>
    </div>
  </div>
</footer>

<style>
  .footer {
    background: #f9fafb;
    border-top: 1px solid #e5e7eb;
    padding: 2rem 0;
    margin-top: auto;
  }

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

  .footer-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 1rem;
  }

  .footer-info {
    color: #6b7280;
  }

  .copyright {
    font-size: 0.875rem;
  }

  .built-with {
    font-size: 0.75rem;
    margin-top: 0.25rem;
  }

  .footer-links {
    display: flex;
    gap: 1.5rem;
  }

  .footer-link {
    color: #6b7280;
    text-decoration: none;
    font-size: 0.875rem;
    transition: color 0.2s;
  }

  .footer-link:hover {
    color: #2563eb;
  }
</style>

四、布局继承 #

4.1 基础布局 #

astro
---
// src/layouts/BaseLayout.astro

interface Props {
  title: string;
}

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

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

4.2 继承布局 #

astro
---
// src/layouts/BlogLayout.astro
import BaseLayout from './BaseLayout.astro';
import Sidebar from '../components/Sidebar.astro';

interface Props {
  title: string;
  author: string;
  date: string;
}

const { title, author, date } = Astro.props;
---

<BaseLayout title={title}>
  <article class="blog-post">
    <header class="post-header">
      <h1>{title}</h1>
      <p class="meta">
        <span>作者: {author}</span>
        <span>日期: {date}</span>
      </p>
    </header>
    
    <div class="post-content">
      <slot />
    </div>
  </article>
  
  <Sidebar />
</BaseLayout>

<style>
  .blog-post {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }

  .post-header {
    margin-bottom: 2rem;
  }

  .meta {
    color: #6b7280;
    font-size: 0.875rem;
  }

  .meta span {
    margin-right: 1rem;
  }
</style>

4.3 使用继承布局 #

astro
---
// src/pages/blog/[slug].astro
import BlogLayout from '../../layouts/BlogLayout.astro';

const { slug } = Astro.params;
const post = await getPost(slug);
---

<BlogLayout 
  title={post.title}
  author={post.author}
  date={post.date}
>
  <p>{post.content}</p>
</BlogLayout>

五、多布局系统 #

5.1 不同页面类型使用不同布局 #

text
src/layouts/
├── Layout.astro         # 基础布局
├── BlogLayout.astro     # 博客布局
├── DocsLayout.astro     # 文档布局
└── AdminLayout.astro    # 管理后台布局

5.2 文档布局示例 #

astro
---
// src/layouts/DocsLayout.astro
import Layout from './Layout.astro';
import Sidebar from '../components/DocsSidebar.astro';
import TOC from '../components/TOC.astro';

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

const { title, description } = Astro.props;

const headings = Astro.props.headings || [];
---

<Layout title={title} description={description}>
  <div class="docs-container">
    <aside class="sidebar">
      <Sidebar />
    </aside>
    
    <main class="docs-main">
      <article class="docs-content">
        <slot />
      </article>
    </main>
    
    <aside class="toc">
      <TOC headings={headings} />
    </aside>
  </div>
</Layout>

<style>
  .docs-container {
    display: grid;
    grid-template-columns: 250px 1fr 200px;
    max-width: 1400px;
    margin: 0 auto;
    min-height: calc(100vh - 64px);
  }

  .sidebar {
    border-right: 1px solid #e5e7eb;
    padding: 2rem 1rem;
  }

  .docs-main {
    padding: 2rem;
  }

  .toc {
    border-left: 1px solid #e5e7eb;
    padding: 2rem 1rem;
  }

  @media (max-width: 1024px) {
    .docs-container {
      grid-template-columns: 250px 1fr;
    }
    
    .toc {
      display: none;
    }
  }

  @media (max-width: 768px) {
    .docs-container {
      grid-template-columns: 1fr;
    }
    
    .sidebar {
      display: none;
    }
  }
</style>

六、布局插槽 #

6.1 具名插槽 #

astro
---
// src/layouts/Layout.astro
---

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <slot name="head" />
  </head>
  <body>
    <slot name="header" />
    
    <main>
      <slot />
    </main>
    
    <slot name="footer" />
  </body>
</html>

6.2 使用具名插槽 #

astro
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
---

<Layout>
  <meta slot="head" name="keywords" content="astro, tutorial" />
  
  <header slot="header">
    <h1>网站标题</h1>
  </header>
  
  <p>这是主要内容。</p>
  
  <footer slot="footer">
    <p>自定义页脚</p>
  </footer>
</Layout>

七、布局最佳实践 #

7.1 布局结构建议 #

text
推荐布局结构:

src/
├── layouts/
│   ├── Layout.astro          # 基础布局
│   ├── BlogLayout.astro      # 博客布局
│   └── DocsLayout.astro      # 文档布局
├── components/
│   ├── Header.astro          # 导航头部
│   ├── Footer.astro          # 页脚
│   ├── Sidebar.astro         # 侧边栏
│   └── ...
└── pages/
    ├── index.astro
    ├── blog/
    │   └── [slug].astro
    └── docs/
        └── [...path].astro

7.2 Props 设计 #

astro
---
// 好的 Props 设计
interface Props {
  title: string;
  description?: string;
  image?: string;
  noIndex?: boolean;
}

// 避免
interface Props {
  data: any;  // 太宽泛
}

7.3 SEO 优化 #

astro
---
// src/layouts/Layout.astro

interface Props {
  title: string;
  description?: string;
  image?: string;
  canonicalURL?: string;
  noIndex?: boolean;
}

const { 
  title, 
  description,
  image,
  canonicalURL,
  noIndex = false 
} = Astro.props;

const siteTitle = 'My Site';
const fullTitle = title ? `${title} | ${siteTitle}` : siteTitle;
---

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
    {description && <meta name="description" content={description} />}
    {noIndex && <meta name="robots" content="noindex, nofollow" />}
    
    {canonicalURL && <link rel="canonical" href={canonicalURL} />}
    
    <!-- Open Graph -->
    <meta property="og:title" content={fullTitle} />
    {description && <meta property="og:description" content={description} />}
    {image && <meta property="og:image" content={image} />}
    
    <title>{fullTitle}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

八、总结 #

布局组件核心要点:

text
┌─────────────────────────────────────────────────────┐
│                 布局组件要点                         │
├─────────────────────────────────────────────────────┤
│                                                     │
│  🎯 统一结构   定义页面公共结构                      │
│                                                     │
│  🔄 代码复用   避免重复编写                          │
│                                                     │
│  📦 插槽机制   灵活插入内容                          │
│                                                     │
│  🏗️ 布局继承   多层布局嵌套                         │
│                                                     │
│  🎨 样式隔离   组件级样式管理                        │
│                                                     │
└─────────────────────────────────────────────────────┘

下一步,让我们学习 Astro配置,掌握项目配置方法!

最后更新:2026-03-28