六维教程

EdgeOne Makers 最佳实践

前面学了那么多 EdgeOne Makers 的功能,写代码、搭数据库、部署上线都没问题。但真要把项目丢到生产环境跑起来,你大概率会遇到一堆问题。

项目文件越堆越乱怎么组织? 接口慢了怎么排查? API Key 放代码里被爬了怎么办? 账单突然暴涨怎么控制? 这些问题靠”能跑就行”是搞不定的。

今天这篇文章就把实战里踩过的坑和总结的经验一次性讲清楚,覆盖六个方面: 项目结构、性能优化、安全防护、成本控制、常见问题、生产部署。你可以当速查手册用,遇到问题翻一翻。

项目结构最佳实践

刚开始写 demo 的时候,把所有东西塞一个文件里没啥问题。一旦项目超过 10 个页面、20 个接口,目录乱了你找代码都找不到。

推荐的项目结构

先看一个比较通用的目录组织方式:

my-edgeone-project/
├── edgeone.json            # 项目配置文件
├── functions/              # Edge Functions 云函数目录
│   ├── api/                # API 接口按业务分组
│   │   ├── user/
│   │   │   ├── login.js    # 登录接口
│   │   │   └── profile.js  # 用户资料接口
│   │   └── article/
│   │       ├── list.js     # 文章列表
│   │       └── detail.js   # 文章详情
│   ├── middleware/          # 中间件
│   │   ├── auth.js         # 鉴权中间件
│   │   └── logger.js       # 日志中间件
│   └── utils/              # 工具函数
│       ├── response.js     # 统一响应格式
│       └── validate.js     # 参数校验
├── public/                 # 静态资源
│   ├── css/
│   ├── js/
│   └── images/
└── database/               # 数据库迁移脚本
    ├── migrations/
    └── seeds/

文件命名规范

类型 命名规则 示例
云函数文件 小写加横线 user-login.js
中间件 功能名加 .js auth.js
工具函数 功能名加 .js response.js
静态资源 和前端框架保持一致 main.css

统一响应格式

项目里每个接口返回的数据格式应该统一,前端才好处理:

// functions/utils/response.js

// 成功响应
export function success(data, message = 'ok') {
  return new Response(
    JSON.stringify({
      code: 0,
      message,
      data
    }),
    {
      headers: {
        'Content-Type': 'application/json'
      }
    }
  );
}

// 失败响应
export function fail(message = 'error', code = -1, httpStatus = 400) {
  return new Response(
    JSON.stringify({
      code,
      message,
      data: null
    }),
    {
      status: httpStatus,
      headers: {
        'Content-Type': 'application/json'
      }
    }
  );
}

用的时候直接引入:

// functions/api/user/login.js
import { success, fail } from '../../utils/response.js';

export default async function handler(request, env) {
  try {
    const body = await request.json();

    if (!body.username || !body.password) {
      return fail('用户名和密码不能为空');
    }

    // ... 业务逻辑

    return success({ token: 'xxx' }, '登录成功');
  } catch (e) {
    return fail('服务器内部错误', -1, 500);
  }
}

统一返回格式之后,前端只要判断 code === 0 就知道成功了,不用每个接口单独处理。

性能优化技巧

接口响应慢是用户流失的头号原因。EdgeOne Makers 跑在边缘节点上,本身就比传统服务器快不少,但还是有优化空间。

缓存策略

不是所有数据都需要每次都从数据库查。能缓存的就缓存起来:

// functions/api/article/list.js
export default async function handler(request, env) {
  // 先从缓存读
  const cacheKey = new URL(request.url).toString();
  const cache = caches.default;
  let response = await cache.match(cacheKey);

  if (!response) {
    // 缓存没有,查数据库
    const articles = await env.DB.prepare(
      'SELECT id, title, summary FROM articles ORDER BY created_at DESC LIMIT 20'
    ).all();

    response = new Response(
      JSON.stringify({ code: 0, data: articles.results }),
      {
        headers: {
          'Content-Type': 'application/json',
          // 缓存 5 分钟
          'Cache-Control': 'public, max-age=300'
        }
      }
    );

    // 写入缓存
    await cache.put(cacheKey, response.clone());
  }

  return response;
}

缓存时间怎么选,看数据变化频率:

数据类型 建议缓存时间 说明
文章列表 5 分钟 更新不频繁
用户资料 1 分钟 用户可能修改
配置数据 1 小时 几乎不变
实时数据 不缓存 必须每次查最新

数据库查询优化

数据库查询慢是最常见的问题,几个要注意的点:

-- 加索引,查询速度能快几十倍
CREATE INDEX idx_articles_category ON articles(category_id);

