Chapter 02: Astro Core Features

Haiyue
14min
Learning Objectives
  • Master Astro component syntax and writing conventions
  • Understand the Islands Architecture pattern
  • Learn to use Astro’s built-in components and APIs
  • Proficiently use frontmatter and props passing

Astro Component Syntax

.astro File Structure

Astro components consist of three parts:

---
// Component Script (Frontmatter)
// JavaScript/TypeScript code executed at build time
const title = "My Component";
const items = ["item1", "item2", "item3"];
---

<!-- Component Template -->
<!-- Similar to HTML, but supports JSX expressions -->
<div class="container">
  <h1>{title}</h1>
  <ul>
    {items.map(item => <li>{item}</li>)}
  </ul>
</div>

<style>
  /* Component Styles */
  /* Scoped to current component by default */
  .container {
    padding: 1rem;
    border: 1px solid #e5e7eb;
  }
</style>

Component Props and Data Passing

---
// src/components/UserCard.astro
export interface Props {
  name: string;
  email: string;
  avatar?: string;
  isActive?: boolean;
}

const { name, email, avatar, isActive = false } = Astro.props;
---

<div class="user-card" class:list={['user-card', { active: isActive }]}>
  {avatar && <img src={avatar} alt={name} class="avatar" />}
  <div class="info">
    <h3>{name}</h3>
    <p>{email}</p>
    {isActive && <span class="status">Online</span>}
  </div>
</div>

<style>
  .user-card {
    display: flex;
    align-items: center;
    padding: 1rem;
    border-radius: 8px;
    background: #f9fafb;
  }

  .user-card.active {
    background: #ecfdf5;
    border: 1px solid #10b981;
  }

  .avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    margin-right: 1rem;
  }

  .status {
    color: #10b981;
    font-weight: 500;
    font-size: 0.875rem;
  }
</style>

Using Components

---
// src/pages/users.astro
import UserCard from '../components/UserCard.astro';

const users = [
  {
    name: "John Doe",
    email: "john@example.com",
    avatar: "/avatars/john.jpg",
    isActive: true
  },
  {
    name: "Jane Smith",
    email: "jane@example.com",
    isActive: false
  }
];
---

<html>
  <head>
    <title>User List</title>
  </head>
  <body>
    <h1>User List</h1>
    <div class="users-grid">
      {users.map(user => (
        <UserCard
          name={user.name}
          email={user.email}
          avatar={user.avatar}
          isActive={user.isActive}
        />
      ))}
    </div>
  </body>
</html>

Islands Architecture Explained

What is Islands Architecture

Islands Architecture is Astro’s core innovation, allowing creation of independent interactive “islands” within static HTML pages.

🔄 正在渲染 Mermaid 图表...

Client Directives

Astro provides multiple client directives to control component hydration timing:

# Example of client directive explanations
def astro_client_directives():
    """
    Astro Client Directives Explained
    """
    directives = {
        "client:load": {
            "description": "Hydrate immediately on page load",
            "use_case": "Critical interactive components",
            "priority": "High"
        },
        "client:idle": {
            "description": "Hydrate when browser is idle",
            "use_case": "Secondary interactive components",
            "priority": "Medium"
        },
        "client:visible": {
            "description": "Hydrate when component enters viewport",
            "use_case": "Components lower on the page",
            "priority": "Low"
        },
        "client:media": {
            "description": "Hydrate when media query matches",
            "use_case": "Mobile-specific components",
            "priority": "Conditional"
        },
        "client:only": {
            "description": "Only render on client",
            "use_case": "Components depending on browser APIs",
            "priority": "Special"
        }
    }

    print("Astro Client Directives Comparison:")
    for directive, info in directives.items():
        print(f"{directive}:")
        print(f"  Description: {info['description']}")
        print(f"  Use Case: {info['use_case']}")
        print(f"  Priority: {info['priority']}")
        print()

    return directives

# Example invocation
directives_info = astro_client_directives()

Practical Application Example

