动态数据 #

一、SSR 模式概述 #

1.1 启用 SSR #

astro.config.mjs 中配置:

javascript
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/server';

export default defineConfig({
  output: 'server',
  adapter: vercel(),
});

1.2 SSR vs 静态生成 #

特性 静态生成 (SSG) 服务端渲染 (SSR)
数据获取时机 构建时 请求时
内容更新 需要重新构建 实时更新
响应速度 极快 较快
服务器需求 需要
适用场景 内容网站 动态应用

二、请求时数据获取 #

2.1 基本用法 #

在 SSR 模式下,每次请求都会执行 Frontmatter 中的代码:

astro
---
// src/pages/dashboard.astro
const response = await fetch('https://api.example.com/stats');
const stats = await response.json();
---

<div class="dashboard">
  <h1>实时统计</h1>
  <p>访问量: {stats.views}</p>
  <p>用户数: {stats.users}</p>
  <p>更新时间: {new Date().toLocaleString('zh-CN')}</p>
</div>

2.2 请求参数处理 #

astro
---
// src/pages/search.astro
const query = Astro.url.searchParams.get('q') || '';
const page = parseInt(Astro.url.searchParams.get('page') || '1');

const response = await fetch(
  `https://api.example.com/search?q=${encodeURIComponent(query)}&page=${page}`
);
const results = await response.json();
---

