# 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