# EdgeOne Blog 技术总结报告(真实项目开发版)
> 本文档基于根哥官网 Blog 项目真实开发过程总结,包含所有踩过的坑和解决方案
---
## 一、路由模式选择
### 1.1 两种路由模式的坑
**背景**:项目初期尝试了两种路由模式,导致路由冲突
| 模式 | 文件 | 方式 | 状态 |
|------|------|------|------|
| ~~Service Worker~~ | `edge-functions/index.js` | `addEventListener('fetch', ...)` | ❌ 已删除 |
| ✅ 文件系统路由 | `edge-functions/api/*.js` | `export async function onRequest(context)` | ✅ 最终方案 |
**踩坑记录**:
- Service Worker 模式会拦截**所有**入站请求
- 文件系统路由由 EdgeOne 运行时自动匹配路径
- **两者不能共存**:Service Worker 优先拦截,文件路由永不触发
- 表现:文章添加成功但列表不显示(Service Worker 返回空数组)
**结论**:EdgeOne Pages 项目中,`edge-functions/` 目录下应**仅使用文件系统路由**
---
## 二、KV存储使用方式(踩坑无数)
### 2.1 核心坑:env.BLOG_KV 是字符串!
**错误认知**:以为 `env.BLOG_KV` 是 KV 存储对象
**实际情况**:
```javascript
// ❌ 错误:env.BLOG_KV 只是字符串 "BLOG_KV"
const kv = env.BLOG_KV;
console.log(typeof kv); // "string"
console.log(kv); // "BLOG_KV"
// ✅ 正确:使用全局变量
const kv = globalThis.BLOG_KV;
console.log(typeof kv); // "object"
console.log(kv.put); // function
```
### 2.2 生产环境 KV 不支持 list 方法
**错误表现**:
```
kv.list is not a function
```
**原因**:EdgeOne 生产环境的 KV 只有 `get`、`put`、`delete` 三个方法
**解决方案**:维护索引键模拟 list
```javascript
// 写入时同时更新索引
export async function kvPut(kv, key, value) {
await kv.put(key, value);
const prefix = key.substring(0, key.lastIndexOf('_') + 1);
const indexKey = `_idx:${prefix}`;
const raw = await kv.get(indexKey);
const keys = raw ? JSON.parse(raw) : [];
if (!keys.includes(key)) {
keys.push(key);
await kv.put(indexKey, JSON.stringify(keys));
}
}
// 删除时同时更新索引
export async function kvDelete(kv, key) {
await kv.delete(key);
const prefix = key.substring(0, key.lastIndexOf('_') + 1);
const indexKey = `_idx:${prefix}`;
const raw = await kv.get(indexKey);
if (raw) {
const keys = JSON.parse(raw).filter(k => k !== key);
await kv.put(indexKey, JSON.stringify(keys));
}
}
```
### 2.3 KV 过期时间设置
EdgeOne KV 的 `put` 方法支持 `expirationTtl` 参数实现数据自动过期:
```javascript
// Token 自动过期示例(1小时后自动删除)
await kv.put(`token_${userId}`, tokenValue, {
expirationTtl: 3600 // 秒
});
// 会话数据自动清理
await kv.put(`session_${sessionId}`, sessionData, {
expirationTtl: 1800 // 30分钟
});
```
### 2.4 正确的 KV 封装
```javascript
// edge-functions/lib/kv.js
const memoryStorage = {};
export function getKV(env) {
// ✅ 优先使用全局变量(EdgeOne Pages 正确方式)
const globalKV = globalThis.BLOG_KV;
if (globalKV && typeof globalKV === 'object' && typeof globalKV.put === 'function') {
return globalKV;
}
// 降级:内存存储(本地开发)
// 注意:env.BLOG_KV 永远是字符串,不能作为 KV 对象使用
return {
put: async (key, value) => { memoryStorage[key] = value; },
get: async (key) => memoryStorage[key] || null,
delete: async (key) => { delete memoryStorage[key]; },
list: async ({ prefix }) => ({
keys: Object.keys(memoryStorage).filter(k => k.startsWith(prefix)).map(k => ({ name: k }))
})
};
}
```
---
## 三、Blob存储使用方式(SDK坑)
### 3.1 核心坑:Blob 不通过 env 注入!
**错误认知**:以为可以通过 `env.BLOG_BLOB` 或 `globalThis.BLOG_BLOB` 访问
**实际情况**:
- 官方 Blob **不通过 env 注入**
- 也不存在 `globalThis.BLOG_BLOB`
- `env.BLOG_BLOB` **永远是 undefined**
### 3.2 必须安装官方 SDK
```bash
# ❌ 错误:没有安装 SDK
npm install # 只有 serve,没有 @edgeone/pages-blob
# ✅ 正确:安装 SDK
npm install @edgeone/pages-blob
```
### 3.3 正确的 Blob 调用方式
```javascript
// ❌ 错误:尝试从 env 获取
const blobStorage = globalThis.BLOG_BLOB || env.BLOG_BLOB;
await blobStorage.put(blobName, fileBuffer, {...}); // undefined.put() 崩溃
// ✅ 正确:通过 SDK 获取
import { getStore } from "@edgeone/pages-blob";
export async function onRequest(context) {
const store = getStore("blog-uploads"); // 命名空间自动创建
// 设置 Content-Type(重要)
await store.set(blobName, fileBuffer, {
httpMetadata: { contentType: 'image/jpeg' }
});
// 获取数据,支持多种类型
const buffer = await store.get(blobName, { type: "arrayBuffer" });
const text = await store.get(blobName, { type: "text" });
const json = await store.get(blobName, { type: "json" });
}
```
### 3.4 store.set() 的 Content-Type 控制
```javascript
// 图片上传
await store.set(`images/${fileName}`, fileBuffer, {
httpMetadata: { contentType: 'image/png' }
});
// JSON 数据
await store.set(`data/config.json`, JSON.stringify(config), {
httpMetadata: { contentType: 'application/json' }
});
// 文件下载
await store.set(`downloads/report.pdf`, pdfBuffer, {
httpMetadata: {
contentType: 'application/pdf',
contentDisposition: 'attachment; filename="report.pdf"'
}
});
```
### 3.5 edgeone.json 中的 Blob 配置无效
```json
// ❌ 错误:这是自己发明的字段,EdgeOne 不识别
"BLOG_BLOB": { "namespace": "BLOG_BLOB", "type": "blob" }
// ✅ 正确:Blob 不需要 edgeone.json 配置,通过 SDK 使用
```
---
## 四、认证与 Token(安全坑)
### 4.1 原始 Token 格式的安全问题
**原格式**:
```javascript
const token = `eo_${Date.now()}_${adminUsername}_${adminPassword.length}`;
```
**问题**:
- Token 中包含密码长度,泄露敏感信息
- 格式固定,可预测
- 无过期机制
### 4.2 安全的 Token 生成
```javascript
// ✅ 使用密码学安全的随机数
function generateSecureToken() {
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes); // EdgeOne 支持
const token = Array.from(randomBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return `eo_token_${token}`;
}
```
### 4.3 请求频率限制
```javascript
const TOKEN_CONFIG = {
rateLimit: {
window: 60 * 1000, // 1分钟
maxRequests: 10 // 最大10次
}
};
```
---
## 五、异步函数调用(踩坑多次)
### 5.1 核心坑:async 函数缺少 await
**问题现象**:管理后台所有 API 返回 HTTP 545 错误
**错误代码**:
```javascript
// ❌ 错误:requireAuth 是 async 函数,但没有 await
export async function onRequest(context) {
const authError = requireAuth(request, env); // 返回 Promise!
if (authError) return authError; // Promise 是 truthy,总是执行
// 后续代码不会执行
}
```
**正确代码**:
```javascript
// ✅ 正确:添加 await
export async function onRequest(context) {
const authError = await requireAuth(request, env); // 返回 null 或 Response
if (authError) return authError;
// 正常执行业务逻辑
}
```
**受影响文件**:20+ 个 admin API 文件都需要修复
---
## 六、预览 URL 限制(坑)
### 6.1 POST 请求被转换为 GET
**问题**:EdgeOne 预览 URL(带 `eo_token` 参数)会:
1. 将 POST 转换为 GET
2. 清空请求 body
### 6.2 解决方案:同时支持 GET 和 POST
**后端**:
```javascript
export async function onRequest(context) {
const { request, env } = context;
const url = new URL(request.url);
// 优先从查询参数获取(预览 URL GET 模式)
let username = url.searchParams.get('username') || '';
let password = url.searchParams.get('password') || '';
// 没有查询参数时尝试从 body 读取(正常 POST 模式)
if (!username && request.method !== 'GET') {
const bodyText = await request.text();
if (bodyText) {
const body = JSON.parse(bodyText);
username = body.username || '';
}
}
}
```
**前端**:
```javascript
const loginUrl = new URL('/api/admin/login', window.location.origin);
loginUrl.searchParams.set('username', username);
loginUrl.searchParams.set('password', password);
await fetch(loginUrl.toString(), {
method: 'POST',
body: JSON.stringify({ username, password })
});
```
> ⚠️ **安全警告**:将密码放入 URL Query 参数的做法**仅限预览环境使用**。这种方式会导致密码出现在浏览器历史记录、服务器访问日志、CDN日志中,**生产环境绝对不能这样做**。生产环境应仅使用 POST body 传递敏感信息。
---
## 七、本地开发(坑)
### 7.1 serve 模块 --port 参数不兼容
**问题**:`npx edgeone pages dev` 使用 serve 模块,`--port` 参数不兼容
**解决方案**:使用自定义 server.js
```bash
npm run dev # 使用 server.js 而非 edgeone dev
```
### 7.2 无痕模式无法登录
**原因**:无痕模式下 localStorage 可能不可用
**解决方案**:
```javascript
function setStorage(key, value) {
try { localStorage.setItem(key, value); return true; }
catch (e) {
try { sessionStorage.setItem(key, value); return true; }
catch (e) { return false; }
}
}
```
---
## 八、API 响应格式
### 8.1 统一响应格式
```javascript
// ✅ 标准响应
return new Response(JSON.stringify({
code: 0, // 0=成功, -1=失败
data: result,
message: 'success'
}), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
// ❌ 错误:缺少 Content-Type
return new Response(JSON.stringify({ code: 0, data: result }));
```
### 8.2 错误响应
```javascript
return new Response(JSON.stringify({
code: -1,
message: 'Not found'
}), { status: 404 });
return new Response(JSON.stringify({
code: -1,
message: error.message
}), { status: 500 });
```
---
## 九、动态路由文件命名
### 9.1 正确的文件结构
```
edge-functions/api/
├── resources.js # GET /api/resources (列表)
├── resources/
│ └── [id].js # GET/DELETE /api/resources/{id}
```
### 9.2 获取 ID 的正确方式
```javascript
// ✅ 正确:动态获取最后一个路径段
const pathParts = url.pathname.split('/').filter(p => p);
const id = pathParts[pathParts.length - 1];
// ❌ 错误:固定索引
const id = pathParts[3]; // 路径变化时失效
```
---
## 十、部署命令
### 10.1 首次部署
```bash
# 指定项目名称
npx edgeone pages deploy --name www-ligen-cn --skip-env-sync
# 后续部署
npx edgeone pages deploy --skip-env-sync
```
### 10.2 关键参数说明
| 参数 | 说明 |
|------|------|
| `--name <project>` | 指定 EdgeOne Pages 项目名称 |
| `--skip-env-sync` | **跳过环境变量同步**,避免覆盖线上已配置的敏感变量(如密码)。此参数非常重要,不加可能导致生产环境配置被本地文件覆盖。 |
### 10.3 部署后验证
```bash
# 测试 API
curl https://blog.ligen.cn/api/about
curl https://blog.ligen.cn/api/resources
# 验证 KV
curl https://blog.ligen.cn/api/debug-kv
```
---
## 十一、环境变量配置
### 11.1 edgeone.json 配置
```json
{
"functions": {
"nodeVersion": "20",
"envVariables": {
"ADMIN_USERNAME": { "namespace": "ADMIN_USERNAME", "type": "plaintext" },
"ADMIN_PASSWORD": { "namespace": "ADMIN_PASSWORD", "type": "plaintext" }
}
}
}
```
### 11.2 本地开发 .env
```
ADMIN_USERNAME=ligen
ADMIN_PASSWORD=你的密码
```
---
## 十二、常见错误速查表
| 错误现象 | 可能原因 | 修复方法 |
|----------|----------|----------|
| HTTP 545 | async 函数缺少 await | `await requireAuth(request, env)` |
| API 404 | 路由文件缺失 | 创建 `[id].js` 文件 |
| `kv.get is not a function` | 使用了 `env.BLOG_KV` | 改用 `globalThis.BLOG_KV` |
| `kv.list is not a function` | 生产 KV 不支持 list | 使用索引键模拟 list |
| Blob 上传失败 | 没安装 SDK | `npm install @edgeone/pages-blob` |
| Blob undefined | 尝试从 env 获取 | 使用 `getStore()` SDK |
| 预览 URL 登录失败 | POST 被转 GET | 同时支持 GET 和 POST |
| 列表不显示数据 | Service Worker 拦截 | 删除 index.js,只用文件系统路由 |
---
## 十三、文件结构
```
edge-functions/
├── _middleware.js # 全局中间件(CORS)
├── routes.json # 路由配置
├── lib/
│ └── kv.js # KV 存储工具库
├── api/
│ ├── articles.js # 公开文章列表
│ ├── resources.js # 公开资源列表
│ ├── resources/
│ │ └── [id].js # 资源详情
│ ├── menu.js # 公开菜单
│ ├── about.js # 关于信息
│ ├── achievements.js # 成就展示
│ ├── banners.js # 焦点图管理
│ ├── timeline.js # 时间线数据
│ ├── ai-tools.js # AI工具导航
│ ├── blob/
│ │ └── [name].js # Blob 文件访问
│ ├── blob-direct.js # Blob 直接访问
│ ├── test-blob.js # Blob 测试接口
│ ├── debug-kv.js # KV 调试端点
│ └── admin/
│ ├── login.js # 管理员登录
│ ├── logout.js # 登出
│ ├── articles.js # 文章 CRUD
│ ├── articles/
│ │ └── [id].js # 单篇文章 CRUD
│ ├── resources.js # 资源 CRUD
│ ├── resources/
│ │ └── [id].js # 单篇资源 CRUD
│ ├── menu.js # 菜单 CRUD
│ ├── menu/
│ │ └── [id].js # 单条菜单 CRUD
│ ├── about.js # 关于设置
│ ├── upload.js # 文件上传
│ ├── banners.js # 焦点图管理
│ └── statistics.js # 统计数据
```
---
## 十四、经验总结
### 14.1 技术层面
1. **KV/Blob 必须通过全局变量或 SDK 访问**,不能通过 env
2. **async 函数必须使用 await**,否则返回 Promise 而非实际结果
3. **预览 URL 有特殊限制**,POST 会被转 GET
4. **生产 KV 不支持 list**,需要维护索引键
### 14.2 开发流程
1. **先本地测试再部署**
2. **部署后立即验证核心 API**
3. **保留部署历史以便回滚**
---
**版本**: v2.0
**日期**: 2026年6月
**平台**: EdgeOne Pages