云计算百科
云计算领域专业知识百科平台

从零部署全栈网站:Cloudflare Pages + Functions + D1 完整实践

Cloudflare 的这套组合适合中小型全栈项目:

  • Cloudflare Pages:托管 React、Vue、Vite、静态站点等前端产物;
  • Cloudflare Pages Functions:在边缘运行 API 代码,不需要单独购买或维护传统服务器;
  • Cloudflare D1:Cloudflare 托管的 serverless SQLite 数据库;
  • GitHub 集成:推送代码后自动构建、部署;PR 或非生产分支可以得到预览环境。

Pages Functions 可以通过绑定直接访问 D1;前端静态资源与 API 可以同域部署,因此通常不需要额外处理跨域问题。(developers.cloudflare.com)


一、最终架构

一次请求的大致路径如下:

  • 浏览器从 Pages 获取 React/Vite 构建后的静态文件;
  • 前端通过 fetch("/api/…") 调用同域 API;
  • functions/ 目录中的 Pages Functions 处理请求;
  • Function 使用 context.env.DB 访问 D1;
  • GitHub 的 main 分支有新提交时,Pages 自动构建并更新生产站点。
  • Cloudflare Pages 的 Git 集成支持推送自动部署、PR/分支预览 URL,以及 GitHub 内的构建状态检查。(developers.cloudflare.com)


    二、准备条件

    开始前需要准备:

    • 一个 Cloudflare 账号;
    • 一个 GitHub 仓库;
    • Node.js 20 或更高版本;
    • npm、pnpm 或 yarn;
    • 一个本地 Git 环境;
    • 可选:Cloudflare 自己托管的域名。

    本文以 React + Vite + TypeScript 为例,但后端部分同样适用于 Vue、Svelte、Astro 或纯静态 HTML 项目。


    三、初始化 Vite 项目

    npm create vite@latest cf-pages-d1-demo — –template react-ts
    cd cf-pages-d1-demo

    npm install
    npm install -D wrangler @cloudflare/workers-types

    建议在 package.json 中准备以下脚本:

    {
    "scripts": {
    "dev": "vite",
    "build": "tsc –noEmit && vite build",
    "preview": "vite preview",
    "dev:pages": "npm run build && wrangler pages dev dist"
    }
    }

    Vite 默认会把生产构建产物输出到 dist,而 Cloudflare Pages 的构建目录需要填写实际输出目录。(developers.cloudflare.com)


    四、创建 D1 数据库

    先登录 Cloudflare:

    npx wrangler login

    创建生产数据库:

    npx wrangler d1 create cf-pages-d1-demo

    终端会输出类似以下内容:

    database_name = "cf-pages-d1-demo"
    database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

    建议再创建一个预览数据库,避免测试分支或 PR 误写入正式数据:

    npx wrangler d1 create cf-pages-d1-demo-preview

    需要保存两组信息:

    • 生产数据库名称和 ID;
    • 预览数据库名称和 ID。

    五、配置 Wrangler 和 D1 绑定

    在项目根目录新建 wrangler.toml:

    name = "cf-pages-d1-demo"
    pages_build_output_dir = "./dist"
    compatibility_date = "2026-07-31"

    [[d1_databases]]
    binding = "DB"
    database_name = "cf-pages-d1-demo"
    database_id = "<你的生产数据库 ID>"
    preview_database_id = "<你的预览数据库 ID>"
    migrations_dir = "migrations"

    这里最重要的是:

    binding = "DB"

    它决定了后端代码中访问数据库的变量名:

    context.env.DB

    Cloudflare Pages 的 D1 绑定既可以通过 Dashboard 设置,也可以写入 Wrangler 配置文件。对于长期维护项目,更推荐把 Wrangler 配置提交到 Git,让配置和代码一起版本化;一旦以 Wrangler 配置作为来源,就应避免和 Dashboard 中的同类设置长期混用,以免出现配置不一致。(developers.cloudflare.com)

    如果你暂时不想维护 wrangler.toml,也可以在 Cloudflare Dashboard 中进入:

    Workers & Pages
    → 选择项目
    → Settings
    → Bindings
    → Add
    → D1 database bindings

    绑定变量名填写 DB,然后选择创建好的 D1 数据库。完成后需要重新部署,绑定才会生效。(developers.cloudflare.com)


    六、创建数据库迁移文件

    不要直接在生产数据库中手工改表。推荐把数据库结构变更写成迁移文件,并提交到 Git。

    创建迁移:

    npx wrangler d1 migrations create cf-pages-d1-demo initial_schema

    在 migrations/ 目录中写入 SQL,例如:

    — migrations/0001_initial_schema.sql

    CREATE TABLE IF NOT EXISTS todos (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    done INTEGER NOT NULL DEFAULT 0,
    created_at TEXT NOT NULL
    );

    CREATE INDEX IF NOT EXISTS idx_todos_created_at
    ON todos(created_at DESC);

    先在本地数据库应用迁移:

    npx wrangler d1 migrations apply cf-pages-d1-demo –local

    确认无误后,再应用到生产数据库:

    npx wrangler d1 migrations apply cf-pages-d1-demo –remote

    如果配置了预览数据库,也应同步执行:

    npx wrangler d1 migrations apply cf-pages-d1-demo –preview

    D1 的迁移文件会记录已应用状态;migrations apply 会应用尚未执行的迁移。对于生产环境,建议用数据库名称而不是绑定名执行迁移,避免因为绑定名被修改而误操作错误的数据库。(developers.cloudflare.com)

    重要提醒: Wrangler 的 D1 命令默认偏向本地数据库行为。真正执行生产迁移或查询时,必须明确带上 –remote。(developers.cloudflare.com)


    七、编写 Pages Functions API

    Cloudflare Pages 会根据根目录 functions/ 中的文件路径自动生成路由。

    目录结构如下:

    cf-pages-d1-demo/
    ├── functions/
    │ └── api/
    │ └── todos.ts
    ├── migrations/
    │ └── 0001_initial_schema.sql
    ├── src/
    │ └── App.tsx
    ├── wrangler.toml
    └── package.json

    其中:

    functions/api/todos.ts

    会自动对应:

    /api/todos

    Pages Functions 的 functions/ 必须位于项目根目录,而不是 src/ 或 dist/ 内。(developers.cloudflare.com)

    示例:查询和新增待办事项

    // functions/api/todos.ts

    type Env = {
    DB: D1Database;
    };

    type Todo = {
    id: string;
    title: string;
    done: number;
    created_at: string;
    };

    export const onRequestGet: PagesFunction<Env> = async ({ env }) => {
    const result = await env.DB
    .prepare(
    `SELECT id, title, done, created_at
    FROM todos
    ORDER BY created_at DESC`,
    )
    .all<Todo>();

    return Response.json(result.results);
    };

    export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
    const input = await request.json() as { title?: unknown };
    const title = typeof input.title === "string" ? input.title.trim() : "";

    if (!title || title.length > 120) {
    return Response.json(
    { error: "title 必须是 1 到 120 个字符" },
    { status: 400 },
    );
    }

    const todo = {
    id: crypto.randomUUID(),
    title,
    done: 0,
    created_at: new Date().toISOString(),
    };

    await env.DB
    .prepare(
    `INSERT INTO todos (id, title, done, created_at)
    VALUES (?, ?, ?, ?)`,
    )
    .bind(todo.id, todo.title, todo.done, todo.created_at)
    .run();

    return Response.json(todo, { status: 201 });
    };

    D1 应始终使用 prepare(…).bind(…) 参数绑定,而不是把用户输入直接拼接到 SQL 字符串中。D1 的 Workers Binding API 支持 prepare、bind、run、first、all 和批量执行等操作。(developers.cloudflare.com)


    八、前端调用 API

    在 src/App.tsx 中写一个最小示例:

    import { FormEvent, useEffect, useState } from "react";

    type Todo = {
    id: string;
    title: string;
    done: number;
    created_at: string;
    };

    export default function App() {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [title, setTitle] = useState("");
    const [loading, setLoading] = useState(true);

    async function loadTodos() {
    setLoading(true);

    const response = await fetch("/api/todos", {
    cache: "no-store",
    });

    if (!response.ok) {
    throw new Error("加载失败");
    }

    setTodos(await response.json());
    setLoading(false);
    }

    useEffect(() => {
    void loadTodos();
    }, []);

    async function addTodo(event: FormEvent) {
    event.preventDefault();

    const response = await fetch("/api/todos", {
    method: "POST",
    headers: {
    "content-type": "application/json",
    },
    body: JSON.stringify({ title }),
    });

    if (!response.ok) {
    alert("新增失败");
    return;
    }

    setTitle("");
    await loadTodos();
    }

    return (
    <main>
    <h1>Cloudflare Pages + D1 Demo</h1>

    <form onSubmit={addTodo}>
    <input
    value={title}
    placeholder="输入待办事项"
    onChange={(event) => setTitle(event.target.value)}
    />
    <button disabled={!title.trim()} type="submit">
    添加
    </button>
    </form>

    {loading ? (
    <p>加载中…</p>
    ) : (
    <ul>
    {todos.map((todo) => (
    <li key={todo.id}>{todo.title}</li>
    ))}
    </ul>
    )}
    </main>
    );
    }

    前端和 Functions 部署在同一个 Pages 项目中时,调用 /api/todos 即可,不需要写本地开发端口或单独维护后端域名。


    九、本地运行完整环境

    仅执行:

    npm run dev

    通常只能运行 Vite 前端,不能完整模拟 Pages Functions 和 D1。

    要本地测试“前端 + Functions + D1”,先构建,再使用 Wrangler:

    npm run build

    npx wrangler pages dev dist –d1 DB=<你的数据库 ID>

    也可以写入脚本:

    {
    "scripts": {
    "dev:pages": "npm run build && wrangler pages dev dist –d1 DB=<你的数据库 ID>"
    }
    }

    然后启动:

    npm run dev:pages

    本地浏览器访问 Wrangler 输出的地址,例如:

    http://127.0.0.1:8788

    Cloudflare 的本地开发默认使用本地持久化数据,而不是生产 D1;因此应先在本地验证迁移和 API,再明确使用 –remote 操作远程数据库。(developers.cloudflare.com)


    十、配置环境变量与密钥

    1. 非敏感变量

    例如运行环境名称、功能开关等:

    [vars]
    APP_ENV = "production"

    在 Function 中读取:

    type Env = {
    DB: D1Database;
    APP_ENV: string;
    };

    export const onRequest: PagesFunction<Env> = async ({ env }) => {
    return Response.json({
    environment: env.APP_ENV,
    });
    };

    2. 敏感变量

    例如管理员密码、JWT 密钥、第三方 API Key,必须使用 Secret。

    本地创建 .dev.vars:

    ADMIN_PASSWORD="replace-me"
    JWT_SECRET="replace-me-too"

    并加入 .gitignore:

    .dev.vars*
    .env*

    生产环境可以在 Dashboard 中进入:

    Workers & Pages
    → 项目
    → Settings
    → Variables and Secrets
    → Add
    → Encrypt

    也可以通过 Wrangler 写入:

    npx wrangler pages secret put ADMIN_PASSWORD \\
    –project-name cf-pages-d1-demo

    npx wrangler pages secret put JWT_SECRET \\
    –project-name cf-pages-d1-demo

    Pages 的 Secret 只能在运行时通过 context.env 读取;本地开发可以使用 .dev.vars 或 .env,但这类文件不应提交到 Git。(developers.cloudflare.com)


    十一、部署到 Cloudflare Pages:推荐 GitHub 自动部署

    这是最适合长期项目的方案。

    1. 初始化 Git 并推送 GitHub

    git init
    git add .
    git commit -m "feat: initial Cloudflare Pages app"

    git branch -M main
    git remote add origin git@github.com:<你的用户名>/cf-pages-d1-demo.git
    git push -u origin main

    2. 在 Cloudflare Dashboard 创建 Pages 项目

    进入:

    Workers & Pages
    → Create application
    → Pages
    → Connect to Git

    然后:

  • 授权 Cloudflare 访问 GitHub;

  • 选择仓库;

  • 设置生产分支为 main;

  • 设置构建命令:

    npm run build

  • 设置输出目录:

    dist

  • 创建并等待第一次部署完成。

  • 如果项目已在 GitHub 中集成 Pages,后续推送到生产分支会触发自动构建和发布;其他分支或 PR 可用于生成预览部署。(developers.cloudflare.com)

    3. 之后的日常发布流程

    git add .
    git commit -m "feat: add todo API"
    git push origin main

    Cloudflare Pages 会自动完成:

    拉取仓库
    → 安装依赖
    → 执行构建
    → 部署静态资源
    → 部署 Functions
    → 更新 pages.dev 和自定义域名


    十二、命令行直接部署方案

    如果不想用 GitHub 自动部署,也可以直接用 Wrangler。

    先创建 Pages 项目:

    npx wrangler pages project create cf-pages-d1-demo \\
    –production-branch main

    构建并部署:

    npm run build

    npx wrangler pages deploy dist \\
    –project-name cf-pages-d1-demo \\
    –branch main

    wrangler pages deploy 可以直接发布构建目录;–project-name 指向目标 Pages 项目,–branch 用于指定部署分支。(developers.cloudflare.com)

    需要注意的是:

    • 带 Functions 的项目不能依赖 Dashboard 的 Direct Upload;
    • 若项目包含 functions/,应选择 Git 集成或 Wrangler 进行部署。(developers.cloudflare.com)

    十三、部署前后的数据库流程

    推荐把发布拆成两个步骤:

    生产发布时的推荐命令顺序:

    # 1. 查看生产库尚未执行的迁移
    npx wrangler d1 migrations list cf-pages-d1-demo –remote

    # 2. 应用生产迁移
    npx wrangler d1 migrations apply cf-pages-d1-demo –remote

    # 3. 查询确认表结构
    npx wrangler d1 execute cf-pages-d1-demo \\
    –remote \\
    –command "SELECT name FROM sqlite_schema WHERE type='table' ORDER BY name;"

    # 4. 发布代码
    git push origin main

    D1 支持通过 migrations list 查看未应用迁移,并通过 –remote 对生产数据库执行迁移或查询。(developers.cloudflare.com)


    十四、自定义域名

    部署成功后,默认会得到:

    https://<项目名>.pages.dev

    添加自己的域名时,进入:

    Workers & Pages
    → 项目
    → Custom domains
    → Set up a custom domain

    如果域名本身由 Cloudflare 托管,通常可以在 Dashboard 中完成关联;如果 DNS 在其他平台,则按 Cloudflare 给出的 CNAME 或 DNS 提示配置。

    建议至少准备:

    example.com
    www.example.com

    并把其中一个配置为主域名,另一个做跳转。


    十五、常见问题与排查

    1. 页面正常,但 /api/* 返回 404

    检查:

    • functions/ 是否位于项目根目录;
    • 文件路径是否正确,例如 functions/api/todos.ts;
    • 是否重新部署;
    • 是否误把 Functions 放进了 src/ 或 dist/。

    Pages Functions 的路由来自根目录 functions/ 的文件结构。(developers.cloudflare.com)


    2. Function 报错:Cannot read properties of undefined (reading 'prepare')

    通常表示:

    context.env.DB

    没有拿到 D1 binding。

    检查:

    • wrangler.toml 的 binding = "DB";
    • 代码是否同样使用 env.DB;
    • Dashboard 中是否正确绑定;
    • 修改 binding 后是否重新部署。

    绑定名必须在配置、部署环境和代码中保持一致。(developers.cloudflare.com)


    3. 本地可用,生产环境提示“没有表”或 no such table

    通常是只执行了:

    npx wrangler d1 migrations apply … –local

    但没有执行:

    npx wrangler d1 migrations apply … –remote

    本地 D1 和生产 D1 是不同的数据环境;生产数据库需要单独应用迁移。(developers.cloudflare.com)


    4. Dashboard 配置和 wrangler.toml 对不上

    例如:

    • Dashboard 中绑定了数据库 A;
    • 仓库里的 wrangler.toml 指向数据库 B;
    • 下一次 Git 部署后,行为突然改变。

    解决方法:

  • 选择 Dashboard 或 Wrangler 配置作为主来源;
  • 推荐将 Wrangler 配置纳入版本控制;
  • 如果项目已有 Dashboard 配置,可以先执行:
  • npx wrangler pages download config <项目名>

    下载当前项目配置后再进行统一维护。(developers.cloudflare.com)


    5. 把密码或 API Key 写进前端代码

    不要这样做:

    const ADMIN_PASSWORD = "123456";

    浏览器最终会下载前端 JavaScript,任何写在前端包里的“密钥”都不是真正的秘密。

    正确做法:

    • Secret 存在 Cloudflare;
    • Function 使用 context.env.SECRET_NAME;
    • 前端只调用 API,不接触真正密钥。

    6. D1 查询越来越慢

    优先检查:

    • 是否为 WHERE、JOIN、ORDER BY 常用字段建立索引;
    • 是否避免查询全部数据;
    • 是否分页;
    • 是否把多条相互依赖较低的 SQL 放到批量操作中;
    • 是否给高频查询查看执行计划。

    D1 的索引适用于经常作为筛选条件、唯一约束或联合查询条件的列。(developers.cloudflare.com)


    十六、生产环境建议

    数据库

    • 所有表结构变更都必须通过 migration;
    • 生产迁移前先在本地和 Preview 验证;
    • 不要直接在线手改结构;
    • 高频查询字段建立索引;
    • 保存 created_at、updated_at、操作日志等审计字段。

    API

    • 所有写操作都验证输入;
    • SQL 一律使用参数绑定;
    • 管理接口必须鉴权;
    • 对写操作增加幂等键或请求 ID;
    • 对错误返回统一 JSON 格式。

    前端

    • API 请求使用同域相对路径,如 /api/todos;
    • 写操作完成后刷新或更新本地状态;
    • 对网络错误、加载状态、空状态做处理;
    • 不把管理员密码、第三方密钥写到浏览器代码里。

    部署

    • main 只放可上线代码;
    • 开发分支通过 Pages Preview 验证;
    • 数据库迁移与代码发布按顺序执行;
    • 每次生产发布后至少验证:
      • 首页;
      • 一个读接口;
      • 一个写接口;
      • 数据库是否写入;
      • Pages 构建日志是否成功。

    十七、上线检查清单

    [ ] 前端 npm run build 成功
    [ ] functions/ 位于项目根目录
    [ ] wrangler.toml 的 pages_build_output_dir 指向 dist
    [ ] D1 binding 名称与代码中的 env.DB 一致
    [ ] 本地 migration 已通过
    [ ] Preview D1 migration 已通过
    [ ] Production D1 migration 已通过
    [ ] 所有 Secret 已在 Cloudflare 配置
    [ ] .dev.vars / .env 未提交 Git
    [ ] GitHub main 已连接到 Pages 生产分支
    [ ] Pages 构建命令为 npm run build
    [ ] Pages 输出目录为 dist
    [ ] 部署后 API 与数据库读写已验证
    [ ] 自定义域名和 HTTPS 已验证


    结语

    Cloudflare Pages + Functions + D1 的核心价值是把传统的:

    前端托管 + Node 服务端 + 数据库 + CI/CD

    简化为:

    GitHub 仓库
    → Cloudflare Pages
    → Pages Functions
    → D1

    对于作品集、管理后台、预约系统、点餐工具、内容管理、小型 SaaS 原型、内部工具等项目,这套架构通常已经足够。静态资源由 Pages 承载,Function 请求计入 Workers 使用额度;因此正式上线前仍应结合项目访问量与当前套餐限制评估容量。(developers.cloudflare.com)

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从零部署全栈网站:Cloudflare Pages + Functions + D1 完整实践
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!