<div>
  <form>
    <input type="text" name="q" value={query} />
    <button type="submit">搜索</button>
  </form>
  
  {query && (
    <div>
      <p>搜索 "{query}" 的结果:</p>
      {results.items.map(item => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  )}
</div>

2.3 POST 请求处理 #

astro
---
// src/pages/api/contact.astro
if (Astro.request.method === 'POST') {
  const data = await Astro.request.json();
  
  const response = await fetch('https://api.example.com/contact', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });
  
  if (response.ok) {
    return new Response(JSON.stringify({ success: true }), {
      status: 200,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }
  
  return new Response(JSON.stringify({ error: 'Failed' }), {
    status: 500,
  });
}

return new Response('Method not allowed', { status: 405 });
---

三、混合渲染模式 #

3.1 启用混合模式 #

javascript
// astro.config.mjs
export default defineConfig({
  output: 'hybrid',
});

3.2 预渲染静态页面 #

astro
---
// src/pages/blog/index.astro
export const prerender = true;

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>

3.3 动态渲染页面 #

astro
---
// src/pages/dashboard.astro
export const prerender = false;

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

<div>
  <h1>实时数据</h1>
  <p>{stats.value}</p>
</div>

四、缓存策略 #

4.1 响应缓存 #

astro
---
// src/pages/api/data.astro
const response = await fetch('https://api.example.com/data', {
  headers: {
    'Cache-Control': 'max-age=3600',
  },
});

const data = await response.json();

return new Response(JSON.stringify(data), {
  headers: {
    'Content-Type': 'application/json',
    'Cache-Control': 'public, max-age=3600',
  },
});
---

4.2 自定义缓存 #

typescript
// src/lib/cache.ts
interface CacheEntry<T> {
  data: T;
  timestamp: number;
  ttl: number;
}

class MemoryCache {
  private cache = new Map<string, CacheEntry<any>>();

  get<T>(key: string): T | null {
    const entry = this.cache.get(key);
    
    if (!entry) return null;
    
    if (Date.now() - entry.timestamp > entry.ttl) {
      this.cache.delete(key);
      return null;
    }
    
    return entry.data;
  }

  set<T>(key: string, data: T, ttl: number): void {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      ttl,
    });
  }

  delete(key: string): void {
    this.cache.delete(key);
  }

  clear(): void {
    this.cache.clear();
  }
}

export const cache = new MemoryCache();

4.3 使用缓存 #

astro
---
// src/pages/news.astro
import { cache } from '../lib/cache';

const cacheKey = 'news-data';
const ttl = 5 * 60 * 1000; // 5分钟

let news = cache.get(cacheKey);

if (!news) {
  news = await fetch('https://api.example.com/news').then(r => r.json());
  cache.set(cacheKey, news, ttl);
}
---

<div>
  {news.map(item => (
    <article key={item.id}>{item.title}</article>
  ))}
</div>

五、Cookie 和 Session #

astro
---
// src/pages/dashboard.astro
const cookies = Astro.cookies;

const token = cookies.get('token')?.value;
const userId = cookies.get('userId')?.value;

if (!token) {
  return Astro.redirect('/login');
}

const user = await fetch(`https://api.example.com/users/${userId}`, {
  headers: {
    'Authorization': `Bearer ${token}`,
  },
}).then(r => r.json());
---

<div>
  <h1>欢迎, {user.name}</h1>
</div>
astro
---
// src/pages/login.astro
if (Astro.request.method === 'POST') {
  const { email, password } = await Astro.request.json();
  
  const response = await fetch('https://api.example.com/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  
  const { token, user } = await response.json();
  
  Astro.cookies.set('token', token, {
    path: '/',
    maxAge: 60 * 60 * 24 * 7,
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
  });
  
  Astro.cookies.set('userId', user.id, {
    path: '/',
    maxAge: 60 * 60 * 24 * 7,
  });
  
  return Astro.redirect('/dashboard');
}
---

5.3 Session 管理 #

typescript
// src/lib/session.ts
import { createHash, randomBytes } from 'crypto';

interface SessionData {
  userId: string;
  createdAt: number;
  expiresAt: number;
}

const sessions = new Map<string, SessionData>();

function generateSessionId(): string {
  return randomBytes(32).toString('hex');
}

function createSession(userId: string, ttl = 60 * 60 * 24): string {
  const sessionId = generateSessionId();
  const now = Date.now();
  
  sessions.set(sessionId, {
    userId,
    createdAt: now,
    expiresAt: now + ttl * 1000,
  });
  
  return sessionId;
}

function getSession(sessionId: string): SessionData | null {
  const session = sessions.get(sessionId);
  
  if (!session) return null;
  
  if (Date.now() > session.expiresAt) {
    sessions.delete(sessionId);
    return null;
  }
  
  return session;
}

function deleteSession(sessionId: string): void {
  sessions.delete(sessionId);
}

export { createSession, getSession, deleteSession };

六、实时数据 #

6.1 轮询数据 #

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

<div id="live-stats">
  <h1>实时统计</h1>
  <p>在线用户: <span id="online-users">{stats.onlineUsers}</span></p>
  <p>当前请求: <span id="requests">{stats.requests}</span></p>
</div>

<script>
  async function updateStats() {
    const response = await fetch('/api/stats/live');
    const stats = await response.json();
    
    document.getElementById('online-users').textContent = stats.onlineUsers;
    document.getElementById('requests').textContent = stats.requests;
  }
  
  setInterval(updateStats, 5000);
</script>

6.2 API 端点 #

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

return new Response(JSON.stringify(stats), {
  headers: {
    'Content-Type': 'application/json',
    'Cache-Control': 'no-cache',
  },
});
---

七、用户认证 #

7.1 登录流程 #

astro
---
// src/pages/login.astro
import { createSession } from '../lib/session';

const error = Astro.url.searchParams.get('error');

if (Astro.request.method === 'POST') {
  const formData = await Astro.request.formData();
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;
  
  try {
    const response = await fetch(`${import.meta.env.API_URL}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    
    if (!response.ok) {
      throw new Error('登录失败');
    }
    
    const { user } = await response.json();
    const sessionId = createSession(user.id);
    
    Astro.cookies.set('sessionId', sessionId, {
      path: '/',
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
    });
    
    return Astro.redirect('/dashboard');
  } catch (e) {
    return Astro.redirect('/login?error=1');
  }
}
---

<div class="login-form">
  {error && <p class="error">登录失败,请重试</p>}
  
  <form method="POST">
    <div>
      <label for="email">邮箱</label>
      <input type="email" name="email" required />
    </div>
    
    <div>
      <label for="password">密码</label>
      <input type="password" name="password" required />
    </div>
    
    <button type="submit">登录</button>
  </form>
</div>

7.2 受保护页面 #

astro
---
// src/pages/dashboard.astro
import { getSession } from '../lib/session';

const sessionId = Astro.cookies.get('sessionId')?.value;

if (!sessionId) {
  return Astro.redirect('/login');
}

const session = getSession(sessionId);

if (!session) {
  Astro.cookies.delete('sessionId');
  return Astro.redirect('/login');
}

const user = await fetch(`${import.meta.env.API_URL}/users/${session.userId}`).then(r => r.json());
---

<div class="dashboard">
  <h1>欢迎, {user.name}</h1>
  <p>邮箱: {user.email}</p>
  
  <form action="/logout" method="POST">
    <button type="submit">退出登录</button>
  </form>
</div>

7.3 登出处理 #

astro
---
// src/pages/logout.astro
import { deleteSession } from '../lib/session';

if (Astro.request.method === 'POST') {
  const sessionId = Astro.cookies.get('sessionId')?.value;
  
  if (sessionId) {
    deleteSession(sessionId);
  }
  
  Astro.cookies.delete('sessionId', { path: '/' });
  
  return Astro.redirect('/login');
}

return Astro.redirect('/');
---

八、表单处理 #

8.1 表单提交 #

astro
---
// src/pages/contact.astro
let success = false;
let error = '';

if (Astro.request.method === 'POST') {
  const formData = await Astro.request.formData();
  
  const data = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  };
  
  try {
    const response = await fetch(`${import.meta.env.API_URL}/contact`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    
    if (response.ok) {
      success = true;
    } else {
      error = '提交失败,请重试';
    }
  } catch (e) {
    error = '网络错误,请重试';
  }
}
---

<div class="contact-form">
  {success && (
    <div class="success">提交成功!</div>
  )}
  
  {error && (
    <div class="error">{error}</div>
  )}
  
  <form method="POST">
    <div>
      <label for="name">姓名</label>
      <input type="text" name="name" required />
    </div>
    
    <div>
      <label for="email">邮箱</label>
      <input type="email" name="email" required />
    </div>
    
    <div>
      <label for="message">留言</label>
      <textarea name="message" rows="5" required></textarea>
    </div>
    
    <button type="submit">提交</button>
  </form>
</div>

九、总结 #

动态数据核心要点:

text
┌─────────────────────────────────────────────────────┐
│                 动态数据要点                         │
├─────────────────────────────────────────────────────┤
│                                                     │
│  🔄 SSR 模式   请求时获取数据                       │
│                                                     │
│  🔀 混合渲染   静态 + 动态结合                      │
│                                                     │
│  📦 缓存策略   内存缓存、响应缓存                   │
│                                                     │
│  🍪 Cookie     存储用户状态                         │
│                                                     │
│  🔐 认证       登录、登出、会话管理                 │
│                                                     │
│  📝 表单       POST 请求处理                        │
│                                                     │
└─────────────────────────────────────────────────────┘

下一步,让我们学习 内容集合入门,掌握 Astro 的内容管理功能!

最后更新:2026-03-28