絵夢の小站

0%

EdgeOne Pages Function+KV存储 简易入门


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

截止本文更新,几个严重的bug已经修复了,比如掉dev、返回ok等。我突然发现就在发文当天(2026-04-14 17:19

)官方就更新了文档。

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 edgeone
提示

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

官方文档强调:“切记请勿在 edgeone.json 或 package.json 中配置 edgeone pages dev!”。

不过可以配置别的。比如这个"edev"pnpm edev

// package.json
{
	"private": true,
	"type": "module",
	"scripts": {
		"dev": "vite dev --host 0.0.0.0 --port 20090",
		"edev": "edgeone pages dev --skip-env-sync true --port 20090",
		"build": "vite build",
		"preview": "vite preview --host 0.0.0.0 --port 21090",
		"check": "biome check"
	},
	// ...
}
  • --skip-env-sync true 用于跳过控制台环境变量同步

  • --port 20090 用于重写edgeone自己重写了的端口,它默认使用8088

工作流

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

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

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

  4. 最后,运行edgeone pages dev开始本地调试。

关于无法启动CLI(包括566)

Edge Functions 调试服务有启动次数限制,因此尽量避免频繁退出启动 dev 服务(dev 服务内热更新不会增加启动次数)。如果多次重启 Dev 导致报错 566,除开账号抽风,通常就是触发了限制。建议此时去点杯奶茶,冷静一会再回来。

关于console不输出

EO CLI环境下,console.log不通过前后端任意一边控制台输出。不知道什么毛病,出了问题只能放响应里返回。

【或已修复】关于返回"ok"但不是真实响应体
  1. 运行时未捕获异常误总会导致API返回plain text script error
  2. 如果没有未捕获异常,正常执行成功,这种情况可能出自文件在调试过程中被新创建调试时间过长网络中断过的情况。
  3. 该问题目前再未出现,或已被修复 (*2026-05-29)
【新bug】API返回404

刷新任意一个API文件,触发重新编译即可完美解决。

通常出现于过长时间没修改任意一个云函数文件的情况。

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异步如网络请求不算在内)。

备注

不导出任何有效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)。键是唯一的标识符,值是与键相关联的数据。
  • 变量名:你的代码不直接通过“命名空间名”访问数据,而是通过“变量名”访问。在使用命名空间数据之前,需要先将命名空间与EO Pages项目通过变量名进行绑定。
flowchart LR subgraph 项目A [Pages 项目 A] KV_A["变量名: USER"] end subgraph 项目B [Pages 项目 B] KV_B["变量名: KV_USER"] KV_ORDER["变量名: KV_ORDER"] end subgraph 项目C [Pages 项目 C] KV_EXAMPLE["变量名: KV"] end KV_A -- 绑定 --> NS_user KV_B -- 绑定 --> NS_user KV_EXAMPLE -.-> |可绑定| NS_user KV_EXAMPLE -.-> |或绑定| NS_inner_user KV_ORDER --> NS_order

重点

  • 虽然官方文档没提,但如cf地,每个键值对可以独立设置过期时间。
  • 可以通过前缀列出键。
  • 无同时操作多个键的API,但可以Promise.all

初始化账户的KV存储

初始化账户的腾讯云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参数似乎不怎么好使。且向options传入任何额外的参数不会报错,此处原因未知、未在文档说明、且疑似近期(26.05)新增功能,建议优先使用expirationTtl

关于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": "Love from blog.emumu.xyz"}, 200, CORS_HEADERS)

评论

加载评论中…