数据获取基础 #

一、数据获取概述 #

1.1 Astro 数据获取特点 #

Astro 在构建时获取数据,这意味着:

text
┌─────────────────────────────────────────────────────┐
│               Astro 数据获取流程                     │
├─────────────────────────────────────────────────────┤
│                                                     │
│  构建时                                             │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐        │
│  │ 数据源  │ →  │ 获取数据 │ →  │ 生成HTML │        │
│  └─────────┘    └─────────┘    └─────────┘        │
│                                                     │
│  运行时                                             │
│  ┌─────────┐                                       │
│  │ 静态HTML │ → 直接发送给用户                      │
│  └─────────┘                                       │
│                                                     │
└─────────────────────────────────────────────────────┘

1.2 数据获取位置 #

在 Astro 组件的 Frontmatter 中获取数据:

astro
---
// 在这里获取数据
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---

<!-- 使用数据 -->
<div>{data.title}</div>

二、使用 Fetch API #

2.1 基本 Fetch #

astro
---
// src/pages/posts.astro
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await response.json();
---

<div>
  {posts.map(post => (
    <article key={post.id}>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </article>
  ))}
</div>

2.2 错误处理 #

astro
---
const response = await fetch('https://api.example.com/posts');

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

const posts = await response.json();
---

<div>
  {posts.map(post => (
    <article key={post.id}>
      <h2>{post.title}</h2>
    </article>
  ))}
</div>

2.3 带请求头的 Fetch #

astro
---
const response = await fetch('https://api.example.com/posts', {
  headers: {
    'Authorization': 'Bearer your-token',
    'Content-Type': 'application/json',
  },
});

const posts = await response.json();
---

<div>
  {posts.map(post => (
    <article key={post.id}>{post.title}</article>
  ))}
</div>

2.4 封装 Fetch 函数 #

astro
---
// src/utils/api.ts
interface FetchOptions {
  headers?: Record<string, string>;
  timeout?: number;
}

async function fetchData<T>(
  url: string, 
  options: FetchOptions = {}
): Promise<T> {
  const { headers = {}, timeout = 5000 } = options;
  
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...headers,
      },
      signal: controller.signal,
    });
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return await response.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

// 使用
const posts = await fetchData<Post[]>('https://api.example.com/posts');
---

<div>
  {posts.map(post => (
    <article key={post.id}>{post.title}</article>
  ))}
</div>

三、本地 JSON 文件 #

3.1 读取本地 JSON #

json
// src/data/products.json
[
  { "id": 1, "name": "商品A", "price": 100 },
  { "id": 2, "name": "商品B", "price": 200 },
  { "id": 3, "name": "商品C", "price": 150 }
]
astro
---
// src/pages/products.astro
import products from '../data/products.json';
---

<div class="products">
  {products.map(product => (
    <div key={product.id} class="product">
      <h3>{product.name}</h3>
      <p>价格: ¥{product.price}</p>
    </div>
  ))}
</div>

3.2 使用 Glob 导入多个文件 #

astro
---
// src/pages/blog/index.astro
const posts = await Astro.glob('../content/blog/*.md');
---

<div>
  {posts.map(post => (
    <article>
      <h2>{post.frontmatter.title}</h2>
      <p>{post.frontmatter.excerpt}</p>
    </article>
  ))}
</div>

3.3 Glob 导入 JSON 文件 #

astro
---
// src/pages/products.astro
const products = await Astro.glob('../data/products/*.json');
---

<div>
  {products.map(({ default: product }) => (
    <div key={product.id}>
      <h3>{product.name}</h3>
      <p>{product.description}</p>
    </div>
  ))}
</div>

四、环境变量 #

4.1 使用环境变量 #

bash
# .env
API_URL=https://api.example.com
API_KEY=your-api-key
astro
---
const apiUrl = import.meta.env.API_URL;
const apiKey = import.meta.env.API_KEY;

const response = await fetch(`${apiUrl}/posts`, {
  headers: {
    'Authorization': `Bearer ${apiKey}`,
  },
});

const posts = await response.json();
---

<div>
  {posts.map(post => (
    <article key={post.id}>{post.title}</article>
  ))}
</div>

4.2 环境变量类型定义 #

typescript
// src/env.d.ts
interface ImportMetaEnv {
  readonly API_URL: string;
  readonly API_KEY: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

五、数据缓存 #

5.1 构建时缓存 #

Astro 默认在构建时获取数据,结果会被缓存:

astro
---
// 这个请求只在构建时执行一次
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
---

<div>
  {posts.map(post => (
    <article key={post.id}>{post.title}</article>
  ))}
</div>

5.2 自定义缓存函数 #

astro
---
// src/utils/cache.ts
const cache = new Map<string, any>();

async function cachedFetch<T>(url: string, ttl = 3600000): Promise<T> {
  const cached = cache.get(url);
  
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data;
  }
  
  const response = await fetch(url);
  const data = await response.json();
  
  cache.set(url, {
    data,
    timestamp: Date.now(),
  });
  
  return data;
}

// 使用
const posts = await cachedFetch<Post[]>('https://api.example.com/posts');
---

六、并行数据获取 #

6.1 Promise.all #

astro
---
const [posts, users, comments] = await Promise.all([
  fetch('https://api.example.com/posts').then(r => r.json()),
  fetch('https://api.example.com/users').then(r => r.json()),
  fetch('https://api.example.com/comments').then(r => r.json()),
]);
---