---
// src/pages/dashboard.astro
import Header from '../components/Header.astro';
import Sidebar from '../components/Sidebar.astro';
import SearchWidget from '../components/SearchWidget.vue';
import Chart from '../components/Chart.react';
import Comments from '../components/Comments.svelte';
---

<html>
  <head>
    <title>Dashboard</title>
  </head>
  <body>
    <!-- Static components - No JavaScript -->
    <Header />
    <Sidebar />

    <main>
      <!-- Hydrate immediately - Search is important -->
      <SearchWidget client:load />

      <!-- Hydrate when idle - Chart is not critical -->
      <Chart client:idle data={chartData} />

      <!-- Hydrate when visible - Comments at page bottom -->
      <Comments client:visible postId="123" />

      <!-- Only hydrate on mobile -->
      <MobileMenu client:media="(max-width: 768px)" />
    </main>
  </body>
</html>

Astro Built-in Components and APIs

Built-in Components

1. <Image /> Component

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<!-- Optimized image component -->
<Image
  src={heroImage}
  alt="Hero Image"
  width={800}
  height={400}
  format="webp"
  quality={80}
/>

<!-- Remote image -->
<Image
  src="https://example.com/image.jpg"
  alt="Remote Image"
  width={300}
  height={200}
  inferSize
/>

2. <Picture /> Component

---
import { Picture } from 'astro:assets';
import responsiveImage from '../assets/responsive.jpg';
---

<Picture
  src={responsiveImage}
  alt="Responsive Image"
  widths={[400, 800, 1200]}
  sizes="(max-width: 800px) 100vw, 800px"
  formats={['avif', 'webp', 'jpg']}
/>

Astro Global Object

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

// Astro.request - Request object
const url = Astro.request.url;
const userAgent = Astro.request.headers.get('user-agent');

// Astro.params - Dynamic route parameters
const { slug } = Astro.params;

// Astro.url - Current page URL
const currentPath = Astro.url.pathname;
const searchParams = Astro.url.searchParams;

// Astro.site - Site configuration
const siteUrl = Astro.site;

// Astro.generator - Astro version info
const generator = Astro.generator;
---

<html>
  <head>
    <title>{title}</title>
    <meta name="description" content={description} />
    <meta name="generator" content={generator} />
  </head>
  <body>
    <h1>{title}</h1>
    <p>Current Path: {currentPath}</p>
    <p>User Agent: {userAgent}</p>
  </body>
</html>

Advanced Frontmatter Usage

Data Fetching

---
// src/pages/blog/[slug].astro
import { getCollection, getEntry } from 'astro:content';

// Get single entry
const { slug } = Astro.params;
const post = await getEntry('blog', slug);

if (!post) {
  return Astro.redirect('/404');
}

// Get related posts
const allPosts = await getCollection('blog');
const relatedPosts = allPosts
  .filter(p => p.id !== slug && p.data.category === post.data.category)
  .slice(0, 3);

// Dynamic imports
const { formatDate } = await import('../../../utils/date.js');
---

<html>
  <head>
    <title>{post.data.title}</title>
    <meta name="description" content={post.data.description} />
  </head>
  <body>
    <article>
      <h1>{post.data.title}</h1>
      <p class="meta">Published: {formatDate(post.data.publishDate)}</p>
      <div set:html={post.body} />
    </article>

    <aside>
      <h3>Related Posts</h3>
      <ul>
        {relatedPosts.map(related => (
          <li>
            <a href={`/blog/${related.slug}`}>
              {related.data.title}
            </a>
          </li>
        ))}
      </ul>
    </aside>
  </body>
</html>

Conditional Rendering and Error Handling

---
// src/components/WeatherWidget.astro
export interface Props {
  city: string;
}

const { city } = Astro.props;

let weatherData = null;
let error = null;