-- 只查需要的字段,别用 SELECT *
SELECT id, title, summary FROM articles WHERE category_id = 1;

-- 分页查询用游标,比 OFFSET 快
SELECT id, title FROM articles
WHERE id > 100
ORDER BY id
LIMIT 20;

图片优化

如果你的项目有图片,图片体积是影响加载速度的重要因素:

// functions/middleware/image-optimize.js
export default async function handler(request, env) {
  const url = new URL(request.url);

  // 只处理图片路径
  if (!url.pathname.startsWith('/images/')) {
    return new Response('Not an image', { status: 404 });
  }

  // 获取原图
  const response = await env.ASSETS.fetch(request);
  const image = await response.arrayBuffer();

  // 根据请求的 Accept 头决定输出格式
  const accept = request.headers.get('Accept') || '';
  let outputFormat = 'jpeg';

  if (accept.includes('webp')) {
    outputFormat = 'webp';
  } else if (accept.includes('avif')) {
    outputFormat = 'avif';
  }

  // 返回优化后的图片
  return new Response(image, {
    headers: {
      'Content-Type': `image/${outputFormat}`,
      'Cache-Control': 'public, max-age=86400'
    }
  });
}
优化手段 效果 适用场景
WebP/AVIF 格式 体积减少 50%+ 现代浏览器
设置长缓存 二次访问秒开 不常变的图片
懒加载 首屏加载快 页面下方图片
压缩质量到 80% 肉眼无差别,体积减半 大部分场景

安全最佳实践

安全这事儿,不出事的时候觉得无所谓,出事了就是大事。API Key 泄露、SQL 注入、接口被刷,每一个都能让你半夜被叫醒。

密钥管理

最最重要的一条: 任何密钥都不要写死在代码里。

// ❌ 错误示范 - 密钥写在代码里
const API_KEY = 'sk-abc123xyz456';
const DB_PASSWORD = 'my_super_secret_password';

// ✅ 正确做法 - 使用环境变量
export default async function handler(request, env) {
  const apiKey = env.API_KEY;        // 从环境变量读取
  const dbPassword = env.DB_PASSWORD;
  // ...
}

EdgeOne Makers 的环境变量在控制台的项目设置里配置,支持加密存储,代码里只能通过 env.XXX 读取,不会被下载或泄露。

参数校验

永远不要信任用户输入的数据:

// functions/utils/validate.js

// 校验用户输入
export function validateUserInput(data) {
  const errors = [];

  // 用户名: 只允许字母数字下划线,3-20 位
  if (!data.username || !/^[a-zA-Z0-9_]{3,20}$/.test(data.username)) {
    errors.push('用户名格式不正确');
  }

  // 邮箱: 基本格式校验
  if (!data.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
    errors.push('邮箱格式不正确');
  }

  // 内容: 不能为空,最长 5000 字
  if (!data.content || data.content.length > 5000) {
    errors.push('内容不能为空且不超过 5000 字');
  }

  return errors;
}

用的时候:

import { validateUserInput } from '../utils/validate.js';

export default async function handler(request, env) {
  const body = await request.json();
  const errors = validateUserInput(body);

  if (errors.length > 0) {
    return new Response(
      JSON.stringify({ code: -1, message: errors.join('; '), data: null }),
      {
        status: 400,
        headers: { 'Content-Type': 'application/json' }
      }
    );
  }

  // 校验通过,继续处理...
}

防 SQL 注入

用参数化查询,不要用字符串拼接 SQL:

// ❌ 危险! 用户输入直接拼进 SQL
const query = `SELECT * FROM users WHERE name = '${req.body.name}'`;
// 如果 name 传入 "' OR '1'='1" 就完蛋了

// ✅ 安全! 用参数化查询
const result = await env.DB.prepare(
  'SELECT * FROM users WHERE name = ?'
).bind(req.body.name).first();

请求限流

防止接口被恶意刷:

// functions/middleware/rate-limit.js

// 简单的内存限流,适合小规模项目
const requestCounts = new Map();

export default async function rateLimit(request, env) {
  const ip = request.headers.get('cf-connecting-ip') || 'unknown';
  const now = Date.now();
  const windowMs = 60 * 1000; // 1 分钟窗口
  const maxRequests = 30;      // 最多 30 次

  if (!requestCounts.has(ip)) {
    requestCounts.set(ip, { count: 0, startTime: now });
  }

  const record = requestCounts.get(ip);

  // 窗口过期就重置
  if (now - record.startTime > windowMs) {
    record.count = 0;
    record.startTime = now;
  }

  record.count++;

  if (record.count > maxRequests) {
    return new Response(
      JSON.stringify({ code: -1, message: '请求太频繁,请稍后再试' }),
      {
        status: 429,
        headers: {
          'Content-Type': 'application/json',
          'Retry-After': '60'
        }
      }
    );
  }

  // 放行,继续处理
  return null;
}

