2717 字
14 分钟
EdgeOne Pages Function+KV存储 简易入门
2026-04-14
2026-05-20
统计加载中…… 阅读:    访客: 统计加载失败

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:

Terminal window
npm install -g edgeone
NOTE

EdgeOne CLI代替node项目原本的dev。它通过查找项目package.json中的dev获知原项目本身的dev命令,然后它在启动dev时,同时启动自身(functions)的dev环境和原项目的dev环境。如果你配置了edgeone.json 中的 devCommand 参数,它会优先读取。

工作流#

  1. (非必需) 运行edgeone pages init初始化项目和实例。

  2. (必需) 通过edgeone login登录,你Pages在哪就选哪个站。

  3. (强烈建议) 通过edgeone pages link绑定Pages项目,这可以同步环境变量,并且允许使用KV存储。

  4. 最后,运行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):

/edge-functions/hello.ts
export default function onRequest(context) {
return new Response('Hello from Edge Functions!');
}

这将从网站根目录起的/hello创建一个API。

路由规则#

Edge Functions 基于 /edge-functions 目录结构生成访问路由。

文件路径路由
/edge-functions/index.tsexample.com/
/edge-functions/hello-pages.tsexample.com/hello-pages
/edge-functions/helloworld.tsexample.com/helloworld
/edge-functions/api/users/list.tsexample.com/api/users/list
/edge-functions/api/users/geo.tsexample.com/api/users/geo
/edge-functions/api/users/[id].tsexample.com/api/users/1024
/edge-functions/api/visit/index.tsexample.com/api/visit
/edge-functions/api/[[default]].tsexample.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 export
type 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_AKV_B绑定同一个命名空间production,那么这个命名空间production会同时被KV_AKV_B使用。

初始化账户#

初始化账户的腾讯云UI操作请查看#快速开始

使用KV#

定义变量名后直接在代码中使用该变量名即可。官方代码大量使用ts ignore掩耳盗铃。此处是我自行总结的类型定义,直接复制下面这段代码即可获得完美的类型提示,可以供AI理解:

// 匹配所有 HTTP Method export
export 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 和处理跨域的代码,希望你也能喜欢。

/edge-functions/utils.ts
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)
EdgeOne Pages Function+KV存储 简易入门
https://blog.emumu.xyz/posts/2026-04-11-00/
作者
月宮絵夢
发布于
2026-04-14
许可协议
CC BY-NC-SA 4.0