Astro SSR 踩坑记录:Cloudflare 适配器的那些细节
从 nodejs_compat 到 platformProxy,从 locals.runtime.env 到构建产物目录,把我迁移过程中真正卡住的地方一条条列出来。
1. nodejs_compat 不是可选项
第一次部署直接报了 500,日志里是一句很含糊的 Dynamic require of "node:buffer" is not supported。
原因是 Astro 的 Cloudflare 适配器的一部分依赖会用到 Node 内置模块的垫片。解决办法是在配置里显式声明:
compatibility_flags = ["nodejs_compat"]
注意 compatibility_date 也要写一个足够新的日期——设成 2024-09-23 之后 nodejs_compat 会自动带上 v2 的行为,比老版本更接近真实 Node。
compatibility_date = "2025-01-01"
compatibility_flags = ["nodejs_compat"]
2. 改完配置一定要重新生成类型
Astro 会把 locals 和 env 的类型写进 .astro/types.d.ts。改完 wrangler.toml 的绑定之后,如果编辑器还在报 Property 'DB' does not exist,先删掉缓存:
rm -rf .astro node_modules/.astro
npm run dev
astro sync 也会重建这些类型文件。
3. output: 'server' 和 prerender 的配合
全站 SSR 之后,所有页面默认都是「每次请求都执行」。但如果某个页面(比如关于页、404)其实不需要实时数据,可以单独标记成静态:
---
export const prerender = true;
---
反过来,如果项目是 output: 'static',想给某个页面开 SSR,也要在该页面写 export const prerender = false。
注意一个坑: 静态页面里访问 Astro.locals.runtime.env 会拿到 undefined,因为它在构建期执行,没有 Worker 运行时。这个报错通常表现为 Cannot read properties of undefined (reading 'DB')。判断标准很简单——只要这个页面读了 D1 或 R2,就必须是 SSR。
4. 构建产物目录必须和 wrangler 对齐
Astro 默认输出到 dist/,而 Cloudflare Pages 默认期望的是别的名字。两边要一致:
pages_build_output_dir = "./dist"
如果你用 Git 集成构建,还要在 Cloudflare 控制台的构建配置里把「构建输出目录」也填成 dist。这两处不一致,部署会成功但访问全是 404——因为 Pages 找不到入口文件。
5. platformProxy 让本地开发能读 D1
这是体验最好的一个特性。开启之后,npm run dev 会启动一个本地 miniflare,把 D1 和 R2 绑定注入到 locals.runtime.env:
// astro.config.mjs
adapter: cloudflare({
platformProxy: {
enabled: true,
configPath: 'wrangler.toml',
},
}),
本地数据落在 .wrangler/state/ 目录下。这个目录应该加进 .gitignore,否则你会把本地测试数据提交上去。
要初始化本地数据库表结构:
wrangler d1 execute blog-db --local --file=./schema.sql
6. 动态路由在 SSR 模式下不需要 getStaticPaths
静态模式里,src/pages/posts/[slug].astro 必须导出 getStaticPaths() 来枚举所有 slug。切到 output: 'server' 之后这个函数必须删掉,否则它仍然会在构建期被调用——而构建期没有 D1 绑定,直接构建失败。
SSR 模式下直接从参数读:
---
const { slug } = Astro.params;
const post = await Astro.locals.runtime.env.DB
.prepare('SELECT * FROM posts WHERE slug = ? AND status = ?')
.bind(slug, 'published')
.first();
if (!post) return new Response(null, { status: 404 });
---
这个 return new Response(...) 在 Astro 里是合法的——返回一个 Response 就会直接作为响应输出,不再渲染模板。这是 SSR 模式下处理 404 最干净的方式。
7. 环境变量:import.meta.env vs locals.runtime.env
两个东西容易混:
| 来源 | 何时可用 | 用途 | |
|---|---|---|---|
import.meta.env.X |
构建时的 .env / Vite |
构建期就固定了 | 非敏感配置、PROD 判断 |
locals.runtime.env.X |
Workers 绑定 / Secrets | 运行时 | D1、R2、密钥 |
密钥只能走 locals.runtime.env。 任何写进 import.meta.env 的东西都会被打进客户端 bundle,等于公开。
设置运行时密钥:
wrangler pages secret put SESSION_SECRET
8. 打包体积
Worker 免费版单个脚本限制 3MB(gzip 后 1MB)。我一开始把 highlight.js 全量引入了,构建产物直接 1.6MB,部署被拒。
解决办法是只注册用得到的语言:
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import bash from 'highlight.js/lib/languages/bash';
// ...只引需要的
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('bash', bash);
从 1.6MB 降到 380KB。原则是:凡是在服务端渲染用的库,都要检查它有没有 tree-shaking 友好的入口。
9. 用 wrangler pages dev 做上线前验证
astro dev 用的是 Vite 的 dev server + miniflare,和真正的 Pages Functions 运行时仍有差异。上线前跑一次本地预览:
npm run build
wrangler pages dev ./dist
这会用真实的 Worker 运行时加载构建产物。我遇到过的差异包括:Cookie 的 secure 属性在 dev server 下被忽略、Response 的流式传输行为不同。这一步能省掉很多「线上才复现」的调试时间。
小结
把上面这些串起来,一个能跑的 Cloudflare + Astro SSR 项目的最小清单是:
output: 'server'+cloudflare适配器compatibility_flags = ["nodejs_compat"]pages_build_output_dir和控制台构建配置两边一致- 本地用
platformProxy,生产用wrangler pages secret - 动态路由删掉
getStaticPaths - 服务端库只引需要的部分
- 上线前用
wrangler pages dev验一遍
剩下的就是正常写业务代码了。