EdgeOne Pages 提供了类似 Cloudflare Workers 的 Serverless 体系。但重要的区别是:EO 只有 KV 存储,没有 D1 数据库和 R2 对象存储。此外,还有不少难以评价的 Bug。

截止本文更新,几个严重的bug已经修复了,比如掉dev、返回ok等。我突然发现就在发文当天(2026-04-14 17:19:17)官方就更新了文档。
Edgeone Pages截至目前仍然是测试阶段
我们假设你已经有了一个正常上线的 EO Pages 项目,并使用 Git 进行管理。如果没有,请先去绑定一个空仓库并初始化一个 TypeScript 项目(没有前端并不影响 Function 的构建)。
从AI开始
如果你完全不希望了解一点原理,但手头有很多赛博黑奴和马内,可以直接前往官方的从AI开始,官方提供了MCP, Skills, .mdc, llms.txt。当然,出于我个人对代码的掌控欲,我还是强烈推荐你往下看。
EdgeOne CLI
本部分参考官方文档EdgeOne CLI。
安装 CLI
将 Pages 项目克隆到本地后,使用以下命令全局安装 EdgeOne CLI:
npm install -g edgeoneNOTEEdgeOne CLI代替node项目原本的dev。它通过查找项目
package.json中的dev获知原项目本身的dev命令,然后它在启动dev时,同时启动自身(functions)的dev环境和原项目的dev环境。如果你配置了edgeone.json中的devCommand参数,它会优先读取。
工作流
-
(非必需) 运行
edgeone pages init初始化项目和实例。 -
(必需) 通过
edgeone login登录,你Pages在哪就选哪个站。 -
(强烈建议) 通过
edgeone pages link绑定Pages项目,这可以同步环境变量,并且允许使用KV存储。 -
最后,运行
edgeone pages dev开始本地调试。
踩坑第一关:dev不动
Edge Functions 调试服务有启动次数限制,因此尽量避免频繁退出启动 dev 服务(dev 服务内热更新不会增加启动次数)。如果多次重启 Dev 导致报错
566,除开账号抽风,通常就是触发了限制。建议此时去点杯奶茶,冷静一会再回来。第二关:输出不了
EO CLI环境下,
console.log无法通过控制台输出。不知道什么毛病,只能放响应里返回。第三关:ok你个头啊我响应呢
调试过程中,可能遇到API返回
ok,但没有真实返回值的情况发生。这种情况可能出自文件在调试过程中被新创建、调试时间过长、网络中断过的情况。总之,该情况只能说明文件现在成功被构建了(否则返回api内部错误),且除了重启dev我目前没有什么好办法,这也是整个EO Func我最诟病的一点,让我强制消耗掉本就不多的dev次数。如果你有办法可以评论说明awa官方文档强调:“切记请勿在 edgeone.json 或 package.json 中配置 edgeone pages dev!”。尊重不理解。
Edge Function
本部分参考官方文档Edge Functions。
在仓库根目录创建一个edge-functions/文件夹。接着创建一个hello.ts(强烈建议用ts,js有未知Bug):
export default function onRequest(context) { return new Response('Hello from Edge Functions!');}这将从网站根目录起的/hello创建一个API。
路由规则
Edge Functions 基于 /edge-functions 目录结构生成访问路由。
| 文件路径 | 路由 |
|---|---|
/edge-functions/index.ts | example.com/ |
/edge-functions/hello-pages.ts | example.com/hello-pages |
/edge-functions/helloworld.ts | example.com/helloworld |
/edge-functions/api/users/list.ts | example.com/api/users/list |
/edge-functions/api/users/geo.ts | example.com/api/users/geo |
/edge-functions/api/users/[id].ts | example.com/api/users/1024 |
/edge-functions/api/visit/index.ts | example.com/api/visit |
/edge-functions/api/[[default]].ts | example.com/api/books/list、example.com/api/books/1024、example.com/api/… |
尾部斜杠的说明官方文档称:“路由尾部斜杠 / 是可选……如果与静态资源路由冲突,优先被路由到静态资源。”
现实情况是: 如果你的前端项目包含了同名路由,
/hello-pages/极有可能被误判为静态资源从而返回404。带尾斜杠的行为非常不可控,强烈建议在请求时永远不要带尾部斜杠。同时出于以上问题,无法保证
/api/hello/index.ts和/api/hello.ts的实际映射,尽管其他框架可能按事实标准分别映射到/api/hello/和/api/hello。避免混用/api/hello/index.ts和/api/hello.ts。另外,路由大小写敏感。
Function Handlers
通过在.ts文件中export如下各方法来Handle对应的HTTP请求
| Handlers 方法 | 描述 |
|---|---|
onRequest(context: EventContext): Response | Promise<Response> | 匹配 HTTP Methods (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS) |
onRequestGet(context: EventContext): Response | Promise<Response> | 匹配 HTTP Methods (GET) |
onRequestPost(context: EventContext): Response | Promise<Response> | 匹配 HTTP Methods (POST) |
onRequestXxx | …… |
同时,支持同步和异步方法。注意CPU执行时间(200ms)的限制(无CPU异步如网络请求不算在内)。
NOTE不导出任何有效Function Handlers的文件仍是项目的一部分,仅会使API不会被创建。
后续文档里的类型不全不提,有的还混有错误ts语法。我自己总结了一套能用的TS类型定义,可以放在上下文里辅助AI用。
// 匹配所有 HTTP Method exporttype EOFuncHandler = (context: EventContext) => Response | Promise<Response>;
export interface EventContext { // 传递给Function Handlers的上下文 request: Request; // 客户端请求对象 Request。 params: Record<string, string | any>; // 动态路由 /edge-functions/api/users/[id].js 参数值。}KV 存储
本部分参考官方文档KV 边缘存储。
基本概念
账户:账户是Pages业务中KV用量统计和计费的最小单位。一个EO Pages账户对应一个KV账户,用户在控制台页面开通后创建。账户存储容量 1GB。
命名空间:数据隔离的唯一容器。一个账户最多10个命名空间。
键值对:键值对是一种用户存储数据的结构,其中每个数据项由两个部分组成:键(Key)和值(Value)。键是唯一的标识符,值是与键相关联的数据。
虽然官方文档没提,但如cf地,每个键值对可以独立设置过期时间。
变量名:你的代码不直接通过“命名空间名”访问数据,而是通过“变量名”访问。
变量名是在项目中使用该命名空间的环境变量名称。在使用命名空间数据之前,需要先将命名空间跟EO Pages项目进行绑定。
举例来说,你可以将变量名KV_EXAMPLE绑定命名空间production,也可以绑定test,代码无需变动即可切换命名空间。但在两个项目A和B中分别使用两个不同变量名KV_A和KV_B绑定同一个命名空间production,那么这个命名空间production会同时被KV_A和KV_B使用。
初始化账户
初始化账户的腾讯云UI操作请查看#快速开始。
使用KV
定义变量名后直接在代码中使用该变量名即可。官方代码大量使用ts ignore掩耳盗铃。此处是我自行总结的类型定义,直接复制下面这段代码即可获得完美的类型提示,可以供AI理解:
// 匹配所有 HTTP Method exportexport type EOFuncHandler = ( context: EventContext,) => Response | Promise<Response>;
export interface EventContext { // 传递给Function Handlers的上下文 request: Request; // 客户端请求对象 Request。 params: Record<string, string | unknown>; // 动态路由如 /edge-functions/api/users/[id].js 的参数值。}
export interface EO_KV { put( // 写入 KV 数据,用于创建新的键值对或者更新已有键值对的值。 key: string, // 需要创建或更新的键,长度小于等于 512 B,仅支持数字、字母及下划线。仅支持单个键。 value: string | ArrayBuffer | ArrayBufferView | ReadableStream, // 需要写入的数据,数据长度小于等于 25 MB。 options?: { expirationTtl?: number }, // 可以使用 expirationTtl 来指定过期时间,单位为秒,必须大于60s。 ): Promise<void>; // 返回一个 Promise,需要 await 该 Promise 以验证写入是否成功。 get(key: string): Promise<string | null>; get(key: string, options: { type: "text" }): Promise<string | null>; get(key: string, options: { type: "json" }): Promise<object | null>; get( // 根据特定的 key 读取 KV 中的数据,并根据指定类型返回数据。 key: string, // 指定获取数据的 key。暂不支持批量获取。 config?: { type: string }, // 用于指定返回的 value 类型,默认为 text。 // text:String,将 value 转成 string 的形式返回。 // json: Object,用于将 json 反序列化为 object 形式返回。 // arrayBuffer: ArrayBuffer,用于将二进制的 value 转换为 ArrayBuffer 返回。 // stream:ReadableStream,通常用于 value 较大的场景。 ): Promise<string | object | ArrayBuffer | ArrayBufferView | ReadableStream>; // 返回一个 Promise。如果 key 不存在或 value 为空时,则返回 null。 delete( // 从 KV 中删除指定的 key。 key: string, // 需要删除的键值对的 key。 ): Promise<void>; // 返回一个 Promise,需要 await 该 Promise 以验证删除是否成功。 list(config: { // 用于遍历 KV 中的所有 key。 prefix?: string; // 用于过滤指定前缀的键。默认为空时,按照字典序排序返回。 limit?: number; // 返回的 key 的最大数量,用于分页。默认值为 256,上限为 256。 cursor?: string; // 游标,从指定键开始遍历,用于分页。默认为空。 }): Promise<null | { complete: boolean; // 标记 list 操作是否完成,true 完成,false 未完成。 cursor: string | null; // 游标,为下一页的第一个 key,当 list 遍历完成时为 null。 keys?: Array<{ // 描述每个 key 的 object 数组。如果没有查询到任何 key,keys 为空。 key: string; // 键值对的 key。 expirationTtl: number; // 过期时间,单位为秒。 expiration: number; // 过期时间戳(秒戳) meta: string; // ? 未知 }>; }>;}通过以下代码即可定义一个提示,不影响实际运行。
declare const MY_KV: EO_KV;其中MY_KV是你绑定到Pages项目的的变量名。
另外,测试得expiration参数似乎不怎么好使。且传入额外的参数不会报错,此处原因未知,建议避免使用。
关于KV N+1查询问题
显然,get 方法只支持传入单个 Key,不支持传入数组进行批量查询。当你使用 list 获取了一堆 Key 之后,不可避免地会遇到 N+1 查询问题。
我在一个官方示例项目中找到了官方解决方案:强行Promise.all。我猜测这个KV读取很快
不过这么说,list上限只有256是不是就有点耐人寻味了?
const page = await my_kv.list({ prefix: '', cursor, limit: 10,});const keys = Array.isArray(page?.keys) ? page.keys : [];const values = await Promise.all( keys.map(async (item) => { const value = await my_kv.get(item.key); return { key: item.key, value, ttl: item.ttl, meta: item.meta, }; }));allItems.push(...values);奖励看到最后的你
最后,送上一段我个人非常喜欢使用的工具函数,可以大幅简化拼装 Response 和处理跨域的代码,希望你也能喜欢。
export const CORS_HEADERS = { 'Access-Control-Allow-Origin': 'https://你的前端域名.com', // 建议别写 * 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization',};
export function createResponse(body: any, status: number = 200, additionalHeaders = CORS_HEADERS) { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json; charset=utf-8', ...additionalHeaders, }, });}
// createResponse({"msg": "I love U"}, 200, CORS_HEADERS)