前言

本文记录了使用 Cloudflare CLI(Wrangler)从零开始创建并部署一个 Nuxt 项目到 Cloudflare Pages 的全过程,同时配合 Hono + D1 搭建后端 API 服务。整个技术栈均为 Cloudflare 原生生态,无需传统服务器。

项目架构

我们最终要构建的架构如下:

  • 前端:Nuxt 3 SSR 应用,部署在 Cloudflare Pages
  • 后端 API:Hono 框架 + TypeScript,运行在 Cloudflare Workers
  • 数据库:Cloudflare D1(基于 SQLite 的全球分布式数据库)
  • 域名:cxyuan.asia → Pages,api.cxyuan.asia → Worker

这种方式的好处是:所有服务都在 Cloudflare 全球 330+ 个节点上运行,无需管理服务器,天然具备 CDN 加速和 DDoS 防护。

1. 安装 Wrangler CLI

Wrangler 是 Cloudflare 官方命令行工具,用于管理 Workers、Pages、D1 等资源。

# 全局安装
npm install -g wrangler

# 或项目内安装
npm install --save-dev wrangler

# 验证版本
wrangler --version

Wrangler v4 是目前的最新版本,支持完整的 Workers + Pages + D1 管理功能。

2. 登录 Cloudflare

安装完成后,需要登录你的 Cloudflare 账号:

wrangler login

执行后浏览器会自动打开 Cloudflare OAuth 授权页面,点击「Authorize」即可。登录后可以查看当前身份:

wrangler whoami

输出示例:

👋 You are logged in with an OAuth Token, associated with the email your@email.com.
┌───────────────────────────┬──────────────────────────────────┐
│ Account Name              │ Account ID                       │
├───────────────────────────┼──────────────────────────────────┤
│ Your Account              │ abc123...                        │
└───────────────────────────┴──────────────────────────────────┘

如果发现权限不足,可以重新登录:

wrangler login

登录时会自动请求全部最新的权限 scope。

3. 创建 Nuxt 项目

使用 npx 创建一个 Nuxt 3 项目:

npx nuxi init cxy-app
cd cxy-app
npm install

添加必要的依赖:

npm install axios nprogress
npm install --save-dev @types/nprogress less vite-plugin-prismjs

4. 配置 Cloudflare Pages 部署

nuxt.config.ts 中设置 Cloudflare Pages 预设:

export default defineNuxtConfig({
  nitro: {
    preset: "cloudflare_pages",
  },
  routeRules: {
    "/**": { swr: 60 },
  },
})

注意:routeRules 中的 swr 会让 Nitro 缓存 SSR 输出的 HTML。如果不需要,可以移除。

package.json 中添加构建脚本:

{
  "scripts": {
    "build": "nuxt build --dotenv .env.production"
  }
}

创建 .env.production 文件:

NUXT_BASE_URL = https://api.cxyuan.asia/api/v1

5. 配置运行时 API 地址

nuxt.config.ts 中添加运行时配置:

runtimeConfig: {
  public: {
    apiBase: process.env.NUXT_BASE_URL,
  },
}

创建 Axios 插件 plugins/axios.ts,在请求时自动注入 Authorization 令牌:

import axios from "axios";
import { getCookie } from "h3";

export default defineNuxtPlugin((nuxtApp) => {
  const baseURL = nuxtApp.$config.public.apiBase;

  const instance = axios.create({
    baseURL,
    timeout: 50000,
    headers: { "Content-Type": "application/json" },
  });

  instance.interceptors.request.use((config) => {
    let token = null;
    if (import.meta.client) {
      token = localStorage.getItem("token");
    } else {
      const event = nuxtApp.ssrContext?.event;
      if (event) token = getCookie(event, "token") || null;
    }
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  });

  instance.interceptors.response.use(
    (response) => response.data,
    (error) => {
      if (error.response && error.response.data) {
        return Promise.resolve(error.response.data);
      }
      return Promise.reject(error);
    },
  );

  nuxtApp.provide("axios", instance);
});

6. 创建 Hono Worker 后端

用 Wrangler 初始化 Worker 项目:

mkdir cxy-server-hono && cd cxy-server-hono
npm init -y
npm install hono
npm install --save-dev wrangler typescript @cloudflare/workers-types
npx wrangler init

wrangler.toml 配置:

name = "cxy-server"
main = "src/index.ts"
compatibility_date = "2025-03-01"
compatibility_flags = ["nodejs_compat"]

[[d1_databases]]
binding = "DB"
database_name = "cxy-db"
database_id = "your-database-id"

[vars]
UPLOAD_BUCKET = "static-cxyuan"
UPLOAD_CDN_DOMAIN = "https://static.cxyuan.asia/"

# 敏感信息使用 wrangler secret put
# wrangler secret put JWT_SECRET
# wrangler secret put UPLOAD_ACCESS_KEY

7. 创建 D1 数据库

# 创建数据库
npx wrangler d1 create cxy-db

# 执行建表 SQL
npx wrangler d1 execute cxy-db --file=./db/schema.sql --remote

建表 SQL 示例:

