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 项目的最小清单是:

  1. output: 'server' + cloudflare 适配器
  2. compatibility_flags = ["nodejs_compat"]
  3. pages_build_output_dir 和控制台构建配置两边一致
  4. 本地用 platformProxy,生产用 wrangler pages secret
  5. 动态路由删掉 getStaticPaths
  6. 服务端库只引需要的部分
  7. 上线前用 wrangler pages dev 验一遍

剩下的就是正常写业务代码了。

← 回到文章列表