面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 02026年5月30日 19:50

Astro View Transitions 如何工作?怎么避免页面过渡踩坑?

Astro View Transitions 的核心是:页面仍然按多页应用导航,但 Astro 在跳转时拦截链接、获取新页面、替换 DOM,并借助浏览器 View Transitions API 做过渡动画。它给静态站带来接近 SPA 的切页手感,但不等于把站点变成完整 SPA;SEO、静态输出和按页加载的优势仍保留。追问View Transitions 和 SPA 路由有什么区别?SPA 通常只替换客户端状态和组件树。Astro 更像增强版 MPA 导航,新页面仍是单独文档,只是在切换时接管加载和替换过程。为什么脚本状态会丢?导航后 DOM 会被替换,旧节点上的事件和第三方实例可能不存在。需要在 astro:page-load 里重新初始化必要脚本。transition:name 有什么边界?同一次过渡里名称应当唯一。列表页要用 slug 或 id 拼接名称,不要所有卡片都叫 cover。什么时候不该使用页面过渡?表单提交、支付、登录回调、后台管理不适合花哨动画。内容站、作品集、相册更适合,但也要控制范围。写段代码<ViewTransitions /><img src={post.cover} transition:name={`cover-${post.slug}`} />
前端阅读 02026年5月30日 19:50

Astro 如何做好 SEO?Meta、结构化数据和站点地图怎么配?

Astro 做 SEO 的优势不在插件多,而在默认输出更接近搜索引擎喜欢的页面:静态 HTML、少 JavaScript、首屏快、内容能直接被爬虫读到。落地时重点不是堆满 meta,而是保证每页有唯一标题、稳定 canonical、准确 description、可分享 OG、合适结构化数据,以及能被发现的 sitemap。追问Astro 为什么比很多 SPA 更适合 SEO?Astro 默认把页面渲染成 HTML,爬虫不需要等客户端 JavaScript 执行完才看到正文。但如果正文依赖客户端接口再拉取,仍可能被漏抓。Meta 标签是不是越多越好?不是,title、description、canonical、OG 和必要的 Twitter Card 通常就够了。每页唯一、准确、和正文匹配更重要。sitemap 和 robots.txt 有哪些坑?最常见的是 site 没配,导致 sitemap 生成错误域名;robots.txt 不提升排名,只控制抓取边界。SEO 组件放 layout 还是页面?基础标签可以放 layout,但标题、描述、发布时间、文章类型必须由页面传入,避免大量页面共用同一 description。写段代码<title>{title}</title><meta name="description" content={description} /><link rel="canonical" href={url} /><script type="application/ld+json" set:html={JSON.stringify(schema)} />
前端阅读 05月28日 06:34

Astro 组件的基本结构是什么?如何定义和使用 Props、插槽?