CREATE TABLE articles (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  title TEXT NOT NULL,
  slug TEXT NOT NULL UNIQUE,
  content TEXT DEFAULT NULL,
  status TEXT DEFAULT 'draft',
  visibility TEXT DEFAULT 'public',
  published_at TEXT DEFAULT NULL,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now')),
  deleted_at TEXT DEFAULT NULL
);

8. 主入口文件:中间件与缓存

src/index.ts 中,我们配置了 CORS、日志、初始化以及基于 caches.default 的响应缓存:

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { initDB } from './config/database'
import { getCacheVersion } from './config/cache'
import routes from './routes'

const app = new Hono()
app.use('*', cors())
app.use('*', logger())

const cacheExclude = ['/captcha/', '/auth/']

app.use('/api/v1/*', async (c, next) => {
  const url = new URL(c.req.url)
  const shouldCache = c.req.method === 'GET'
    && !c.req.header('Authorization')
    && !cacheExclude.some(p => url.pathname.includes(p))

  const cacheKeyUrl = new URL(url.href)
  cacheKeyUrl.searchParams.set('_cv', getCacheVersion())
  const cacheKey = new Request(cacheKeyUrl.href, { method: 'GET' })

  if (shouldCache) {
    const cached = await caches.default.match(cacheKey)
    if (cached) return cached
  }

  await next()

  if (shouldCache && c.res.status === 200) {
    const clone = c.res.clone()
    const bodyText = await clone.text()
    try {
      const json = JSON.parse(bodyText)
      if (json.code === 200) {
        const cacheRes = new Response(bodyText, {
          status: clone.status,
          headers: clone.headers,
        })
        cacheRes.headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=60')
        c.executionCtx.waitUntil(caches.default.put(cacheKey, cacheRes))
      }
    } catch {
      // 非 JSON 响应也缓存
    }
  }
})

这个中间件实现了三级保护:

  • 仅缓存 GET 请求,排除带 Authorization 头(管理员)的请求
  • 排除验证码和登录接口
  • 缓存键带版本号(_cv),部署新版本时自动失效
  • 仅缓存 code === 200 的正常响应,错误/空响应不缓存

9. 时区问题与批量修复

Cloudflare Workers 默认使用 UTC 时间,而 D1 的 datetime('now') 也返回 UTC。对于中国用户,所有时间显示会晚 8 小时。

解决方案:创建东八区工具函数:

const SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000

export function nowShanghai(): string {
  const d = new Date(Date.now() + SHANGHAI_OFFSET_MS)
  return d.toISOString().replace('T', ' ').split('.')[0]
}

export function todayShanghai(): string {
  const d = new Date(Date.now() + SHANGHAI_OFFSET_MS)
  return d.toISOString().split('T')[0]
}

然后批量替换所有 datetime('now')nowShanghai(),并对已有数据执行 SQL 迁移:

UPDATE articles SET
  published_at = datetime(published_at, '+8 hours'),
  created_at   = datetime(created_at, '+8 hours'),
  updated_at   = datetime(updated_at, '+8 hours');

10. 绑定自定义域名

在 Cloudflare Dashboard 中为 Worker 绑定自定义域名:

  1. 进入 Workers & Pages → 选择你的 Worker
  2. Triggers → Custom Domains → Add Custom Domain
  3. 输入 api.cxyuan.asia,Cloudflare 会自动添加 DNS 记录

11. 部署

Worker 部署:

npm run deploy
# 等同于 wrangler deploy

Nuxt 项目通过 Git 集成自动部署:

git add .
git commit -m "init: nuxt cloudflare pages"
git push origin main

在 Cloudflare Dashboard 的 Pages 中关联 Git 仓库,设置构建命令为 npm run build,输出目录为 .output/public。每次 push 到 main 分支就会自动构建部署。

12. 检查部署状态

查询 Worker 的所有部署版本:

npx wrangler deployments list

查询 Pages 部署:

npx wrangler pages deployment list --project-name cxy-app

13. 设置缓存规则(可选)

对于 Pages 站点(cxyuan.asia),可以在 Cloudflare Dashboard 中添加 Cache Rule:

  • Rule name:首页缓存
  • When:Hostname equals cxyuan.asia
  • Then:Edge TTL = 5 minutes

对于 Workers 后端,由于自定义域名不走标准边缘缓存,需在代码内使用 caches.default 自行管理缓存。

14. Wrangler 常用命令速查

# 部署
wrangler deploy

# 查看部署历史
wrangler deployments list

# 回滚到指定版本
wrangler rollback <version-id>

# 管理密钥
echo "your-secret" | wrangler secret put MY_SECRET

# D1 数据库操作
wrangler d1 execute cxy-db --command "SELECT * FROM articles" --remote

# 本地开发
wrangler dev
nuxt dev --host

# Pages 项目列表
wrangler pages project list

# 查看当前身份与权限
wrangler whoami

结语

通过 Cloudflare 原生生态,我们仅用几行配置就完成了一个全栈应用的全球部署:Nuxt SSR 前端 + Hono API 后端 + D1 数据库,全部运行在 Cloudflare 的全球边缘网络上。无需管理服务器、无需配置负载均衡、无需操心 CDN。

这套架构特别适合:个人博客、小型商业网站、SaaS 应用 MVP 版本。当业务增长后,D1 可平滑迁移到 Hyperdrive + 传统数据库,Workers 可扩展到更多微服务。