安全头

给每个响应加上安全相关的 HTTP 头:

// functions/middleware/security-headers.js
export default async function handler(request, env, ctx) {
  const response = await ctx.next();

  // 设置安全头
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-XSS-Protection', '1; mode=block');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

  return response;
}

各安全头的作用:

安全头 作用 防什么
X-Content-Type-Options 禁止浏览器猜测文件类型 MIME 嗅探攻击
X-Frame-Options 禁止页面被 iframe 嵌套 点击劫持
X-XSS-Protection 开启浏览器 XSS 过滤 跨站脚本攻击
Referrer-Policy 控制 Referer 头发送策略 信息泄露

成本控制建议

EdgeOne Makers 免费额度很够用,但不注意的话超额是分分钟的事。

免费额度对比

资源 免费额度 超出后计费
Functions 调用次数 100 万次/月 按次计费
D1 数据库读取 50 亿行/月 按行数计费
D1 数据库写入 1 亿行/月 按行数计费
D1 存储空间 5 GB 按 GB 计费
静态资源流量 无限制 免费

减少数据库读取

数据库读取是最容易超额的。几个实用技巧:

// ❌ 循环里查数据库 - 100 篇文章就是 100 次查询
for (const article of articles) {
  const author = await env.DB.prepare(
    'SELECT name FROM users WHERE id = ?'
  ).bind(article.author_id).first();
  article.author = author;
}

// ✅ 一条 SQL 搞定 - 用 JOIN 一次性查完
const result = await env.DB.prepare(`
  SELECT a.*, u.name as author_name
  FROM articles a
  LEFT JOIN users u ON a.author_id = u.id
  ORDER BY a.created_at DESC
  LIMIT 20
`).all();
优化手段 节省效果 实现难度
JOIN 代替循环查询 节省 90%+ 读取
缓存热点数据 节省 50%+ 读取
分页而不是全量拉取 节省 80%+ 读取
前端去重请求 节省 30%+ 调用

减少 Functions 调用

// ❌ 每次请求都触发函数
// 前端: 页面加载请求一次,滚动请求一次,切换 tab 又请求一次

// ✅ 前端做好请求合并和防抖
// 用 debounce 防抖,用户停止操作后才发请求
let timer = null;
function searchArticles(keyword) {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    const res = await fetch(`/api/article/search?q=${keyword}`);
    // ...
  }, 500); // 停止输入 500ms 后才请求
}

监控用量

edgeone.json 里可以设置告警:

{
  "name": "my-project",
  "alerts": {
    "function_invocations": {
      "threshold": 800000,
      "action": "notify"
    },
    "d1_reads": {
      "threshold": 4000000000,
      "action": "notify"
    }
  }
}

建议把告警阈值设在免费额度的 80%,留个缓冲。

常见问题与解决方案

开发过程中踩过的坑,列出来帮你省时间。

问题一: 环境变量读不到

// 代码里这样写,结果拿到 undefined
const key = env.MY_SECRET_KEY;

// 原因: 环境变量的名字写错了,或者没在当前环境配置
// 解决步骤:
// 1. 去控制台检查变量名有没有拼写错误
// 2. 确认变量在 dev 还是 prod 环境配置的
// 3. 本地开发用 .dev.vars 文件配置

本地开发时创建 .dev.vars 文件:

# .dev.vars (这个文件不要提交到 Git!)
API_KEY=your_local_test_key
DB_PASSWORD=local_test_password

问题二: 数据库连接超时

// 错误: 没有设置超时时间,卡住了
const result = await env.DB.prepare('SELECT * FROM articles').all();

// 正确: 加上 AbortController 控制超时
export default async function handler(request, env) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 秒超时

  try {
    const result = await env.DB.prepare(
      'SELECT id, title FROM articles LIMIT 20'
    ).all();

    clearTimeout(timeoutId);
    return new Response(JSON.stringify({ code: 0, data: result.results }), {
      headers: { 'Content-Type': 'application/json' }
    });
  } catch (e) {
    clearTimeout(timeoutId);
    if (e.name === 'AbortError') {
      return new Response(
        JSON.stringify({ code: -1, message: '查询超时,请稍后重试' }),
        { status: 504, headers: { 'Content-Type': 'application/json' } }
      );
    }
    throw e;
  }
}

问题三: CORS 跨域问题

// 前端调接口报 CORS 错误
// 解决: 在 Edge Function 里设置正确的 CORS 头