Astro 是近年来增长最快的前端框架之一,其组件系统融合了服务端逻辑与客户端模板的独特设计,让开发者可以用最少的 JavaScript 构建高性能页面。本文将系统讲解 Astro 组件的三大核心结构——前置脚本、模板区域和样式作用域,以及 Props 传参与 Slots 插槽的完整用法。Astro 组件的三大结构每个 .astro 文件都由三个可选部分组成:前置脚本(Frontmatter)、HTML 模板和 <style> 样式块。理解这三部分的执行时机和作用域,是掌握 Astro 组件的基础。1. 前置脚本(Frontmatter)用 --- 分隔符包裹的顶部区域,是组件的"服务端大脑":---// 这里的代码在构建时(或 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 语法:<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. 样式作用域<style> /* 默认 scoped,不会影响其他组件 */ h1 { color: #333; } /* 需要全局样式时使用 :global() */ :global(.markdown-body p) { line-height: 1.8; }</style>Astro 的样式默认是作用域隔离的——每个组件的样式会自动添加唯一属性选择器,杜绝样式泄漏。如果需要影响子组件或全局,使用 :global() 选择器。Props:组件间的数据传递Props 是 Astro 组件接收外部数据的标准方式,通过 Astro.props 对象访问。基本用法---// Card.astroconst { title, description } = Astro.props;---<div class="card"> <h2>{title}</h2> <p>{description}</p></div>使用组件时传入 Props:---import Card from './Card.astro';---<Card title="文章标题" description="文章描述" />TypeScript 类型约束为 Props 添加类型定义,可以在构建时捕获错误:---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 传递的最佳实践保持 Props 简单:Props 应该是序列化安全的原始数据(字符串、数字、布尔值、简单对象),避免传递函数或复杂类实例提供默认值:通过解构默认值为可选 Props 设定合理的 fallback使用 ...rest 透传:当包装组件时,用 const { class: className, ...rest } = Astro.props 收集并透传属性---// 包装组件的最佳实践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 让组件成为可复用的布局容器。默认插槽---// Layout.astroconst { title } = Astro.props;---<html> <head><title>{title}</title></head> <body> <main> <slot /> <!-- 所有子内容将渲染在这里 --> </main> </body></html>使用时直接在组件标签内放入内容:---import Layout from './Layout.astro';---<Layout title="我的页面"> <h1>页面标题</h1> <p>这些内容会出现在 <slot /> 的位置</p></Layout>命名插槽当组件需要多个内容入口时,使用命名插槽:---// PageLayout.astroconst { title } = Astro.props;---<div class="page"> <header> <slot name="header" /> <!-- 命名插槽 --> </header> <main> <slot /> <!-- 默认插槽 --> </main> <footer> <slot name="footer" /> <!-- 命名插槽 --> </footer></div>使用命名插槽:---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 内容插槽可以设置默认内容,当没有传入对应内容时自动显示:---// Card.astroconst { title } = Astro.props;---<div class="card"> <h2>{title}</h2> <div class="body"> <slot> <p>暂无内容</p> <!-- Fallback:未传入内容时显示 --> </slot> </div></div>插槽传递(Slot Forwarding)在嵌套布局中,子布局可以将插槽"透传"给父布局:---// BaseLayout.astro---<html> <body> <slot name="head" /> <slot /> </body></html>---// HomeLayout.astroimport BaseLayout from './BaseLayout.astro';---<BaseLayout> <slot name="head" slot="head" /> <slot /></BaseLayout>这样最终页面使用 <HomeLayout> 时,内容会正确传递到 <BaseLayout> 的对应插槽位置。框架组件中的 SlotsAstro 支持在 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)。常见陷阱与注意事项前置脚本不等于客户端脚本:--- 中的代码在服务端执行,需要交互逻辑时应使用 <script> 标签或 client:* 指令模板表达式是静态的:{variable} 在构建时求值,不是响应式绑定Props 无法传递函数:Astro 组件的 Props 是序列化传递的,函数和类实例无法通过 Props 传递样式隔离是默认行为:不要假设子组件能继承父组件的 class 样式组件默认是静态的:需要客户端交互时,必须使用 client:load、client:visible 等水合指令---// 静态组件 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 应用。
前端阅读 05月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 中配置适配器:import { defineConfig } from 'astro/config';import node from '@astrojs/node';export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }),});创建第一个 API 路由使用 APIRoute 类型可以获得完整的类型提示,这是推荐的做法:// src/pages/api/hello.tsimport 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 根据请求方法自动路由到对应函数:// src/pages/api/users.tsimport 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 函数会在请求方法没有对应导出函数时被调用,适合做兜底处理或方法校验。动态路由参数使用方括号语法定义动态路由参数,与页面路由的规则一致:// src/pages/api/users/[id].tsimport 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 路由函数接收一个上下文对象,从中可以提取请求的所有信息:// src/pages/api/search.tsimport 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 对象,你可以完全控制状态码、头信息和响应体:// 成功响应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 中提取凭证:// src/pages/api/admin/stats.tsimport 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 上挂载用户信息:// src/middleware.tsimport { 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 路由保持简洁:// src/lib/api-error.tsexport 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' } } );}在路由中使用:// src/pages/api/data.tsimport 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 预检和响应头来解决:// src/pages/api/public-data.tsimport 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 禁用也能工作组件内部的服务端调用// 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 中提取文件对象,并进行类型和大小校验:// src/pages/api/upload.tsimport 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 路由连接数据库时,分页是最常见的需求之一:// src/pages/api/posts.tsimport 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() 声明所有可能的路径:// src/pages/api/tags/[tag].tsimport 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' }, });};如果需要运行时动态响应,必须将路由标记为按需渲染:export const prerender = false;面试常问:SSG 模式的 API 路由本质上是构建时的数据预生成,适合数据不频繁变化的场景;SSR 模式才是真正的服务端接口,适合实时数据。搞混这两种模式是常见的错误。实战中的常见问题请求体解析失败怎么办? request.json() 在非法 JSON 时会抛异常,必须用 try/catch 包裹。同理 request.formData() 在非表单请求时也会报错。如何实现速率限制? Astro 本身不提供速率限制,需要自行实现或使用中间件。简单的做法是基于 IP 和时间窗口做计数:// src/lib/rate-limit.tsconst 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: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 路由的适用场景区分,以及输入验证和错误处理的最佳实践。
前端阅读 05月27日 21:13