<div>
  <section>
    <h2>文章 ({posts.length})</h2>
    {posts.slice(0, 5).map(post => <p key={post.id}>{post.title}</p>)}
  </section>
  
  <section>
    <h2>用户 ({users.length})</h2>
    {users.slice(0, 5).map(user => <p key={user.id}>{user.name}</p>)}
  </section>
</div>

6.2 封装并行请求 #

astro
---
interface Endpoints {
  posts: string;
  users: string;
  comments: string;
}

async function fetchMultiple<T extends Record<string, string>>(
  endpoints: T
): Promise<{ [K in keyof T]: any }> {
  const entries = Object.entries(endpoints) as [keyof T, string][];
  
  const responses = await Promise.all(
    entries.map(async ([key, url]) => {
      const response = await fetch(url);
      return [key, await response.json()] as const;
    })
  );
  
  return Object.fromEntries(responses) as { [K in keyof T]: any };
}

// 使用
const data = await fetchMultiple({
  posts: 'https://api.example.com/posts',
  users: 'https://api.example.com/users',
});

console.log(data.posts); // 文章数据
console.log(data.users); // 用户数据
---

七、GraphQL API #

7.1 查询 GraphQL #

astro
---
const query = `
  query {
    posts {
      id
      title
      excerpt
      author {
        name
      }
    }
  }
`;

const response = await fetch('https://api.example.com/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ query }),
});

const { data } = await response.json();
---

<div>
  {data.posts.map(post => (
    <article key={post.id}>
      <h2>{post.title}</h2>
      <p>作者: {post.author.name}</p>
      <p>{post.excerpt}</p>
    </article>
  ))}
</div>

7.2 封装 GraphQL 客户端 #

astro
---
// src/utils/graphql.ts
async function graphql<T>(
  endpoint: string,
  query: string,
  variables?: Record<string, any>
): Promise<T> {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, variables }),
  });
  
  const { data, errors } = await response.json();
  
  if (errors) {
    throw new Error(errors[0].message);
  }
  
  return data;
}

// 使用
const data = await graphql<{ posts: Post[] }>(
  'https://api.example.com/graphql',
  `query { posts { id title } }`
);
---

八、数据转换 #

8.1 数据过滤 #

astro
---
const posts = await fetch('https://api.example.com/posts')
  .then(r => r.json())
  .then(data => data.filter(post => post.published));
---

<div>
  {posts.map(post => (
    <article key={post.id}>{post.title}</article>
  ))}
</div>

8.2 数据排序 #

astro
---
const posts = await fetch('https://api.example.com/posts')
  .then(r => r.json())
  .then(data => data.sort((a, b) => 
    new Date(b.date).getTime() - new Date(a.date).getTime()
  ));
---

<div>
  {posts.map(post => (
    <article key={post.id}>
      <time>{post.date}</time>
      <h2>{post.title}</h2>
    </article>
  ))}
</div>

8.3 数据分组 #

astro
---
const posts = await fetch('https://api.example.com/posts').then(r => r.json());

const groupedPosts = posts.reduce((acc, post) => {
  const year = new Date(post.date).getFullYear();
  if (!acc[year]) acc[year] = [];
  acc[year].push(post);
  return acc;
}, {});
---

<div>
  {Object.entries(groupedPosts).map(([year, yearPosts]) => (
    <section key={year}>
      <h2>{year}年</h2>
      {yearPosts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </section>
  ))}
</div>

九、最佳实践 #

9.1 错误处理 #

astro
---
let posts = [];
let error = null;

try {
  const response = await fetch('https://api.example.com/posts');
  if (!response.ok) throw new Error('Failed to fetch');
  posts = await response.json();
} catch (e) {
  error = e.message;
}
---

{error && <div class="error">加载失败: {error}</div>}

{posts.length > 0 && (
  <div>
    {posts.map(post => (
      <article key={post.id}>{post.title}</article>
    ))}
  </div>
)}

9.2 空数据处理 #

astro
---
const posts = await fetch('https://api.example.com/posts')
  .then(r => r.json())
  .catch(() => []);
---

{posts.length === 0 ? (
  <div class="empty">暂无文章</div>
) : (
  <div>
    {posts.map(post => (
      <article key={post.id}>{post.title}</article>
    ))}
  </div>
)}

9.3 类型安全 #

typescript
// src/types/post.ts
interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

interface User {
  id: number;
  name: string;
  email: string;
}
astro
---
import type { Post, User } from '../types';

const posts: Post[] = await fetch('https://api.example.com/posts')
  .then(r => r.json());
---

<div>
  {posts.map((post: Post) => (
    <article key={post.id}>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </article>
  ))}
</div>

十、总结 #

数据获取核心要点:

text
┌─────────────────────────────────────────────────────┐
│                 数据获取要点                         │
├─────────────────────────────────────────────────────┤
│                                                     │
│  🌐 Fetch API   构建时获取远程数据                  │
│                                                     │
│  📁 本地文件    导入 JSON/Markdown 文件             │
│                                                     │
│  📦 Glob 导入   批量导入多个文件                    │
│                                                     │
│  🔐 环境变量    安全存储敏感配置                    │
│                                                     │
│  ⚡ 并行请求    Promise.all 提升效率               │
│                                                     │
└─────────────────────────────────────────────────────┘

下一步,让我们学习 API集成,深入了解外部 API 的集成方法!

最后更新:2026-03-28