Astro 构建与部署
旅程驱动理解从构建到上线:astro build、产物分析、适配器与按需渲染、CI 部署、域名与 HTTPS、路由缓存
知识点地图
- 知识类别:Astro 构建与部署——
astro build产物、适配器与运行模式、SSR/静态取舍、路由缓存与 CI/CD,对应 Astro 官方文档「Deploy your site」系列。 - 解决什么问题:源代码到「全球可访问网站」之间的全部工程环节:构建产物长什么样、跑在什么运行时上、静态与按需渲染怎么选、部署流水线怎么搭。
- 什么时候用到:第一次部署 Astro 站点;给内容站接 CI 门禁;排查「本地好好的、部署就白屏」;SSR 站点的缓存策略落地。
- 本篇主线:「书籍出版」六站旅程;核心决策只有两个——运行模式(第 3-4 站)与缓存策略(第 5 站),其余都是流程。错误选型的代价见第 4 节末的对照表。
- 本篇不讲:Sessions 与 Server Islands 的运行时细节(见 063 篇、131 篇,本篇只讲它们对适配器的要求);环境变量的完整规则(见 135 篇)。
0. 开篇:一本书从手稿到书店的旅程
假设你写完了一本书的手稿。接下来会发生什么?手稿要送到印刷厂排版印刷,印好的样书要送去质检,然后你要选一家出版社(决定书的发行渠道),再决定这本书卖给谁、放在哪个书架的哪个位置,最后由物流车队把书送到全国书店,书店挂上招牌正式营业。每一站都有明确的任务,跳过任何一站,书就到不了读者手里。
网站上线是同一趟旅程。本文按”从构建到上线”的完整旅程组织,你跟着走一遍,就能理解 Astro 站点是如何从源代码变成全球可访问的网站的:
flowchart TD
A["第 1 站 印刷厂:astro build(构建产物)"]
B["第 2 站 质检:产物分析(dist/ 里有什么)"]
C["第 3 站 选出版社:适配器与运行模式(静态 vs 按需渲染)"]
D["第 4 站 决定卖给谁:SSR 与静态的取舍"]
E["第 5 站 物流车队:CI/CD 与平台部署"]
F["第 6 站 挂牌营业:域名、HTTPS 与检查清单"]
A --> B --> C --> D --> E --> F1. 第 1 站,印刷厂:astro build
1.1 构建在做什么
astro build 是 Astro 的”印刷车间”。它分两步完成印刷:
- 打包:把站点的页面、内容与客户端组件(岛屿)打包成 JavaScript;
- 生成:像运行一台小型服务器一样执行打包结果,为每个预渲染页面发起请求、渲染出完整 HTML,输出到
dist/目录。
npm run build
# 构建完成后:
npm run preview # 本地模拟生产环境,预览 dist/ 产物的实际效果preview 值得养成习惯:它跑在构建产物之上,能发现”开发时正常、构建后异常”的差异(例如绝对路径、资源引用问题)。
1.2 构建产物长什么样
一次构建结束后,dist/ 大致是这样的结构:
graph TD
A["dist/"] --> B["index.html(首页,完整静态 HTML)"]
A --> C["about/"]
C --> C1["index.html(目录格式页面)"]
A --> D["posts/"]
D --> D1["hello-world/"]
D1 --> D2["index.html"]
A --> E["_astro/(哈希命名的静态资源)"]
E --> E1["index-abc123.css(打包后的 CSS,含 scoped 规则)"]
E --> E2["hero-7f3k9a.webp(优化后的图片)"]
E --> E3["counter-xyz789.js(岛屿组件的客户端脚本,按需加载)"]1.3 构建提速:Astro 7 的”新印刷机”
Astro 7 把印刷车间整体换成了原生性能:.astro 编译器重写为 Rust、Markdown 管线替换为 Rust 实现的 Sätteri、打包器升级为 Vite 8(内含 Rust 写的 Rolldown)。官方基准测试显示,真实站点构建提速 15%-61%,其中 Markdown 密集的文档站收益最大——例如官方文档站(约 6313 页)从 114.5 秒降到 73.5 秒,Cloudflare 开发者文档站(8431 页)从 386.9 秒降到 261.9 秒。对你来说,构建命令和产物格式完全不变,只是等的时间变短了。
2. 第 2 站,质检:读懂产物
书要质检,产物要分析。构建完成后做三件事,能避免大部分”上线才发现”的意外。
2.1 看结构:页面是否都在
逐个核对 dist/ 中是否生成了所有预期页面(含动态路由展开后的全部实例)。漏页通常是 getStaticPaths() 返回的数据为空导致的。
2.2 看体积:有没有”意外的大包”
重点检查 _astro/ 中最大的几个文件:
# 查看产物中各文件体积(按从大到小排序,前 20 个)
npx serve dist # 或用任意静态服务器,打开 DevTools Network 观察- 出现巨型
.js:八成是某个岛屿误用了client:load,或第三方库被整包打入。回到 006 的水合决策,把不紧急的岛屿降级为client:idle/client:visible; - 出现巨型图片/字体:检查是否绕过
<Image />原图直出,或 Fonts API 未启用。
2.3 看引用:资源路径是否自洽
确认 CSS、JS、图片的引用路径在 preview 下全部可访问(DevTools 无 404)。若站点要部署在域名子路径(如 https://example.com/docs/),则必须配置 base(见第 5 站),否则所有资源路径都会指向错误的根路径。
3. 第 3 站,选出版社:适配器与运行模式
书的发行渠道由出版社决定;网站的”运行方式”由**适配器(adapter)**决定。适配器把 Astro 的产物接入特定平台的运行环境(Node 服务器、Serverless 函数、边缘 Worker)。
3.1 三种运行模式(注意 5.x 之后的最新变化)
| 模式 | 配置 | 渲染时机 | 适用场景 |
|---|---|---|---|
| 静态(默认) | output: 'static' | 构建期一次性生成 | 博客、文档站、营销页(绝大多数内容站) |
| 服务端按需渲染 | output: 'server' + 适配器 | 每次请求 | 登录态、个性化、实时数据 |
| 静态为主 + 个别动态页 | output: 'server' + 页面内 export const prerender = true | 混合 | 大部分静态 + 少量动态(如登录页) |
需要特别说明:Astro 5 起移除了独立的 output: 'hybrid' 模式,简化为——静态模式全站静态;服务端模式默认按需渲染,个别页面用 export const prerender = true 标记为预渲染。判断一个页面是否预渲染,只需看这一行标记:
---
// src/pages/profile.astro(server 模式下)
// 默认按需渲染;加上这一行则改回构建期预渲染
export const prerender = true
---3.2 安装适配器
# 官方适配器:Node.js、Netlify、Vercel、Cloudflare(另有 Deno、Bun 等)
npx astro add node
npx astro add netlify
npx astro add vercel
npx astro add cloudflareastro add 会自动安装适配器、写入 astro.config.mjs 并提示后续步骤。以 Cloudflare 为例:
// astro.config.mjs
import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
export default defineConfig({
output: 'server',
adapter: cloudflare(),
})3.3 本地开发与生产一致(Astro 6+ 的红利)
Astro 6 重构了开发服务器:通过 Vite 的 Environment API,开发环境可以直接运行目标平台的真实运行时。例如 Cloudflare 适配器在开发、预渲染、生产全程运行 workerd 运行时,本地就能直接使用 KV、D1、R2 等绑定,“本地好好的,上线就挂”这类问题从根上减少。
3.4 动态路由的两种命运
同一个 [id].astro 动态路由,在两种模式下走向完全不同:
---
// src/pages/product/[id].astro
// 静态模式:构建期必须通过 getStaticPaths() 枚举所有 id,输出全部页面
// 服务端模式:无需枚举,Astro.params 在请求时按 URL 匹配
const { id } = Astro.params
const product = await fetchProduct(id) // 静态:构建期执行;服务端:请求期执行
---
<h1>{product.name}</h1>理解这条差异,是排查”动态页面为什么是空的 / 为什么构建报错没有生成页面”的关键。
4. 第 4 站,决定卖给谁:SSR 与静态的取舍
选出版社之后要决定”读者是谁、书放哪个书架”。这一站给你一张决策表,避免为了”用上新功能”而给纯内容站强行上 SSR:
| 需求特征 | 推荐模式 | 理由 |
|---|---|---|
| 内容几乎不变(博客、文档、营销页) | 静态 | 零服务器成本、任意静态托管、响应最快、CDN 天然缓存 |
| 每个用户内容不同(登录后个人中心) | 按需渲染(SSR) | 需要请求级数据与 Cookie 能力 |
| 数据偶尔变但不至于每次渲染(文章列表、价格页) | 静态或 SSR + 路由缓存 | “最快的构建是不发生的构建” |
| 大部分静态、少部分动态 | server + prerender = true 标记个别页 | 兼得两者优点 |
注意 site 配置在两种模式下都重要:
// astro.config.mjs
export default defineConfig({
output: 'static',
// 站点正式域名:用于生成规范链接、sitemap 与 Open Graph 绝对地址
site: 'https://docs.example.com',
build: { format: 'directory' }, // 页面输出为 /about/index.html(默认)
})4.1 错误选型的后果对照表
选型错误不报错,只产生「长期账单」。把四种典型错配的后果写清楚,决策时对照:
| 错误选型 | 短期现象 | 长期账单 |
|---|---|---|
| 纯内容站强行上 SSR | 多花适配器与服务器配置功夫 | 每个访客都触发渲染,服务器成本随流量线性上涨;CDN 缓存命中率近乎为零——静态站本可以零成本扛住 |
| 个性化站点用了纯静态 | 登录态、用户数据无处安放 | 被迫在客户端堆 API 请求与状态管理,每个访客下载一段 JS 只为拉自己的昵称(这类局部个性化正确解是 Server Islands 或按需渲染) |
| server 模式但忘配缓存 | 页面能用 | 高读低写的文档页每次请求都全量渲染,流量上涨后源站先崩——routeRules 是免费的性能(第 5 站) |
| 静态站误装了不需要的适配器 | 构建产物多出运行时文件 | 部署目标要求「纯静态文件」时直接部署失败;团队还被误导以为需要服务器 |
判读方法:先问「内容对谁一样」(全一样 -> 静态;按人不同 -> 按需渲染或岛屿),再问「变的频率」(几乎不变 -> 预渲染 + 缓存),两个答案组合出运行模式,而不是从「想用的特性」出发倒推。
5. 沿途的快速通道:路由缓存
“最快的构建是不发生的构建”——对按需渲染站点来说,最快的渲染是不渲染:命中缓存直接返回。Astro 7 中路由缓存(Route Caching)已稳定,通过顶层的 cache 与 routeRules 声明式配置:
// astro.config.mjs(server 模式项目)
import { defineConfig, memoryCache } from 'astro/config'
import node from '@astrojs/node'
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }), // 路由缓存只对按需渲染页面有意义
cache: { provider: memoryCache() }, // 内置内存缓存提供方
// routeRules 是"URL 模式 -> 缓存选项"的对象;模式支持静态路径、
// [id] 动态段与 [...path] Rest 段,不支持 /a/** 这类 glob 通配
routeRules: {
'/blog/[...path]': {
maxAge: 60 * 60, // 缓存 1 小时(对应 Cache-Control: max-age)
swr: 60 * 60 * 24, // 过期后 24 小时内仍可服务旧内容(stale-while-revalidate)
},
},
})缓存语义遵循标准 HTTP 缓存:maxAge 对应 Cache-Control: max-age,swr 对应 stale-while-revalidate,tags 数组可用于定向失效。缓存命中时源站根本不渲染,高读低写的页面(文档、列表)收益最大。
5.1 更进一步的 CDN 缓存提供方(实验特性)
若部署在 Netlify / Vercel / Cloudflare,可把缓存配置下发到平台边缘。三家官方适配器都提供实验性的缓存 provider,用法是把 cache.provider 从 memoryCache() 换成平台专属实现(当前需手动启用,官方计划后续版本自动接入):
// astro.config.mjs(Cloudflare 为例;Netlify/Vercel 同理,从各自适配器包导入)
import { defineConfig } from 'astro/config'
import cacheCloudflare from '@astrojs/cloudflare/cache'
export default defineConfig({
output: 'server',
cache: { provider: cacheCloudflare() },
routeRules: {
'/docs/[...path]': { maxAge: 3600, tags: ['docs'] },
},
})启用后,routeRules 的缓存指令会映射到平台原生缓存头(如 Cloudflare-CDN-Cache-Control)与 Cache-Tag 失效机制,响应缓存在全球边缘节点就近返回,进一步降低源站压力。
6. 第 5 站,物流车队:CI/CD 与平台部署
书的物流是车队,网站的物流是 CI/CD——代码一推,自动构建、自动测试、自动上线。
6.1 手工部署静态站
静态站点的产物是纯文件,可以部署到任何静态托管:
npm run build
# 把 dist/ 上传到任意平台:
# GitHub Pages / 阿里云 OSS / S3 / Nginx 等子路径部署时配置 base:
// astro.config.mjs
export default defineConfig({
base: '/docs/', // 部署在 https://example.com/docs/ 时必配
})6.2 平台一键部署(Netlify / Vercel)
Netlify 与 Vercel 对 Astro 提供第一方支持:在控制台导入 Git 仓库,平台自动识别 Astro 并填入默认参数(构建命令 npm run build、输出目录 dist)。静态与按需渲染均开箱即用,且支持每次提交的预览部署与一键回滚。
6.3 Cloudflare Pages / Workers
npm run build
# 纯静态:直接上传产物
npx wrangler pages deploy dist
# 使用 @astrojs/cloudflare 适配器的 SSR 站点:部署为 Workers
npx wrangler deployAstro 6+ 与 Cloudflare 深度合作(官方适配器在开发期即运行 workerd),KV/D1/R2 绑定在本地即可使用,上线后行为一致。
6.4 用 GitHub Actions 搭一条完整流水线
# .github/workflows/deploy.yml(示意)
name: build-and-deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22 # Astro 7 要求 Node 22+
- run: npm ci # 按锁文件精确安装依赖
- run: npm run build # 生产构建
- run: npx astro check # 类型检查作为质量门禁,失败即阻断
# 之后可按需上传 dist/ 到 OSS / S3 / 触发平台部署钩子流水线的价值在于门禁:安装、构建、类型检查任一环节失败,部署自动阻断,把错误挡在上线之前。FANDEX 的 CI 还包含链接检查与构建统计,保证 2000+ 篇文档构建稳定、无死链。
6.5 真实案例:FANDEX 的 GitHub Pages 流水线
本仓库(FANDEX)的 .github/workflows/deploy.yml 就是一条值得逐段读的生产流水线——内容站 + GitHub Pages 形态的完整样本:
# 关键步骤节选(完整文件见 .github/workflows/deploy.yml)
on:
push:
branches: [main]
paths: ['app-web/**', 'cnt-content/**', 'pnpm-lock.yaml', '...'] # 触发路径过滤
pull_request:
branches: [main] # PR 也跑构建验证
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }} # 旧构建自动取消
jobs:
build:
steps:
- run: pnpm install --frozen-lockfile # 锁文件精确安装
- run: pnpm -C app-web audit --prod --audit-level high # 依赖安全门禁
- run: pnpm --filter @fandex/web sync # 内容元数据补全(管线第 1 步)
- run: node scripts/content-audit.mjs # 内容审计门禁(HIGH 级问题阻断)
- run: pnpm typecheck && pnpm --filter @fandex/web lint
- run: pnpm build:web # Astro 生产构建
- run: pnpm --filter @fandex/web qa # 构建后 QA 检查
- uses: actions/upload-pages-artifact@v5
with: { path: app-web/dist }
deploy:
needs: build
if: github.event_name == 'push' # 只有 push 到 main 才真正部署
uses: actions/deploy-pages@v5六个值得抄走的设计:
- paths 过滤触发:内容站的大部分提交只动文档或只动前端,过滤路径避免无关提交空烧构建分钟数;
- PR 构建但不部署:
buildjob 对 PR 也执行(改坏了主分支构建立刻红),deployjob 用if: github.event_name == 'push'卡住——「验证免费、发布要过合并关」; - concurrency 取消旧构建:连续推送时只构建最新提交,PR 场景取消旧任务省资源;
- 审计前置:依赖安全审计与内容审计都放在 build 之前——最便宜的失败位置是最早的失败位置;
- sync 在 build 前:内容管线(frontmatter 补全、模块注册)是构建的输入前置步骤,顺序错乱是内容站流水线最常见的翻车点;
- 构建产物走 artifact:
upload-pages-artifact把 dist 作为产物在 job 间传递,部署 job 只认产物不重跑构建——两个 job 职责分离。
7. 第 6 站,挂牌营业:域名、HTTPS 与收尾检查
7.1 域名与 HTTPS
- 自定义域名:在托管平台把域名解析(CNAME/ALIAS)指向站点,并配置平台侧的自定义域;
- HTTPS 证书:Netlify、Vercel、Cloudflare Pages 都提供免费自动证书(Let’s Encrypt 类),配置域名后自动签发与续期,无需手动管理;
- 环境变量:
process.env/import.meta.env中前缀为PUBLIC_的变量才会暴露到浏览器端,密钥类变量只能用于服务端代码。SSR 站点的密钥(数据库口令等)务必只存平台环境变量,绝不提交到仓库。
7.2 上线前检查清单
第一,site 与 base 配置正确,sitemap 与规范链接生成无误;
第二,SSR 站点确认适配器与平台运行时匹配(Node 函数 / 边缘 Worker / Serverless 函数);
第三,动态页面配置 routeRules 缓存策略,上线后观察缓存命中率;
第四,构建产物无意外大 JS 包、无 404 资源(按第 2 站质检流程过一遍);
第五,上线后手动走一遍核心路径:首页、一篇内容页、一个交互岛屿、一个 404 页。
动手实践
任务:给一个静态 Astro 站点跑通「本地构建 -> 产物质检 -> 流水线」全链路。
- 本地跑
npm run build,按第 2 站流程清点dist/:页面数与src/pages对账、_astro/里资源的哈希命名、有无意外大文件; - 制造「错误选型」体验:给纯静态站临时
npx astro add node并切output: 'server',构建对比产物形态(出现服务器入口文件),再切回 static 观察产物恢复——用产物差异理解两种模式的物理区别; - 把 6.4 节的 GitHub Actions 示意流水线搬进自己的项目(GitHub 托管时),把
run:换成你的构建命令,观察首次运行;故意提交一个 frontmatter 错误,验证门禁红,修复后绿灯; - 对照 6.5 节读本仓库的
deploy.yml全文,找出文中六个设计点在你流水线里的对应物(或缺席项)。
参考现象(先自己试,再展开对照)
第 2 题:server 模式的 dist 里出现 entry.mjs(Node 适配器)之类的服务器入口与 host manifest,页面 HTML 不再全量预生成;切回 static 后产物恢复为纯 HTML 目录。物理形态的差异是两种模式所有行为差异的根源。
第 3 题:门禁红的位置在内容审计或构建步骤(取决于错误类型),PR 页面给出失败原因——「错误挡在合并前」的体感。注意流水线的价值密度取决于门禁的严格程度:全部 continue-on-error 的流水线只是个构建计时器。
第 4 题提示:paths 过滤、PR 构建不部署、concurrency、审计前置、sync 前置、artifact 传递——逐项对照后,缺席项就是你自己流水线的下一个改进点。
8. 常见错误与对策
| 常见错误 | 典型报错/现象 | 原因 | 解决办法 |
|---|---|---|---|
| 部署后样式/图片全部 404 | 页面 HTML 正常但资源路径错 | 站点部署在子路径,未配置 base | astro.config.mjs 中设置 base: '/子路径/' 后重新构建 |
| 动态路由构建报”没有生成页面” | 报错提示 getStaticPaths() 返回空 | 静态模式下动态路由必须枚举所有路径 | 修正 getStaticPaths() 数据源;或改用 server 模式按请求渲染 |
| 部署到平台后是空白页 | 平台显示 404 / 无法访问 | 未安装适配器,或输出目录配置不对 | 按平台装对应适配器;确认构建输出目录为 dist |
| SSR 站点本地正常、线上异常 | 生产环境报运行时错误 | 平台运行时与本地 Node 环境差异(老版本尤其常见) | 升级 Astro 6+ 并确认适配器在开发期即运行目标运行时;核对 Node 版本 22+ |
npm run build 在 CI 中失败 | 构建命令报错退出 | CI 环境 Node 版本过低(Astro 7 要求 Node 22+) | actions/setup-node 指定 node-version: 22 |
| 缓存策略不生效 | 页面每次都重新渲染 | 未配置 cache.provider,或只配了 routeRules 没开 provider | 配置 cache: { provider: memoryCache() }(或平台 CDN provider);同时确认 routeRules 用的是 /blog/[...path] 这类路由模式而非 /blog/** 通配 |
9. 一句话记忆
“构建是印刷、产物要质检、适配器定发行渠道、缓存是快速通道、CI 是物流车队、域名与 HTTPS 是最后挂牌——上线是一趟从 dist/ 到全球 CDN 的完整旅程。”