Astro 的 Image 组件如何优化图片加载?

Astro 的 Image 组件在构建时自动完成四件事:生成多尺寸响应式图片、转换现代格式(AVIF/WebP)、压缩质量、注入懒加载属性。浏览器根据 srcset 和 sizes 选择最合适的资源,避免加载冗余像素。基本用法---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,其余默认 lazyformat — 输出格式,默认 WebP远程图片处理远程 URL 需在 astro.config.mjs 白名单域名,否则构建报错:image: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }]}不确定尺寸时加 inferSize,Astro 会在构建时拉取图片获取宽高,避免 CLS 布局偏移。三个常见坑public 目录图片不走优化 — 必须放 src/assets 并 import 引入宽高缺失导致 CLS — 本地图片自动推断,远程图片需手动指定或用 inferSize忘记配 remotePatterns — 远程图片直接报错,排查时先检查配置追问Image 和 Picture 有什么区别? Picture 生成 元素,多格式同时输出让浏览器自行选择;Image 只输出单一最优格式。需要兼容老浏览器时用 Picture。构建时优化和 CDN 实时优化怎么选? 构建时优化零运行时开销,适合静态站点;图片量大或频繁更新时,CDN 实时处理更灵活。两者可以结合使用。如何优化 LCP? 首屏图片设 loading="eager" 并加 fetchpriority="high",同时用 widths 限制首屏图的最大尺寸,避免加载不必要的大图。
前端阅读 05月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 流。
前端阅读 05月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 更合理,因为几乎所有组件都需要交互,岛屿架构优势无法发挥。
前端阅读 05月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/FastifyDocker 和 CI/CDDocker 部署本质还是 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 跑一遍确认瓶颈在渲染还是网络。
前端阅读 05月27日 21:07

什么是 Astro 的内容集合(Content Collections)?如何使用它来管理博客文章或文档?

Astro 内容集合是什么Astro 内容集合(Content Collections)是 Astro 内置的结构化内容管理方案,把散落在项目里的 Markdown、MDX、JSON 等文件统一收进 src/content/ 目录,用 Zod Schema 做 frontmatter 校验,构建时自动完成类型推断和验证——写错字段名直接报编译错误,不用等到线上才发现。怎么用两步走:定义 Schema,写内容文件。在 src/content/config.ts 里声明集合: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 取单条:---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 或数据库,集合只负责静态内容的类型安全。
前端阅读 05月27日 21:06

如何在 Astro 中实现国际化(i18n)?

核心答案Astro 从 4.0 开始内置了 i18n 路由支持,不需要第三方库就能实现多语言网站。在 astro.config.mjs 中配置: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、多语言站点地图。<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 是最常见的错误,搜索引擎会把不同语言的页面当作重复内容。中间件怎么处理语言检测?// src/middleware.tsimport { 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。