标签

Astro

Astro 是一个现代的静态站点生成器(SSG),它允许你使用多种前端框架(如React、Vue、Svelte等)构建网站,并且能够输出干净、轻量级的HTML文件,不含有客户端的 JavaScript。Astro 旨在为构建高性能网站提供最佳的开发体验和最优的加载性能。

Astro
查看更多相关内容
前端5月28日 06:34
Astro 组件的基本结构是什么?如何定义和使用 Props、插槽?Astro 是近年来增长最快的前端框架之一,其组件系统融合了服务端逻辑与客户端模板的独特设计,让开发者可以用最少的 JavaScript 构建高性能页面。本文将系统讲解 Astro 组件的三大核心结构——前置脚本、模板区域和样式作用域,以及 Props 传参与 Slots 插槽的完整用法。 ## Astro 组件的三大结构 每个 `.astro` 文件都由三个可选部分组成:前置脚本(Frontmatter)、HTML 模板和 `<style>` 样式块。理解这三部分的执行时机和作用域,是掌握 Astro 组件的基础。 ### 1. 前置脚本(Frontmatter) 用 `---` 分隔符包裹的顶部区域,是组件的"服务端大脑": ```astro --- // 这里的代码在构建时(或 SSR 请求时)执行,不会发送到浏览器 const title = "我的博客文章"; const date = new Date().toLocaleDateString(); // 支持导入其他组件 import Card from './Card.astro'; // 支持异步操作,如数据获取 const posts = await fetch('/api/posts').then(r => r.json()); --- ``` 关键要点: - 前置脚本中的代码**仅在服务端执行**,永远不会出现在客户端 bundle 中 - 可以使用完整的 JavaScript/TypeScript 语法,包括顶层 `await` - 这里定义的变量可以在下方模板中直接使用 - 无法访问浏览器 API(如 `window`、`document`) ### 2. 模板区域 紧跟在前置脚本之后的 HTML 区域,支持类 JSX 语法: ```astro <h1>{title}</h1> <p>发布于 {date}</p> <div class="posts"> {posts.map(post => ( <Card title={post.title} /> ))} </div> ``` 模板支持的表达式: | 语法 | 用途 | 示例 | |------|------|------| | `{variable}` | 变量插值 | `<h1>{title}</h1>` | | `{condition && <Comp />}` | 条件渲染 | `{isAdmin && <AdminPanel />}` | | `{a ? <A /> : <B />}` | 三元条件 | `{loggedIn ? <Dashboard /> : <Login />}` | | `{items.map(...)}` | 列表渲染 | `{posts.map(p => <Card {...p} />)}` | | `set:html={raw}` | 原始 HTML 注入 | `<div set:html={content} />` | ### 3. 样式作用域 ```astro <style> /* 默认 scoped,不会影响其他组件 */ h1 { color: #333; } /* 需要全局样式时使用 :global() */ :global(.markdown-body p) { line-height: 1.8; } </style> ``` Astro 的样式默认是作用域隔离的——每个组件的样式会自动添加唯一属性选择器,杜绝样式泄漏。如果需要影响子组件或全局,使用 `:global()` 选择器。 ## Props:组件间的数据传递 Props 是 Astro 组件接收外部数据的标准方式,通过 `Astro.props` 对象访问。 ### 基本用法 ```astro --- // Card.astro const { title, description } = Astro.props; --- <div class="card"> <h2>{title}</h2> <p>{description}</p> </div> ``` 使用组件时传入 Props: ```astro --- import Card from './Card.astro'; --- <Card title="文章标题" description="文章描述" /> ``` ### TypeScript 类型约束 为 Props 添加类型定义,可以在构建时捕获错误: ```astro --- interface Props { title: string; description?: string; // 可选属性 count?: number; } const { title, description = '暂无描述', count = 0 } = Astro.props satisfies Props; --- <h1>{title}</h1> <p>{description}</p> <span>数量: {count}</span> ``` 使用 `satisfies` 操作符既能获得类型检查,又能保留解构时的默认值推断。 ### Props 传递的最佳实践 1. **保持 Props 简单**:Props 应该是序列化安全的原始数据(字符串、数字、布尔值、简单对象),避免传递函数或复杂类实例 2. **提供默认值**:通过解构默认值为可选 Props 设定合理的 fallback 3. **使用 `...rest` 透传**:当包装组件时,用 `const { class: className, ...rest } = Astro.props` 收集并透传属性 ```astro --- // 包装组件的最佳实践 interface Props { class?: string; variant?: 'primary' | 'secondary'; } const { class: className = '', variant = 'primary', ...rest } = Astro.props satisfies Props; --- <div class={`btn btn-${variant} ${className}`} {...rest}> <slot /> </div> ``` ## Slots:组件的内容分发 如果说 Props 传递的是"数据",那么 Slots 传递的就是"内容"。Slots 让组件成为可复用的布局容器。 ### 默认插槽 ```astro --- // Layout.astro const { title } = Astro.props; --- <html> <head><title>{title}</title></head> <body> <main> <slot /> <!-- 所有子内容将渲染在这里 --> </main> </body> </html> ``` 使用时直接在组件标签内放入内容: ```astro --- import Layout from './Layout.astro'; --- <Layout title="我的页面"> <h1>页面标题</h1> <p>这些内容会出现在 <slot /> 的位置</p> </Layout> ``` ### 命名插槽 当组件需要多个内容入口时,使用命名插槽: ```astro --- // PageLayout.astro const { title } = Astro.props; --- <div class="page"> <header> <slot name="header" /> <!-- 命名插槽 --> </header> <main> <slot /> <!-- 默认插槽 --> </main> <footer> <slot name="footer" /> <!-- 命名插槽 --> </footer> </div> ``` 使用命名插槽: ```astro --- import PageLayout from './PageLayout.astro'; --- <PageLayout title="首页"> <nav slot="header"> <a href="/">首页</a> <a href="/about">关于</a> </nav> <!-- 没有 slot 属性的内容进入默认插槽 --> <h1>欢迎</h1> <p>这是主要内容</p> <p slot="footer">版权信息</p> </PageLayout> ``` ### 插槽的 Fallback 内容 插槽可以设置默认内容,当没有传入对应内容时自动显示: ```astro --- // Card.astro const { title } = Astro.props; --- <div class="card"> <h2>{title}</h2> <div class="body"> <slot> <p>暂无内容</p> <!-- Fallback:未传入内容时显示 --> </slot> </div> </div> ``` ### 插槽传递(Slot Forwarding) 在嵌套布局中,子布局可以将插槽"透传"给父布局: ```astro --- // BaseLayout.astro --- <html> <body> <slot name="head" /> <slot /> </body> </html> ``` ```astro --- // HomeLayout.astro import BaseLayout from './BaseLayout.astro'; --- <BaseLayout> <slot name="head" slot="head" /> <slot /> </BaseLayout> ``` 这样最终页面使用 `<HomeLayout>` 时,内容会正确传递到 `<BaseLayout>` 的对应插槽位置。 ### 框架组件中的 Slots Astro 支持在 React、Vue、Svelte 等框架组件中使用插槽,但各框架的接收方式不同: | 框架 | 默认插槽 | 命名插槽 | |------|---------|---------| | React / Preact / Solid | `children` prop | `slotName` 顶级 prop | | Vue | `<slot />` | `<slot name="xxx" />` | | Svelte | `<slot />` | `<slot name="xxx" />` | 注意:传给框架组件的命名插槽名会从 `kebab-case` 转为 `camelCase`(如 `slot="my-header"` 在 React 中变为 `myHeader` prop)。 ## 常见陷阱与注意事项 1. **前置脚本不等于客户端脚本**:`---` 中的代码在服务端执行,需要交互逻辑时应使用 `<script>` 标签或 `client:*` 指令 2. **模板表达式是静态的**:`{variable}` 在构建时求值,不是响应式绑定 3. **Props 无法传递函数**:Astro 组件的 Props 是序列化传递的,函数和类实例无法通过 Props 传递 4. **样式隔离是默认行为**:不要假设子组件能继承父组件的 class 样式 5. **组件默认是静态的**:需要客户端交互时,必须使用 `client:load`、`client:visible` 等水合指令 ```astro --- // 静态组件 vs 交互组件 import StaticCard from './StaticCard.astro'; // 始终静态 import InteractiveCounter from './Counter.jsx'; // 需要水合指令 --- <StaticCard title="静态内容" /> <!-- client:load = 页面加载时立即水合 --> <InteractiveCounter client:load /> <!-- client:visible = 进入视口时才水合,节省资源 --> <InteractiveCounter client:visible /> ``` ## 总结 Astro 组件的设计哲学是**默认静态、按需交互**: - **三大结构**:前置脚本处理服务端逻辑,模板渲染 HTML,样式自动隔离 - **Props**:通过 `Astro.props` 传递数据,配合 TypeScript 类型约束确保安全 - **Slots**:通过默认插槽和命名插槽实现内容分发,支持嵌套透传和跨框架使用 - **核心原则**:能静态就不动态,需要交互时使用 `client:*` 水合指令 掌握这三个核心概念,就能构建出结构清晰、性能优秀的 Astro 应用。
前端5月28日 06:28
如何在 Astro 中创建和使用 API 路由?如何处理请求和响应?Astro 的 API 路由(Server Endpoints)允许你在项目中创建服务端接口,处理 HTTP 请求并返回响应。这是 Astro 构建全栈应用的核心能力之一,面试中常考请求处理方式、SSR/SSG 模式差异、类型安全等知识点。 ## API 路由的基本原理 API 路由文件放在 `src/pages/` 目录下,文件路径即接口路径。与页面组件不同,API 路由文件使用 `.ts` 或 `.js` 扩展名,导出的是 HTTP 方法函数而非 Astro 组件。 Astro 使用 Web 标准的 `Request` 和 `Response` 对象,与 Cloudflare Workers、Deno 等运行时保持一致,这意味着你不需要学习 Express 那样的 `req`/`res` 专属 API,掌握 Fetch API 标准即可上手。 关键前提:API 路由需要服务端渲染(SSR)模式才能在请求时动态执行。如果你的项目是纯静态站点(SSG),API 路由只会在构建时执行一次。需要在 `astro.config.mjs` 中配置适配器: ```typescript import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), }); ``` ## 创建第一个 API 路由 使用 `APIRoute` 类型可以获得完整的类型提示,这是推荐的做法: ```typescript // src/pages/api/hello.ts import type { APIRoute } from 'astro'; export const GET: APIRoute = async ({ request }) => { return new Response( JSON.stringify({ message: 'Hello, World!', timestamp: Date.now() }), { status: 200, headers: { 'Content-Type': 'application/json' }, } ); }; ``` 访问 `/api/hello` 即可得到 JSON 响应。`APIRoute` 类型会自动推断 `params`、`request`、`cookies` 等参数的类型,避免手写类型声明。 ## 支持哪些 HTTP 方法 每个 API 路由文件可以导出多个 HTTP 方法函数,Astro 根据请求方法自动路由到对应函数: ```typescript // src/pages/api/users.ts import type { APIRoute } from 'astro'; export const GET: APIRoute = async ({ request, url }) => { const users = await fetchUsers(); return new Response(JSON.stringify(users), { headers: { 'Content-Type': 'application/json' }, }); }; export const POST: APIRoute = async ({ request }) => { const body = await request.json(); const newUser = await createUser(body); return new Response(JSON.stringify(newUser), { status: 201, headers: { 'Content-Type': 'application/json' }, }); }; export const DELETE: APIRoute = async ({ request }) => { const body = await request.json(); await deleteUser(body.id); return new Response(null, { status: 204 }); }; ``` 支持的导出函数名包括 `GET`、`POST`、`PUT`、`PATCH`、`DELETE`、`OPTIONS` 和 `ALL`。`ALL` 函数会在请求方法没有对应导出函数时被调用,适合做兜底处理或方法校验。 ## 动态路由参数 使用方括号语法定义动态路由参数,与页面路由的规则一致: ```typescript // src/pages/api/users/[id].ts import type { APIRoute } from 'astro'; export const GET: APIRoute = async ({ params }) => { const { id } = params; const user = await fetchUserById(id); if (!user) { return new Response( JSON.stringify({ error: 'User not found' }), { status: 404, headers: { 'Content-Type': 'application/json' } } ); } return new Response(JSON.stringify(user), { headers: { 'Content-Type': 'application/json' }, }); }; ``` 如果需要捕获多个路径段,使用剩余参数语法 `[...path].ts`,`params.path` 会得到完整的路径数组。静态模式下,动态路由必须导出 `getStaticPaths()` 来预生成路径。 ## 请求处理:获取请求体、查询参数和请求头 API 路由函数接收一个上下文对象,从中可以提取请求的所有信息: ```typescript // src/pages/api/search.ts import type { APIRoute } from 'astro'; export const POST: APIRoute = async ({ request, url, cookies }) => { try { // 请求体:根据 Content-Type 选择解析方式 const body = await request.json(); // JSON 请求体 // const formData = await request.formData(); // 表单数据 // const text = await request.text(); // 纯文本 // 查询参数 const limit = parseInt(url.searchParams.get('limit') || '10'); const page = parseInt(url.searchParams.get('page') || '1'); // 请求头 const authHeader = request.headers.get('Authorization'); const contentType = request.headers.get('Content-Type'); // Cookie const sessionToken = cookies.get('session')?.value; const results = await search(body.query, { limit, page }); return new Response(JSON.stringify({ results, page, limit }), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300', }, }); } catch (error) { return new Response( JSON.stringify({ error: 'Invalid request body' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } }; ``` 注意 `request.json()` 只能调用一次,因为 `Request.body` 是 ReadableStream,消费后不可重读。如果需要多次读取,先 `clone()` 再解析。 ## 响应构建:状态码、头信息和重定向 Astro 返回的是标准 `Response` 对象,你可以完全控制状态码、头信息和响应体: ```typescript // 成功响应 return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', }, }); // 创建资源 return new Response(JSON.stringify(newItem), { status: 201, headers: { 'Location': `/api/items/${newItem.id}` }, }); // 重定向 return Response.redirect(new URL('/api/new-path', request.url), 301); // 无内容 return new Response(null, { status: 204 }); ``` 面试中容易被问到:Astro 4+ 使用的是原生 `Response` 构造函数,不再返回 Astro 自定义的响应对象。如果你看到教程中使用 `({ body, status })` 的写法,那是 Astro 3 及更早版本的旧语法,已经废弃。 ## 身份验证与授权 API 路由中实现鉴权通常从请求头或 Cookie 中提取凭证: ```typescript // src/pages/api/admin/stats.ts import type { APIRoute } from 'astro'; export const GET: APIRoute = async ({ request, cookies }) => { // 方式一:Bearer Token const token = request.headers.get('Authorization')?.replace('Bearer ', ''); if (!token) { return new Response( JSON.stringify({ error: 'Missing authorization token' }), { status: 401, headers: { 'Content-Type': 'application/json' } } ); } const user = await verifyToken(token); if (!user) { return new Response( JSON.stringify({ error: 'Invalid or expired token' }), { status: 403, headers: { 'Content-Type': 'application/json' } } ); } // 方式二:Session Cookie(配合中间件更方便) const sessionId = cookies.get('session_id')?.value; const stats = await fetchAdminStats(user.id); return new Response(JSON.stringify(stats), { headers: { 'Content-Type': 'application/json' }, }); }; ``` 更推荐的做法是将鉴权逻辑提取到中间件(middleware)中,避免每个路由重复编写。中间件在 API 路由执行前运行,可以在 `locals` 上挂载用户信息: ```typescript // src/middleware.ts import { defineMiddleware } from 'astro:middleware'; export const onRequest = defineMiddleware(async (context, next) => { const token = context.request.headers.get('Authorization')?.replace('Bearer ', ''); if (token) { const user = await verifyToken(token); if (user) { context.locals.user = user; } } return next(); }); ``` 在 API 路由中直接读取 `context.locals.user` 即可判断身份。 ## 错误处理策略 推荐封装统一的错误处理工具,让 API 路由保持简洁: ```typescript // src/lib/api-error.ts export class ApiError extends Error { constructor( public statusCode: number, message: string, public code?: string ) { super(message); this.name = 'ApiError'; } } export function handleApiError(error: unknown): Response { if (error instanceof ApiError) { return new Response( JSON.stringify({ error: error.message, code: error.code }), { status: error.statusCode, headers: { 'Content-Type': 'application/json' } } ); } console.error('Unexpected error:', error); return new Response( JSON.stringify({ error: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); } ``` 在路由中使用: ```typescript // src/pages/api/data.ts import type { APIRoute } from 'astro'; import { ApiError, handleApiError } from '../../lib/api-error'; export const GET: APIRoute = async ({ params }) => { try { const data = await fetchData(params.id); if (!data) throw new ApiError(404, 'Data not found', 'NOT_FOUND'); return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { return handleApiError(error); } }; ``` ## CORS 跨域配置 如果你的 API 需要被其他域名的前端调用,必须处理 CORS。可以通过 OPTIONS 预检和响应头来解决: ```typescript // src/pages/api/public-data.ts import type { APIRoute } from 'astro'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', }; export const OPTIONS: APIRoute = async () => { return new Response(null, { status: 204, headers: corsHeaders }); }; export const GET: APIRoute = async ({ request }) => { const data = await fetchPublicData(); return new Response(JSON.stringify(data), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); }; ``` 更优雅的做法是在中间件中统一添加 CORS 头,避免每个路由重复定义。 ## API 路由与 Astro Actions 的区别 Astro 4.9+ 引入了 Actions,这是处理服务端逻辑的新方式。面试中经常考察两者的适用场景: **API 路由适合:** - 对外提供 REST 接口,供第三方或前端 SPA 调用 - 需要处理多种 HTTP 方法的场景 - Webhook 回调接收 - 需要自定义响应格式(非 JSON)的场景 **Actions 适合:** - 表单提交和数据变更 - 需要输入验证(Zod schema)和类型安全的场景 - 渐进增强需求——即使 JavaScript 禁用也能工作 - 组件内部的服务端调用 ```typescript // Actions 示例:带验证的表单处理 import { defineAction } from 'astro:actions'; import { z } from 'astro:schema'; export const server = { createPost: defineAction({ input: z.object({ title: z.string().min(1).max(200), content: z.string().min(1), }), handler: async (input) => { const post = await db.post.create({ data: input }); return post; }, }), }; ``` 面试要点:Actions 底层仍基于 API 路由实现,但它封装了验证、序列化和错误处理,适合大多数表单交互场景。如果你不需要 REST 语义或对外暴露接口,优先用 Actions。 ## 文件上传处理 处理文件上传需要从 `formData` 中提取文件对象,并进行类型和大小校验: ```typescript // src/pages/api/upload.ts import type { APIRoute } from 'astro'; export const POST: APIRoute = async ({ request }) => { try { const formData = await request.formData(); const file = formData.get('file') as File | null; if (!file) { return new Response( JSON.stringify({ error: 'No file provided' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!allowedTypes.includes(file.type)) { return new Response( JSON.stringify({ error: 'Unsupported file type' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { return new Response( JSON.stringify({ error: 'File exceeds 5MB limit' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } const url = await uploadToStorage(file); return new Response(JSON.stringify({ url }), { status: 201, headers: { 'Content-Type': 'application/json' }, }); } catch (error) { return new Response( JSON.stringify({ error: 'Upload failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); } }; ``` 大文件上传建议使用流式处理(`request.body` 是 ReadableStream),避免将整个文件加载到内存。 ## 数据库集成与分页查询 API 路由连接数据库时,分页是最常见的需求之一: ```typescript // src/pages/api/posts.ts import type { APIRoute } from 'astro'; import { db } from '../../lib/db'; export const GET: APIRoute = async ({ url }) => { const page = Math.max(1, parseInt(url.searchParams.get('page') || '1')); const limit = Math.min(50, Math.max(1, parseInt(url.searchParams.get('limit') || '10'))); const offset = (page - 1) * limit; const [posts, total] = await Promise.all([ db.post.findMany({ take: limit, skip: offset, orderBy: { createdAt: 'desc' }, }), db.post.count(), ]); return new Response( JSON.stringify({ posts, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }, }), { headers: { 'Content-Type': 'application/json' } } ); }; ``` 注意对 `page` 和 `limit` 做了边界处理,防止负数或过大值导致的异常查询。 ## SSG 模式下的 API 路由 静态站点生成模式下,API 路由在构建时执行,产出的 JSON 文件会被当作静态资源。这意味着动态路由需要通过 `getStaticPaths()` 声明所有可能的路径: ```typescript // src/pages/api/tags/[tag].ts import type { APIRoute } from 'astro'; export async function getStaticPaths() { const tags = await fetchAllTags(); return tags.map(tag => ({ params: { tag } })); } export const GET: APIRoute = async ({ params }) => { const posts = await fetchPostsByTag(params.tag); return new Response(JSON.stringify(posts), { headers: { 'Content-Type': 'application/json' }, }); }; ``` 如果需要运行时动态响应,必须将路由标记为按需渲染: ```typescript export const prerender = false; ``` 面试常问:SSG 模式的 API 路由本质上是构建时的数据预生成,适合数据不频繁变化的场景;SSR 模式才是真正的服务端接口,适合实时数据。搞混这两种模式是常见的错误。 ## 实战中的常见问题 **请求体解析失败怎么办?** `request.json()` 在非法 JSON 时会抛异常,必须用 try/catch 包裹。同理 `request.formData()` 在非表单请求时也会报错。 **如何实现速率限制?** Astro 本身不提供速率限制,需要自行实现或使用中间件。简单的做法是基于 IP 和时间窗口做计数: ```typescript // src/lib/rate-limit.ts const requests = new Map<string, { count: number; resetAt: number }>(); export function rateLimit(ip: string, limit = 100, windowMs = 60000): boolean { const now = Date.now(); const record = requests.get(ip); if (!record || now > record.resetAt) { requests.set(ip, { count: 1, resetAt: now + windowMs }); return true; } record.count++; return record.count <= limit; } ``` 生产环境建议用 Redis 存储计数,避免内存泄漏和分布式场景下的不一致问题。 **如何做输入验证?** 除了 Actions 内置的 Zod 验证,API 路由中也可以直接用 Zod: ```typescript import { z } from 'zod'; const CreatePostSchema = z.object({ title: z.string().min(1).max(200), content: z.string().min(1), tags: z.array(z.string()).optional(), }); export const POST: APIRoute = async ({ request }) => { const body = await request.json(); const result = CreatePostSchema.safeParse(body); if (!result.success) { return new Response( JSON.stringify({ error: 'Validation failed', details: result.error.flatten() }), { status: 422, headers: { 'Content-Type': 'application/json' } } ); } const post = await createPost(result.data); return new Response(JSON.stringify(post), { status: 201, headers: { 'Content-Type': 'application/json' }, }); }; ``` 这样做的好处是类型从验证结果中推断,不需要手动声明 `body` 的类型。 掌握 Astro API 路由的关键在于理解它是基于 Web 标准的请求响应模型,与 Express 等框架的专有 API 不同。核心知识点包括:SSR/SSG 模式选择、`APIRoute` 类型标注、中间件集成鉴权、Actions 与 API 路由的适用场景区分,以及输入验证和错误处理的最佳实践。
前端5月27日 21:13
Astro 的 Image 组件如何优化图片加载?Astro 的 Image 组件在构建时自动完成四件事:生成多尺寸响应式图片、转换现代格式(AVIF/WebP)、压缩质量、注入懒加载属性。浏览器根据 srcset 和 sizes 选择最合适的资源,避免加载冗余像素。 ## 基本用法 ```astro --- import { Image } from 'astro:assets'; import hero from '../assets/hero.jpg'; --- <Image src={hero} alt="首页横幅" widths={[400, 800, 1200]} sizes="(max-width: 768px) 100vw, 50vw" formats={['avif', 'webp', 'jpeg']} /> ``` widths 配合 sizes 让窄屏加载小图、宽屏加载大图。formats 按优先级尝试,AVIF 不可用时回退 WebP,再回退 JPEG。 ## 关键属性速查 - **widths / sizes** — 响应式断点与显示尺寸,缺一不可 - **quality** — 压缩质量,推荐 75-85,肉眼无损但体积显著降低 - **loading** — 首屏图用 eager + priority,其余默认 lazy - **format** — 输出格式,默认 WebP ## 远程图片处理 远程 URL 需在 astro.config.mjs 白名单域名,否则构建报错: ```javascript image: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }] } ``` 不确定尺寸时加 inferSize,Astro 会在构建时拉取图片获取宽高,避免 CLS 布局偏移。 ## 三个常见坑 1. **public 目录图片不走优化** — 必须放 src/assets 并 import 引入 2. **宽高缺失导致 CLS** — 本地图片自动推断,远程图片需手动指定或用 inferSize 3. **忘记配 remotePatterns** — 远程图片直接报错,排查时先检查配置 ## 追问 **Image 和 Picture 有什么区别?** Picture 生成 <picture> 元素,多格式同时输出让浏览器自行选择;Image 只输出单一最优格式。需要兼容老浏览器时用 Picture。 **构建时优化和 CDN 实时优化怎么选?** 构建时优化零运行时开销,适合静态站点;图片量大或频繁更新时,CDN 实时处理更灵活。两者可以结合使用。 **如何优化 LCP?** 首屏图片设 loading="eager" 并加 fetchpriority="high",同时用 widths 限制首屏图的最大尺寸,避免加载不必要的大图。
前端5月27日 21:10
Astro 有哪些性能优化策略?## 核心策略:零 JS 默认 + 岛屿架构 Astro 默认只输出纯 HTML,不向客户端发送任何 JavaScript。交互组件通过 `client:*` 指令按需水合,这就是岛屿架构——页面像海洋(静态 HTML),交互区域像岛屿( hydrated JS)。 选择水合指令的原则:首屏交互用 `client:load`,非关键交互用 `client:idle`,滚动可见才用 `client:visible`,响应媒体查询用 `client:media`。多数场景 `client:visible` 就够了。 ## 图片和字体优化 用 `<Image>` 组件自动生成 avif/webp 多格式、多尺寸响应式图片。首屏图设 `loading="eager" + priority`,其余全部 `loading="lazy"`。 字体用 `preload` 预加载 woff2 文件,避免 FOIT(无样式文字闪烁)阻塞渲染。 ## 数据获取和构建优化 多个异步数据请求用 `Promise.all` 并行获取,不要串行 await。利用 Astro 的内容集合类型安全地查询数据。 构建配置中设 `inlineStylesheets: 'auto'` 让小样式内联、大样式外链。Vite 的 `manualChunks` 把 vendor 代码拆分,避免单文件过大。 ## 缓存和部署 静态资源设 `Cache-Control: public, max-age=31536000, immutable`,API 响应按业务设短期缓存。部署选对适配器——Vercel 用 `@astrojs/vercel`,Cloudflare 用 `@astrojs/cloudflare`,Netlify 用 `@astrojs/netlify`,让平台做它最擅长的事。 ## 实战效果 同样内容的博客,Next.js 加载约 2.8s,Astro 约 0.9s。Astro 站点的 Core Web Vitals "Good" 比例达 60%,而 WordPress/Gatsby 仅 38%。 --- **追问:Astro 的岛屿架构和 React Server Components 的服务端组件有什么本质区别?** 岛屿架构在 HTML 层面就隔离了交互边界,非交互区域零 JS;RSC 虽然也在服务端渲染,但交互组件仍需客户端 JS bundle 整体加载,粒度更粗。简单说:Astro 岛屿 = HTML 里嵌入 JS 岛,RSC = JS 里嵌入 HTML 流。
前端5月27日 21:09
Astro 的岛屿架构(Islands Architecture)是如何工作的?client 指令有哪些类型?## 岛屿架构核心原理 Astro 默认输出纯静态 HTML,只有被 `client:*` 标记的组件才在客户端加载 JS 并水合——这些交互组件就是"岛屿",周围是静态 HTML"海洋"。 核心思路:**能静态就静态,需要交互才加载 JS。** ## 五种 client 指令 | 指令 | 水合时机 | 场景 | |---|---|---| | `client:load` | 页面加载后立即水合 | 导航栏、首屏轮播 | | `client:idle` | 浏览器空闲时(`requestIdleCallback`) | 订阅表单 | | `client:visible` | 进入视口时(`IntersectionObserver`) | 评论区 | | `client:media` | 匹配媒体查询时 | 移动端菜单 | | `client:only` | 跳过 SSR,纯客户端渲染 | 依赖浏览器 API 的组件 | 直接加在组件上即可:`<Nav client:load />`,不写 `client:*` 则只输出 HTML,零 JS。 ## 与 SPA 的区别 SPA 全量下载 JS 再水合,岛屿架构只下载被标记组件的 JS,各岛屿独立水合互不阻塞。Astro 页面客户端 JS 体积通常只有同等 SPA 的 5%。 ## 指令选择思路 首屏交互用 `client:load`,非关键用 `client:idle`,滚动可见用 `client:visible`,响应式用 `client:media`,必须依赖浏览器环境才用 `client:only`。**拿不准就不用——默认静态即最优解。** ## 追问 **Q: `client:only` 和 `client:load` 都在客户端渲染,区别是什么?** `client:load` 先 SSR 输出 HTML 再水合;`client:only` 跳过 SSR,客户端从零渲染,首屏空白闪烁,仅用于无法在 Node 运行的组件。 **Q: 岛屿架构适合所有项目吗?** 不适合。适合内容驱动型网站(博客、文档、官网)。重交互应用(在线编辑器、IM)用 SPA 更合理,因为几乎所有组件都需要交互,岛屿架构优势无法发挥。
前端5月27日 21:08
如何部署 Astro 应用到不同的平台(Vercel、Netlify、Node.js)?有哪些部署最佳实践?## 核心答案 Astro 部署分两条路线:**静态站点(SSG)直接丢到 Vercel/Netlify 就行,零配置**;SSR 应用则必须装对应平台的适配器(`@astrojs/vercel`、`@astrojs/netlify`、`@astrojs/node`),在 `astro.config.mjs` 里设 `output: 'server'` 并注册适配器。 选平台的关键判断:纯内容站选哪个都差不多,Vercel 对 SSR 支持更成熟(支持 ISR 缓存),Netlify 的 Edge Functions 延迟更低,自建服务器用 Node.js 适配器跑 `node ./dist/server/entry.mjs`。 ## 静态部署:三行命令搞定 Vercel 和 Netlify 对 SSG 项目开箱即用——连仓库、自动构建、自动部署,不需要任何适配器。GitHub Pages 也没问题,在 `astro.config.mjs` 里配好 `site` 和 `base` 就行。 ## SSR 部署的关键区别 三个平台的适配器装法一样(`npx astro add vercel/netlify/node`),但运行时差异很大: - **Vercel**:支持 ISR(增量静态再生),设 `isr: true` 可对动态页面做缓存,适合内容偶尔更新的场景 - **Netlify**:Edge Functions 跑在 Deno runtime 上,冷启动极快,但有些 Node API 用不了 - **Node.js**:分 `standalone` 和 `middleware` 两种模式——前者直接跑,后者可以嵌入 Express/Fastify ## Docker 和 CI/CD Docker 部署本质还是 Node.js 适配器:多阶段构建,builder 阶段装依赖+构建,runner 阶段只拷贝产物。CI/CD 就是标准的 checkout → install → build → deploy 流水线,各平台都有现成 Action。 ## 追问:这些你大概率会被接着问 **Q:SSG 和 SSR 能混用吗?** 能。设 `output: 'hybrid'`,默认静态,需要动态的页面加 `export const prerender = false`。 **Q:环境变量怎么区分公开和私有?** `PUBLIC_` 前缀的变量客户端可见,其他的只在服务端。别把密钥暴露到前端代码里。 **Q:部署后首屏慢怎么排查?** 先看是不是 SSR 模式下冷启动问题(Edge Functions 可缓解),再查有没有大量未优化图片,最后用 Lighthouse 跑一遍确认瓶颈在渲染还是网络。
前端5月27日 21:07
什么是 Astro 的内容集合(Content Collections)?如何使用它来管理博客文章或文档?## Astro 内容集合是什么 Astro 内容集合(Content Collections)是 Astro 内置的结构化内容管理方案,把散落在项目里的 Markdown、MDX、JSON 等文件统一收进 `src/content/` 目录,用 Zod Schema 做 frontmatter 校验,构建时自动完成类型推断和验证——写错字段名直接报编译错误,不用等到线上才发现。 ## 怎么用 两步走:定义 Schema,写内容文件。 在 `src/content/config.ts` 里声明集合: ```typescript import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ schema: z.object({ title: z.string(), date: z.coerce.date(), tags: z.array(z.string()), }), }); export const collections = { blog }; ``` 然后在 `src/content/blog/` 下创建 Markdown 文件,frontmatter 必须符合 schema 定义,否则构建失败。 页面里用 `getCollection` 批量查询,用 `getEntry` 按 slug 取单条: ```astro --- import { getCollection, getEntry } from 'astro:content'; const posts = await getCollection('blog'); const post = await getEntry('blog', 'my-post'); const { Content } = await post.render(); --- <Content /> ``` 动态路由配合 `getStaticPaths` 就能自动生成所有文章页面。 ## 追问:和直接在 pages 目录放 Markdown 有什么区别 没有内容集合时,每个 Markdown 文件就是一个路由,frontmatter 没有任何校验——`title` 拼成 `titel` 不会报错,日期格式不对也不会拦你。集合的核心价值就是**构建时校验 + 类型安全**,`getCollection` 返回的数据有完整的 TypeScript 类型,IDE 自动补全直接可用。 ## 追问:一个项目能定义多个集合吗 可以。`config.ts` 里 export 多个集合就行,博客一个、文档一个、产品数据一个,各自独立的 schema,互不干扰。数据型内容(JSON/YAML)用 `type: 'data'`,文本型(Markdown/MDX)用 `type: 'content'`。 ## 追问:内容集合有什么局限 内容集合是构建时处理的,不支持运行时动态添加内容。如果你的站点需要用户投稿或实时更新内容,得搭配 Headless CMS 或数据库,集合只负责静态内容的类型安全。
前端5月27日 21:06
如何在 Astro 中实现国际化(i18n)?## 核心答案 Astro 从 4.0 开始内置了 i18n 路由支持,不需要第三方库就能实现多语言网站。在 `astro.config.mjs` 中配置: ```javascript export default defineConfig({ i18n: { locales: ['en', 'zh', 'ja'], defaultLocale: 'en', routing: { prefixDefaultLocale: false, }, }, }); ``` 这样默认语言走 `/about`,其他语言走 `/zh/about`、`/ja/about`。配合 `astro:i18n` 模块提供的 `getRelativeLocaleUrl()` 生成各语言链接,再用中间件做语言检测和重定向,一个完整的多语言站点就跑起来了。 ## 路由策略怎么选? **子目录路由**(`/zh/about`)是主流方案,配置简单、共享域名权重,适合大多数项目。子域名方案(`zh.example.com`)需要额外 DNS 和证书配置,只在团队和资源充足时考虑。 默认语言是否加前缀,取决于你的目标用户——如果主要受众是英语用户,`prefixDefaultLocale: false` 让 URL 更干净;如果各语言地位平等,统一加前缀更一致。 ## 翻译文件怎么组织? UI 文本用 JSON 文件按语言分目录存放: ``` src/i18n/ en/common.json zh/common.json ja/common.json ``` 页面内容则用 Astro 的内容集合(Content Collections),按语言建集合或用 `slug` 后缀区分。读取时根据 `Astro.currentLocale` 过滤对应语言的内容。 ## SEO 要注意什么? 三件事:**hreflang 标签**、**规范 URL**、**多语言站点地图**。 ```astro <link rel="alternate" hreflang="en" href="/en" /> <link rel="alternate" hreflang="zh" href="/zh" /> <link rel="alternate" hreflang="x-default" href="/" /> ``` 配合 `@astrojs/sitemap` 的 `i18n` 配置项,自动生成多语言 sitemap。漏掉 hreflang 是最常见的错误,搜索引擎会把不同语言的页面当作重复内容。 ## 中间件怎么处理语言检测? ```typescript // src/middleware.ts import { defineMiddleware } from 'astro:middleware'; export const onRequest = defineMiddleware((context, next) => { const locale = context.url.pathname.split('/')[1]; const supported = ['en', 'zh', 'ja']; if (!supported.includes(locale)) { const browserLang = context.request.headers .get('accept-language') ?.split(',')[0].split('-')[0] || 'en'; const target = supported.includes(browserLang) ? browserLang : 'en'; return context.redirect(`/${target}${context.url.pathname}`); } context.locals.locale = locale; return next(); }); ``` 根据 `Accept-Language` 头判断浏览器语言,首次访问自动跳转。 ## 追问 - **astro-i18next 和原生 i18n 有什么区别?** 原生只管路由,不管翻译加载;astro-i18next 补了翻译函数和运行时,但增加了包体积。Astro 5 之后推荐原生路由 + 自建翻译工具函数。 - **SSR 模式下 i18n 有什么坑?** 静态模式下每个语言预生成页面没问题;SSR 模式要注意中间件里不能阻塞渲染,语言检测逻辑必须同步完成,且需要处理 cookie 记住用户偏好。 - **RTL 语言怎么处理?** 在根布局根据 `locale` 动态设置 `dir` 属性:`<html dir={isRTL ? 'rtl' : 'ltr'}>`,再用 CSS 逻辑属性(`margin-inline-start`)替代 `margin-left`。