try {
  const response = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${import.meta.env.WEATHER_API_KEY}`);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  weatherData = await response.json();
} catch (err) {
  error = err.message;
  console.error(`Failed to fetch weather data: ${error}`);
}
---

<div class="weather-widget">
  <h3>Weather - {city}</h3>

  {error ? (
    <div class="error">
      <p>⚠️ Unable to fetch weather data</p>
      <p class="error-detail">{error}</p>
    </div>
  ) : weatherData ? (
    <div class="weather-info">
      <div class="temperature">
        {Math.round(weatherData.main.temp - 273.15)}°C
      </div>
      <div class="description">
        {weatherData.weather[0].description}
      </div>
      <div class="details">
        <span>Humidity: {weatherData.main.humidity}%</span>
        <span>Wind: {weatherData.wind.speed} m/s</span>
      </div>
    </div>
  ) : (
    <div class="loading">
      <p>⏳ Loading...</p>
    </div>
  )}
</div>

<style>
  .weather-widget {
    padding: 1rem;
    border-radius: 8px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
  }

  .error {
    color: #fecaca;
  }

  .error-detail {
    font-size: 0.875rem;
    opacity: 0.8;
  }

  .temperature {
    font-size: 2rem;
    font-weight: bold;
    margin: 0.5rem 0;
  }

  .details {
    display: flex;
    gap: 1rem;
    font-size: 0.875rem;
    margin-top: 0.5rem;
  }
</style>

Styles and CSS Integration

Scoped Styles

---
// src/components/Button.astro
export interface Props {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
}

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

<button
  class={`btn btn-${variant} btn-${size}`}
  disabled={disabled}
  {...Astro.props}
>
  <slot />
</button>

<style>
  .btn {
    border: none;
    border-radius: 6px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s ease;
  }

  .btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Size variants */
  .btn-sm { padding: 0.5rem 1rem; font-size: 0.875rem; }
  .btn-md { padding: 0.75rem 1.5rem; font-size: 1rem; }
  .btn-lg { padding: 1rem 2rem; font-size: 1.125rem; }

  /* Color variants */
  .btn-primary {
    background: #3b82f6;
    color: white;
  }
  .btn-primary:hover:not(:disabled) {
    background: #2563eb;
  }

  .btn-secondary {
    background: #6b7280;
    color: white;
  }
  .btn-secondary:hover:not(:disabled) {
    background: #4b5563;
  }

  .btn-danger {
    background: #ef4444;
    color: white;
  }
  .btn-danger:hover:not(:disabled) {
    background: #dc2626;
  }
</style>

Global Styles

---
// src/layouts/BaseLayout.astro
export interface Props {
  title: string;
  description?: string;
}

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

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title}</title>
    {description && <meta name="description" content={description} />}
  </head>
  <body>
    <slot />
  </body>
</html>

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

  body {
    font-family: system-ui, -apple-system, sans-serif;
    line-height: 1.6;
    color: #1f2937;
  }

  h1, h2, h3, h4, h5, h6 {
    margin-bottom: 0.5rem;
    font-weight: 600;
  }

  p {
    margin-bottom: 1rem;
  }

  a {
    color: #3b82f6;
    text-decoration: none;
  }

  a:hover {
    text-decoration: underline;
  }
</style>

Summary

In this chapter, we’ve learned in depth about Astro’s core features:

  1. Component Syntax: Three-part structure (frontmatter, template, styles)
  2. Islands Architecture: Selective hydration for performance optimization
  3. Client Directives: Control component hydration timing
  4. Built-in Components: Image, Picture and other optimized components
  5. Astro Global Object: Access request, params, URL and other information
  6. Advanced Frontmatter: Data fetching, error handling, conditional rendering
  7. Style System: Scoped and global styles
Core Philosophy

Astro’s design philosophy is “static by default, interactive as needed”. Through Islands Architecture, we can maintain excellent performance while adding interactivity where needed.

Best Practices
  • Prefer static components, only add client directives when necessary
  • Choose appropriate hydration timing (load, idle, visible)
  • Utilize Astro’s built-in components to optimize resources like images
  • Use scoped styles appropriately to avoid style conflicts