<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>流欺の博客</title>
  <link>https://blog-d2z.pages.dev</link>
  <description>记录自己日常的博客~</description>
  <language>zh-CN</language>
  <atom:link href="https://blog-d2z.pages.dev/api/rss" rel="self" type="application/rss+xml"/>
  
    <item>
      <title><![CDATA[deployment]]></title>
      <link>https://blog-d2z.pages.dev/post/2</link>
      <guid>https://blog-d2z.pages.dev/post/2</guid>
      <pubDate>Fri, 03 Jul 2026 09:00:35 GMT</pubDate>
      <description><![CDATA[# 免费部署博客：原理、架构与流程

## 一、核心原理

### 为什么能免费？

Cloudflare 提供了一套免费的 Serverless 生态，恰好覆盖了一个博客系统的全部需求：

| 需求 | Cloudflare 产品 | 免费额度 |
|------|----------------|---------|
| 静态资源托管 | Pages | 无限站点 / 无限请求 / 无限带宽 ]]></description>
      <content:encoded><![CDATA[<h1>免费部署博客：原理、架构与流程</h1>
<h2>一、核心原理</h2>
<h3>为什么能免费？</h3>
<p>Cloudflare 提供了一套免费的 Serverless 生态，恰好覆盖了一个博客系统的全部需求：</p>
<table>
<thead>
<tr>
<th>需求</th>
<th>Cloudflare 产品</th>
<th>免费额度</th>
</tr>
</thead>
<tbody><tr>
<td>静态资源托管</td>
<td>Pages</td>
<td>无限站点 / 无限请求 / 无限带宽</td>
</tr>
<tr>
<td>后端 API</td>
<td>Pages Functions / Workers</td>
<td>每天 10 万次请求</td>
</tr>
<tr>
<td>数据库</td>
<td>D1 (SQLite)</td>
<td>5GB 存储 / 每秒 50 次读 + 25 次写</td>
</tr>
<tr>
<td>DNS / CDN</td>
<td>Cloudflare DNS</td>
<td>全球 CDN 加速</td>
</tr>
<tr>
<td>HTTPS 证书</td>
<td>内置</td>
<td>自动颁发，无需配置</td>
</tr>
</tbody></table>
<p>一个个人博客的流量和存储远低于上述限额，因此<strong>完全零成本 24 小时在线</strong>。</p>
<hr>
<h2>二、架构设计</h2>
<h3>整体架构图</h3>
<pre><code>用户浏览器
    │
    ▼
┌─────────────────────────────────────────────────┐
│              Cloudflare CDN (全球边缘节点)          │
│                                                   │
│  ┌──────────────────┐    ┌────────────────────┐  │
│  │  静态资源 (SPA)     │    │  API (/api/*)        │  │
│  │  client/dist/     │    │  Pages Functions    │  │
│  │  React + Vite     │    │  Hono Framework     │  │
│  └──────────────────┘    └────────┬───────────┘  │
│                                    │               │
│                            ┌───────┴───────┐      │
│                            │    D1 (SQLite)   │      │
│                            │   blog-db        │      │
│                            └───────────────┘      │
└─────────────────────────────────────────────────┘
</code></pre>
<h3>为什么是「Pages Functions」而不是「Workers」？</h3>
<ul>
<li><strong>Pages Functions</strong> 和 Pages 项目天然一体，部署时无需分别管理</li>
<li>自动处理 SPA 路由回退（所有非 <code>/api/*</code> 请求返回 <code>index.html</code>）</li>
<li>部署命令更简单：直接 deploy <code>client/dist</code>，Functions 目录自动关联</li>
</ul>
<h3>技术选型</h3>
<table>
<thead>
<tr>
<th>层</th>
<th>技术</th>
<th>选型理由</th>
</tr>
</thead>
<tbody><tr>
<td>前端框架</td>
<td>React 18 + React Router 6</td>
<td>SPA，客户端路由</td>
</tr>
<tr>
<td>构建</td>
<td>Vite 5</td>
<td>极快的 HMR 和构建</td>
</tr>
<tr>
<td>API 框架</td>
<td>Hono 4</td>
<td>专为 Cloudflare Workers/Pages 设计的轻量 Web 框架</td>
</tr>
<tr>
<td>数据库</td>
<td>D1 (SQLite 兼容)</td>
<td>边缘 SQL 数据库，与 Cloudflare 生态原生集成</td>
</tr>
<tr>
<td>认证</td>
<td>JWT (hono/jwt)</td>
<td>无状态鉴权，适合 Serverless</td>
</tr>
<tr>
<td>密码存储</td>
<td>PBKDF2 + SHA-512</td>
<td>Web Crypto API 实现，无需第三方库</td>
</tr>
<tr>
<td>Markdown</td>
<td>marked</td>
<td>纯 JS 渲染，零依赖</td>
</tr>
</tbody></table>
<hr>
<h2>三、项目结构</h2>
<pre><code>blog/
├── client/                    # React SPA 前端
│   ├── src/
│   │   ├── api.js             # API 客户端 (自动注入 JWT token)
│   │   ├── main.jsx           # React 入口 + 路由定义
│   │   ├── App.css            # 全局样式 (CSS 变量 + glassmorphism)
│   │   ├── components/
│   │   │   ├── Layout.jsx         # 整体布局 (顶栏+侧栏+页脚)
│   │   │   ├── PostCard.jsx       # 文章卡片
│   │   │   ├── ProfileCard.jsx    # 个人信息卡片
│   │   │   ├── CommentSection.jsx # 评论组件
│   │   │   ├── KanbanGirl.jsx     # Live2D 看板娘
│   │   │   └── Background.jsx     # 动态壁纸 (5分钟轮换)
│   │   └── pages/
│   │       ├── Home.jsx           # 首页 (文章列表+标签+布局切换)
│   │       ├── Post.jsx           # 文章详情 + 评论
│   │       ├── Search.jsx         # 全文搜索
│   │       ├── Guestbook.jsx      # 留言板
│   │       ├── Login.jsx          # 管理员登录
│   │       └── admin/
│   │           ├── Dashboard.jsx  # 文章管理 (CRUD)
│   │           ├── EditPost.jsx   # 编辑文章
│   │           ├── NewPost.jsx    # 新建文章
│   │           └── ProfileEdit.jsx# 个人设置 (名称/头像/密码)
│   └── vite.config.js         # Vite 配置 (含开发代理)
├── functions/
│   └── api/
│       └── [[path]].ts        # 全部 API 路由 (Hono + Pages Functions)
├── schema.sql                 # D1 建表语句
├── seed-admin.cjs             # 生成管理员 PBKDF2 密码哈希
├── wrangler.toml              # Cloudflare Pages + D1 配置
├── .dev.vars                  # 本地开发环境变量
└── package.json               # 依赖 + 脚本
</code></pre>
<hr>
<h2>四、核心技术细节</h2>
<h3>4.1 API 路由 — Hono on Pages Functions</h3>
<p><code>functions/api/[[path]].ts</code> 是唯一的 API 入口文件，使用 Hono 框架处理所有路由。</p>
<p><strong>关键设计：</strong></p>
<pre><code class="language-typescript">// Hono 连接 Pages Functions
import { Hono } from &#39;hono&#39;
import { handle } from &#39;hono/cloudflare-pages&#39;

const app = new Hono&lt;{ Bindings: Bindings; Variables: Variables }&gt;()

app.get(&#39;/api/posts&#39;, async (c) =&gt; { /* ... */ })
app.post(&#39;/api/auth/login&#39;, async (c) =&gt; { /* ... */ })
app.put(&#39;/api/profile&#39;, authGuard, async (c) =&gt; { /* ... */ })

export const onRequest = handle(app)
</code></pre>
<p><strong>为什么使用 <code>[[path]].ts</code>（catch-all 路由）？</strong></p>
<ul>
<li><code>/api/posts</code> → <code>[[path]].ts</code> 处理</li>
<li><code>/api/profile</code> → 同文件处理</li>
<li>所有 <code>/api/*</code> 请求统一路由，避免为每个端点创建单独文件</li>
</ul>
<p><strong>为什么可以在同一个文件里处理所有路由？</strong></p>
<ul>
<li>Hono 内部实现了路由树匹配，<code>app.get(&#39;/api/posts&#39;, ...)</code> 和 <code>app.get(&#39;/api/profile&#39;, ...)</code> 互不干扰</li>
<li>这是 Hono/Express/Koa 风格的标准做法，不是 hack</li>
</ul>
<h3>4.2 JWT 认证</h3>
<pre><code class="language-typescript">// 签发 token
const token = await sign({ username: user.username }, secret, &#39;HS256&#39;)

// 验证 token
const payload = await verify(token, secret, &#39;HS256&#39;)
</code></pre>
<ul>
<li>使用 <strong>hono/jwt</strong> 的 <code>sign()</code> 和 <code>verify()</code></li>
<li><strong>必须显式指定 <code>&#39;HS256&#39;</code> 算法</strong>，否则 verify 会失败（与 jsonwebtoken 库不同）</li>
<li>Token 无过期时间（管理员登录长期有效）</li>
<li><code>authGuard</code> 中间件拦截所有写操作（文章/POST/DELETE、评论删除、留言删除、个人资料修改）</li>
</ul>
<h3>4.3 认证流程</h3>
<pre><code>1. POST /api/auth/login { username, password }
2. 服务端查 users 表 → PBKDF2 密码验证
3. 验证通过 → sign({ username }, JWT_SECRET, &#39;HS256&#39;) → 返回 token
4. 客户端 localStorage.setItem(&#39;token&#39;, token)
5. 后续请求自动在 Authorization 头注入 Bearer token
6. authGuard 中间件 verify() 验证 token
</code></pre>
<h3>4.4 数据库 — D1</h3>
<p>D1 是 Cloudflare 的边缘 SQLite 数据库，语法与 SQLite 完全兼容。</p>
<p><strong>数据表设计：</strong></p>
<ul>
<li><code>posts</code> — 博客文章 (title, content, summary, timestamps)</li>
<li><code>tags</code> — 标签 (name, UNIQUE)</li>
<li><code>post_tags</code> — 文章-标签多对多关联</li>
<li><code>comments</code> — 文章评论 (author, content, post_id FK)</li>
<li><code>guestbook</code> — 留言板 (author, content)</li>
<li><code>profile</code> — 个人资料单例 (id=1, name, bio, avatar, layout)</li>
<li><code>users</code> — 管理员账户 (username, password_hash)</li>
</ul>
<p><strong>头像存储：</strong> 直接存 base64 数据 URL 在 profile 表的 <code>avatar</code> 列，无需文件存储。适合小尺寸头像（&lt;100KB）。</p>
<h3>4.5 SPA 路由回退</h3>
<p>生产环境中，所有非 <code>/api/*</code> 的请求都应该返回 <code>index.html</code>，由 React Router 处理客户端路由。</p>
<ul>
<li><strong>Cloudflare Pages 自动处理</strong> SPA 回退</li>
<li>后端无需任何 <code>_redirects</code> 或 catch-all 规则（Pages Functions 不匹配的路径自动走静态文件）</li>
<li>开发模式下由 Vite 处理</li>
</ul>
<h3>4.6 前端 API 客户端</h3>
<p><code>client/src/api.js</code> 统一封装：</p>
<pre><code class="language-javascript">const API_BASE = &#39;/api&#39;  // 相对路径，生产环境走同源

async function request(path, options = {}) {
  const token = localStorage.getItem(&#39;token&#39;)
  const headers = {
    &#39;Content-Type&#39;: &#39;application/json&#39;,
    ...(token &amp;&amp; { Authorization: `Bearer ${token}` })
  }
  const res = await fetch(`${API_BASE}${path}`, { ...options, headers })
  const data = await res.json()
  if (!res.ok) throw new Error(data.error)
  return data
}
</code></pre>
<ul>
<li>所有请求走同源 <code>/api/...</code>，无需 CORS 配置</li>
<li>Token 自动注入，401 时前端提示重新登录</li>
</ul>
<hr>
<h2>五、迁移路线图</h2>
<h3>原始架构 vs 目标架构</h3>
<table>
<thead>
<tr>
<th>组件</th>
<th>原始 (Bun + VPS)</th>
<th>目标 (Cloudflare)</th>
</tr>
</thead>
<tbody><tr>
<td>运行时</td>
<td>Bun + Express</td>
<td>Pages Functions (V8 Isolate)</td>
</tr>
<tr>
<td>数据库</td>
<td>SQLite 文件 (bun:sqlite)</td>
<td>D1 (边缘 SQLite)</td>
</tr>
<tr>
<td>文件存储</td>
<td>本地 <code>/uploads</code> 目录</td>
<td>base64 存数据库</td>
</tr>
<tr>
<td>部署</td>
<td>Cloudflare Tunnel</td>
<td>直接部署到 CDN</td>
</tr>
<tr>
<td>认证</td>
<td>jsonwebtoken + PBKDF2</td>
<td>hono/jwt + Web Crypto PBKDF2</td>
</tr>
</tbody></table>
<h3>迁移步骤</h3>
<h4>第一步：剥离后端（最关键的改造）</h4>
<p>原始代码中，Express 服务器同时负责 API 和静态文件托管：</p>
<pre><code>server/src/index.js → Express app
  ├── /api/* 路由
  ├── 静态文件托管 client/dist/
  └── SPA fallback
</code></pre>
<p>需要拆分为两层：</p>
<pre><code>Cloudflare Pages
  ├── Pages Functions (/api/*)  → 替代 Express 路由
  └── 静态文件 (client/dist/)   → 替代 Express 静态托管
</code></pre>
<p><strong>主要挑战：</strong></p>
<ol>
<li><p><strong>Express → Hono 重写</strong></p>
<ul>
<li>Express <code>req.body</code> → Hono <code>c.req.json()</code></li>
<li>Express <code>req.params</code> → Hono <code>c.req.param(&#39;key&#39;)</code></li>
<li>Express <code>res.json()</code> → Hono <code>return c.json()</code></li>
<li>Express <code>res.status()</code> → Hono <code>c.json({...}, 400)</code></li>
<li>Express 中间件签名 <code>(req, res, next)</code> → Hono 中间件签名 <code>(c, next)</code></li>
</ul>
</li>
<li><p><strong>bun:sqlite → D1</strong></p>
<ul>
<li><code>db.query(&#39;SELECT * FROM posts&#39;).all()</code> → <code>c.env.DB.prepare(&#39;SELECT * FROM posts&#39;).all()</code></li>
<li><code>db.run()</code> → <code>c.env.DB.prepare().run()</code></li>
<li>参数绑定：<code>?</code> 占位符在 D1 中同样支持</li>
</ul>
</li>
<li><p><strong>文件上传 → base64</strong></p>
<ul>
<li>原始用 <code>multer</code> 中间件处理 <code>multipart/form-data</code></li>
<li>改为前端将图片转为 base64 data URL，通过 JSON body 发送</li>
<li>好处：无需文件存储，无需处理 FormData</li>
</ul>
</li>
<li><p><strong>jsonwebtoken → hono/jwt</strong></p>
<ul>
<li><code>jwt.sign()</code> → <code>sign()</code> (hono/jwt)</li>
<li><code>jwt.verify()</code> → <code>verify()</code> (hono/jwt)</li>
<li><strong>关键差异：</strong> hono/jwt 的 verify 必须传入算法参数 <code>&#39;HS256&#39;</code>，否则静默失败</li>
</ul>
</li>
<li><p><strong>Bun PBKDF2 → Web Crypto PBKDF2</strong></p>
<ul>
<li><code>Bun.password.hash()</code> / <code>Bun.password.verify()</code> → Web Crypto API <code>crypto.subtle.deriveBits()</code></li>
<li>原因：Pages Functions 运行在 V8 Isolate 中，没有 Bun 的密码 API</li>
</ul>
</li>
<li><p><strong>Schema 迁移</strong></p>
<ul>
<li>从 <code>server/data/blog.db</code> 提取 SQLite schema</li>
<li>写出 <code>schema.sql</code></li>
<li>通过 <code>wrangler d1 execute --remote --file=schema.sql</code> 应用到 D1</li>
</ul>
</li>
</ol>
<h4>第二步：前端适配</h4>
<p>前端改造相对简单：</p>
<ul>
<li>API_BASE 从 <code>http://localhost:3000/api</code> 改为 <code>/api</code>（同源请求）</li>
<li>移除 FormData 上传，改用 base64</li>
<li>其他代码基本不变（React Router、组件、样式全部保留）</li>
</ul>
<h4>第三步：部署配置</h4>
<ol>
<li>创建 <code>wrangler.toml</code>，绑定 D1 数据库</li>
<li>创建 <code>.dev.vars</code>，配置本地开发环境变量</li>
<li>设置 <code>package.json</code> 脚本（dev/build/deploy）</li>
</ol>
<h4>第四步：生产部署</h4>
<pre><code class="language-bash">bun run build                              # Vite 构建前端
bunx wrangler pages deploy client/dist     # 部署到 Cloudflare Pages
</code></pre>
<hr>
<h2>六、本地开发</h2>
<h3>开发架构</h3>
<pre><code>┌─────────────────────────────┐
│   Vite Dev Server :5173      │
│   (React HMR + 代理)         │
│                              │
│   /api/* → localhost:8788 ───┼──┐
└──────────────────────────────┘  │
                                   ▼
┌─────────────────────────────┐
│   Wrangler Dev :8788         │
│   (Pages Functions + D1)     │
│                              │
│   本地 D1 模拟 (SQLite)       │
│   数据目录: .wrangler/state/  │
└─────────────────────────────┘
</code></pre>
<h3>开发命令</h3>
<pre><code class="language-bash">bun install                    # 安装依赖
bun run db:local               # 初始化本地 D1 并建表
bun run dev                    # 同时启动 Vite + Wrangler
</code></pre>
<p>Vite 配置中 <code>proxy: { &#39;/api&#39;: &#39;http://localhost:8788&#39; }</code> 将 API 请求转发到 Wrangler 模拟的 Pages Functions。</p>
<h3>环境变量</h3>
<p>本地开发环境变量在 <code>.dev.vars</code> 中定义，Pages Functions 在本地运行时会自动加载（无需 <code>dotenv</code>）。</p>
<hr>
<h2>七、为什么这样设计</h2>
<h3>函数入口的单文件模式</h3>
<p><code>functions/api/[[path]].ts</code> 是整个 API 的唯一入口，而不是每个端点一个文件（如 <code>functions/api/posts.ts</code>、<code>functions/api/profile.ts</code>）。</p>
<p><strong>原因：</strong></p>
<ul>
<li><strong>Hono 路由树</strong>：Hono 在单个 app 实例中管理所有路由，路由匹配效率高</li>
<li><strong>共享中间件和工具函数</strong>：认证中间件、密码哈希、数据库操作都在同一个文件中，无需跨文件导入</li>
<li><strong>部署简单</strong>：只有一个 Function 入口，冷启动后所有路由共享同一个 V8 Isolate 实例</li>
<li><strong>符合 Cloudflare 最佳实践</strong>：Workers 官方推荐使用 Hono/Itsy 等框架，而不是每个路由一个文件</li>
</ul>
<h3>为什么不用 Workers + KV？</h3>
<ul>
<li><strong>Workers 单独部署</strong> 意味着要管理两个独立项目（Pages + Workers），并处理跨域</li>
<li><strong>Pages Functions 与 Pages 一体</strong>，部署时无需额外配置</li>
<li><strong>D1 比 KV 更适合关系型数据</strong>：文章、标签、评论需要 JOIN 和关联查询</li>
<li>KV 适合键值缓存（如 URL 重定向），不适合复杂查询</li>
</ul>
<h3>头像为什么用 base64 而不是文件上传？</h3>
<ul>
<li>Pages Functions 本身不提供持久化文件存储</li>
<li>使用 R2（Cloudflare 的对象存储）会增加架构复杂度</li>
<li>base64 直接存数据库，读写简单，适合小尺寸头像</li>
<li>避免 FormData 解析和 multipart 处理</li>
</ul>
<hr>
<h2>八、费用分析</h2>
<p>假设月均 PV 50,000 次：</p>
<table>
<thead>
<tr>
<th>资源</th>
<th>用量</th>
<th>免费额度</th>
<th>是否超额</th>
</tr>
</thead>
<tbody><tr>
<td>静态请求</td>
<td>45,000 次</td>
<td>无限</td>
<td>✗</td>
</tr>
<tr>
<td>API 请求</td>
<td>5,000 次</td>
<td>100,000/天</td>
<td>✗</td>
</tr>
<tr>
<td>D1 读</td>
<td>~15,000 次</td>
<td>50 次/秒</td>
<td>✗</td>
</tr>
<tr>
<td>D1 写</td>
<td>~500 次</td>
<td>25 次/秒</td>
<td>✗</td>
</tr>
<tr>
<td>存储</td>
<td>~10MB</td>
<td>5GB</td>
<td>✗</td>
</tr>
</tbody></table>
<p><strong>结论：个人博客完全不需要付费。</strong></p>
<hr>
<h2>九、常见问题</h2>
<h3>Q: 为什么不直接用 GitHub Pages？</h3>
<p>A: GitHub Pages 只能托管静态文件，无法运行服务端代码。博客需要后端 API（文章管理、认证、评论等），必须使用能运行服务端逻辑的平台。</p>
<h3>Q: Pages Functions 冷启动慢吗？</h3>
<p>A: Hono 框架极轻量（约 12KB），冷启动通常在 50-200ms。相比 Express（约 2MB），Hono 在 Serverless 场景有明显优势。</p>
<h3>Q: D1 数据库丢失了怎么办？</h3>
<p>A: D1 自动多副本存储，且有自动备份。可通过 <code>wrangler d1 backup</code> 手动创建备份。</p>
<h3>Q: 如何绑定自定义域名？</h3>
<p>A: 在 Cloudflare Dashboard → Workers &amp; Pages → 项目 → 自定义域中添加。要求域名的 DNS 托管在 Cloudflare 上，HTTPS 证书自动颁发。</p>
<h3>Q: Token 过期了怎么办？</h3>
<p>A: 当前 JWT 没有设置过期时间。登出时清除 localStorage 即可。如果需要过期机制，在 <code>sign()</code> 时添加 <code>exp</code> 负载即可。</p>
<hr>
<h2>十、总结</h2>
<p>这套架构的本质是：</p>
<blockquote>
<p><strong>将传统 VPS 上的 monolithic 应用拆分为 Cloudflare Serverless 生态的标准架构：Pages（静态）+ Pages Functions（API）+ D1（数据库）。</strong></p>
</blockquote>
<p>核心改造点：</p>
<ol>
<li>Express → Hono（适配 Pages Functions 运行时）</li>
<li>bun:sqlite → D1（切换到边缘 SQLite）</li>
<li>jsonwebtoken → hono/jwt（无 Node.js 依赖的 JWT）</li>
<li>文件上传 → base64（避免文件存储）</li>
<li>Bun PBKDF2 → Web Crypto PBKDF2（兼容 V8 Isolate）</li>
</ol>
<p>改造完成后实现：</p>
<ul>
<li><strong>零成本</strong> 运行，无需付费计划</li>
<li><strong>全球 CDN</strong> 加速，延迟更低</li>
<li><strong>零运维</strong>，无需管理服务器、证书、进程</li>
<li><strong>自动扩展</strong>，流量增长无需干预</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[你好世界]]></title>
      <link>https://blog-d2z.pages.dev/post/1</link>
      <guid>https://blog-d2z.pages.dev/post/1</guid>
      <pubDate>Wed, 01 Jul 2026 23:29:49 GMT</pubDate>
      <description><![CDATA[# 第一篇博客

这是我的第一篇博客文章。]]></description>
      <content:encoded><![CDATA[<h1>第一篇博客</h1>
<p>这是我的第一篇博客文章。</p>
]]></content:encoded>
    </item>
</channel>
</rss>