export default async function handler(request, env) {
  // 处理预检请求 (OPTIONS)
  if (request.method === 'OPTIONS') {
    return new Response(null, {
      headers: {
        'Access-Control-Allow-Origin': 'https://yourdomain.com',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400' // 预检结果缓存 1 天
      }
    });
  }

  // 正常处理请求
  const response = await handleRequest(request, env);

  // 给响应也加上 CORS 头
  response.headers.set(
    'Access-Control-Allow-Origin',
    'https://yourdomain.com'
  );

  return response;
}

注意 Access-Control-Allow-Origin 不要设成 *,指定具体的域名才安全。

问题四: 静态资源 404

// 访问 /images/logo.png 返回 404

// 检查清单:
// 1. 文件确实在 public/images/ 目录下
// 2. 文件名大小写是否一致 (Linux 区分大小写)
// 3. edgeone.json 里有没有正确配置 assets 路由

edgeone.json 里静态资源配置:

{
  "name": "my-project",
  "assets": {
    "directory": "./public",
    "binding": "ASSETS"
  }
}

问题五: 函数冷启动慢

// ❌ 全局作用域做耗时操作,每次冷启动都慢
const data = await fetch('https://api.example.com/config').then(r => r.json());

// ✅ 懒加载,第一次用到时才初始化
let configCache = null;

async function getConfig(env) {
  if (!configCache) {
    configCache = await env.CONFIG.get('app_config');
    if (!configCache) {
      configCache = await fetch('https://api.example.com/config')
        .then(r => r.json());
      await env.CONFIG.put('app_config', JSON.stringify(configCache));
    } else {
      configCache = JSON.parse(configCache);
    }
  }
  return configCache;
}

生产环境部署建议

开发环境跑通了不代表生产环境没问题,上线前把这些检查一遍。

上线前检查清单

检查项 说明 重要程度
环境变量已配置 prod 环境的所有密钥都配好 必须
数据库迁移已执行 线上数据库表结构是最新的 必须
CORS 域名已限制 不要在生产环境用 * 必须
错误处理完善 所有接口有 try-catch 必须
日志已接入 能看到线上报错信息 建议
缓存策略已设置 热点数据有缓存 建议
限流已开启 关键接口有频率限制 建议

多环境管理

开发环境和生产环境要分开,别在开发时改了线上数据:

# 本地开发 - 使用 dev 环境
npx edgeone dev

# 部署到生产 - 使用 prod 环境
npx edgeone deploy --env production

edgeone.json 里区分环境:

{
  "name": "my-project",
  "env": {
    "dev": {
      "vars": {
        "API_BASE": "https://dev-api.example.com",
        "DEBUG": "true"
      }
    },
    "production": {
      "vars": {
        "API_BASE": "https://api.example.com",
        "DEBUG": "false"
      }
    }
  }
}

日志记录

生产环境一定要有日志,不然出了问题两眼一抹黑:

// functions/middleware/logger.js
export default async function logger(request, env, ctx) {
  const start = Date.now();

  // 拿到下游处理的结果
  const response = await ctx.next();

  const duration = Date.now() - start;
  const ip = request.headers.get('cf-connecting-ip') || 'unknown';

  // 记录请求日志
  const log = {
    time: new Date().toISOString(),
    method: request.method,
    url: request.url,
    status: response.status,
    duration: `${duration}ms`,
    ip
  };

  // 错误请求重点标记
  if (response.status >= 400) {
    console.error(JSON.stringify(log));
  } else {
    console.log(JSON.stringify(log));
  }

  return response;
}

灰度发布

大版本更新建议先让一小部分流量走新版本:

// 在路由层做灰度分发
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // 10% 的流量走新版接口
    if (url.pathname.startsWith('/api/v2/')) {
      const random = Math.random();
      if (random < 0.1) {
        // 走新版逻辑
        return handleV2(request, env);
      }
    }

    // 其余走老版
    return handleV1(request, env);
  }
};

先灰度 10%,观察一两天没报错,再逐步调到 50%、100%。

速查卡片

要点 说明
统一响应格式 所有接口返回 { code, message, data } 结构
密钥用环境变量 任何密钥不写死在代码里,通过 env.XXX 读取
参数化查询 SQL 用 ? 占位符加 .bind() 绑定参数,防注入
缓存热点数据 文章列表、配置等不常变的数据加 Cache-Control
JOIN 代替循环查询 一条 SQL 搞定关联数据,减少数据库读取次数
请求限流 关键接口加频率限制,防止被恶意刷
多环境隔离 dev 和 production 的环境变量、数据库分开配置
上线前检查清单 环境变量、CORS、错误处理、日志、缓存逐项确认
上一篇
EdgeOne Makers 图片优化与重定向
下一篇
EdgeOne Makers 性能优化实践