CMS:互联网的无名英雄
在无数网站的管理员登录界面背后,隐藏着互联网的无名英雄之一:内容管理系统(CMS)。这款看似基础的软件,承担着草拟和发布博客文章、管理媒体资源、处理用户配置文件等任务,应用场景极其广泛。在这一类别中,有一个名为 Payload 的开源项目尤为突出,它在 GitHub 上获得了超过 35,000 颗星,并引发了巨大的社区热情,以至于最近被 Figma 收购。
今天,我们很高兴向大家展示来自 Payload 团队的一个新模板。通过这个模板,只需点击一下 "Deploy to Cloudflare" 按钮,就能将一个功能齐全的 CMS 部署到 Cloudflare 平台。它会生成一个完全配置好的 Payload 实例,并自动绑定到 Cloudflare D1 和 R2。下文我们将深入探讨实现这一目标背后的技术工作、它所解锁的机遇,以及我们如何利用 Payload 为 Cloudflare TV 提供支持。但首先,让我们看看为什么在 Workers 上托管 CMS 会带来游戏规则的改变。
幕后故事:Cloudflare TV 的 Payload 实例
无服务器设计
大多数 CMS 都设计为托管在 24/7 运行的传统服务器上。这意味着你需要配置硬件或虚拟机、安装 CMS 软件和依赖项、管理端口和防火墙,并应对持续的维护和扩展难题。
这带来了巨大的运维负担,如果服务器需要处理高流量(或突发峰值),成本可能相当高昂。更糟糕的是,无论是否有活跃用户,你都需要为服务器付费。
Cloudflare Workers 的超能力之一在于,你的应用程序和数据可以 24/7 访问,而无需服务器一直运行。当用户使用你的应用程序时,它会在离用户最近的 Cloudflare 服务器上瞬间启动。当用户休息时,Worker 会自动暂停,你无需为未使用的计算资源付费。
当 Payload 运行在 Workers 上时,你既能获得传统 CMS 的所有优势——完全可配置的资产管理、自定义 Webhook、丰富的社区插件库、版本历史——又能享受无服务器架构的便利。我们已经在 Cloudflare TV(我们用于测试新技术的 24/7 视频平台)上试点了基于 Workers 的 Payload 模板。得益于对条件逻辑等功能的支持以及用于构建管理后台的丰富组件集,从传统 CMS 迁移的过程非常轻松。我们的内容库包含超过 2,000 个剧集和 70,000 个资源,而 Payload 强大的筛选和搜索功能让我们能够轻松管理它们。
值得重申的是,CMS 的用例非常广泛,从发布、电子商务,到由 Claude Code 或 Codex 快速构建的定制应用程序仪表板。CMS 提供了一种直观的界面,即使是非技术人员也能轻松上手,并且可以根据项目需求塑造成任何形状。我们非常期待看到大家能用它构建出什么。
OpenNext 开启大门
Payload 最初于 2022 年作为 Node/Express.js 应用程序推出,并迅速积累了人气。2024 年,它引入了对流行的 Next.js 框架的原生支持,这为今天的发布铺平了道路:今年,随着 OpenNext 适配器的 GA 版本发布,Cloudflare 成为了托管 Next.js 应用程序的最佳平台。
得益于这个适配器,使用官方的 OpenNext 入门指南将 Payload 移植到 OpenNext 相对简单。因为我们希望应用程序能在 Workers 上无缝运行,并充分利用 Workers Bindings 的优势,所以我们着手确保对 Cloudflare 数据库和存储产品的支持。
技术深度解析
数据库:从 Postgres 到 D1
1. 使用 Postgres 和 Hyperdrive
在最初的尝试中,我们通过官方的
@payloadcms/db-postgres 适配器将 Payload 连接到外部 Postgres 数据库。由于 Workers 支持 node-postgres 包,一切几乎都能立即工作。由于连接无法在请求之间共享,我们只需禁用连接池:JAVASCRIPTimport { buildConfig } from 'payload' import { postgresAdapter } from '@payloadcms/db-postgres' export default buildConfig({ // ... db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URI, maxUses: 1, }, }), // ... });
当然,禁用连接池会增加整体延迟,因为每个请求都需要先与数据库建立新连接。为了解决这个问题,我们在它前面部署了 Hyperdrive。Hyperdrive 不仅通过建立到数据库服务器的隧道,在整个 Cloudflare 网络中维护一个连接池,还增加了查询缓存,显著提升了性能。
JAVASCRIPTimport { buildConfig } from 'payload' import { postgresAdapter } from '@payloadcms/db-postgres' import { getCloudflareContext } from '@opennextjs/cloudflare'; const cloudflare = await getCloudflareContext({ async: true }); export default buildConfig({ // ... db: postgresAdapter({ pool: { connectionString: cloudflare.env.HYPERDRIVE.connectionString, maxUses: 1, }, }), // ... });
2. 使用 D1 构建自定义适配器
在 Postgres 运行成功后,我们接下来尝试添加对 D1 的支持。D1 是 Cloudflare 基于 SQLite 构建的托管无服务器数据库。
Payload 并不原生支持 D1,但它通过
@payloadcms/db-sqlite 适配器支持 SQLite,该适配器使用 Drizzle ORM 和 libSQL。幸运的是,Drizzle 也支持 D1,因此我们决定以 SQLite 适配器为基础,为 D1 构建一个自定义适配器。D1 和 libSQL 的主要区别在于结果对象,因此我们构建了一个小型方法,将 D1 的结果映射到 libSQL 期望的格式:
JAVASCRIPTexport const execute: Execute<any> = function execute({ db, drizzle, raw, sql: statement }) { const executeFrom = (db ?? drizzle)! const mapToLibSql = (query: SQLiteRaw<D1Result<unknown>>) => { const execute = query.execute query.execute = async () => { const result: D1Result = await execute() const resultLibSQL: Omit<ResultSet, 'toJSON'> = { columns: undefined, columnTypes: undefined, lastInsertRowid: BigInt(result.meta.last_row_id), rows: result.results as any[], rowsAffected: result.meta.rows_written, } return Object.assign(result, resultLibSQL) } return query } if (raw) { const result = mapToLibSql(executeFrom.run(sql.raw(raw))) return result } else { const result = mapToLibSql(executeFrom.run(statement!)) return result } }
除此之外,只需将 D1 绑定直接传递给 Drizzle 的构造函数即可使其工作。
对于部署期间的数据库迁移,我们使用了新发布的 Wrangler 远程绑定功能来连接到远程数据库,使用相同的绑定。这样我们就不需要配置任何 API 令牌来与数据库交互。
媒体存储:使用 R2
Payload 通过
@payloadcms/storage-s3 包提供了官方的 S3 存储适配器。R2 与 S3 兼容,这意味着我们可以使用官方适配器,但与数据库类似,我们希望使用 R2 绑定,而不是创建 API 令牌。因此,我们也决定为 R2 构建一个自定义存储适配器。这个适配器非常简单,因为绑定已经处理了大部分工作:
JAVASCRIPTimport type { Adapter } from '@payloadcms/plugin-cloud-storage/types' import path from 'path' const isMiniflare = process.env.NODE_ENV === 'development'; export const r2Storage: (bucket: R2Bucket) => Adapter = (bucket) => ({ prefix = '' }) => { const key = (filename: string) => path.posix.join(prefix, filename) return { name: 'r2', handleDelete: ({ filename }) => bucket.delete(key(filename)), handleUpload: async ({ file }) => { const buffer = isMiniflare ? new Blob([file.buffer]) : file.buffer await bucket.put(key(file.filename), buffer) }, staticHandler: async (req, { params }) => { const obj = await bucket?.get(key(params.filename), { range: isMiniflare ? undefined : req.headers }) if (obj?.body == undefined) return new Response(null, { status: 404 }) const headers = new Headers() if (!isMiniflare) obj.writeHttpMetadata(headers) return obj.etag === (req.headers.get('etag') || req.headers.get('if-none-match')) ? new Response(null, { headers, status: 304 }) : new Response(obj.body, { headers, status: 200 }) }, } }
部署与性能优化
部署流程
将数据库和存储适配器准备就绪后,我们就成功地在 Cloudflare 的开发者平台上启动了一个完全运行的 Payload 实例。
这个空白模板包含一个简单的数据库,只有两个表:一个用于媒体,另一个用于用户。在这个模板中,可以注册、创建新用户和上传媒体文件。然后,通过修改 Payload 的配置,可以轻松扩展,添加更多的集合、关系和自定义字段。
性能优化:利用读取副本
默认情况下,D1 位于单一位置(可通过位置提示自定义)。由于 Payload 作为 Worker 部署,来自世界各地的请求可能会导致连接数据库的延迟波动很大。
为了解决这个问题,我们可以利用 D1 的全球读取副本功能,该功能在全球范围内部署多个只读副本。为了选择正确的副本并确保顺序一致性,D1 使用会话(sessions),并需要传递一个书签(bookmark)。
目前 Drizzle 尚不支持 D1 sessions,但我们仍然可以使用 "first-primary" 类型的会话,在这种模式下,第一个查询总是命中主实例,后续查询可能会命中其中一个副本。更新适配器以使用副本,只需在初始化 Drizzle 时直接传递 D1 会话即可:
JAVASCRIPTthis.drizzle = drizzle(this.binding.withSession("first-primary"), { logger, schema: this.schema });
经过这个简单的更改,我们立即看到了延迟的改善。当数据库位于美国东部时,来自全球各地的请求的 P50 响应时间减少了 60%。
| 指标 | 无读取副本 | 启用读取副本 | 提升幅度 |
|---|---|---|---|
| P50 | 300ms | 120ms | -60% |
| P90 | 480ms | 250ms | -48% |
| P99 | 760ms | 550ms | -28% |
(数据来源:Cloudflare Dashboard 报告的 Payload Worker 请求的墙钟时间,每个请求涉及两次数据库调用。负载由 4 个全球分布的 uptime checks 生成,每 60 秒向 4 个不同的 URL 发出一次请求。)
由于我们将依靠 Payload 来管理 Cloudflare TV 庞大的内容库,我们处于一个独特的位置,可以在大规模下对其进行测试,并将持续提交 PR 以进行优化和改进。
更多选择:其他 Workers 兼容的 CMS
CMS 的潜在用例是无限的,因此拥有多种选择是件好事。我们选择 Payload 是因为它拥有丰富的组件库、成熟的功能集和庞大的社区——但它不是 Workers 上唯一可用的 CMS。
另一个令人兴奋的项目是 SonicJs,它从头开始构建在 Workers、D1 和 Astro 之上,承诺提供闪电般的速度和灵活的基础。SonicJs 正在开发一个非常适合与 Claude 和 Codex 等智能 AI 助手协作的版本,我们期待看到它的发展。
对于轻量级用例,microfeed 是一个托管在 Cloudflare 上的自托管 CMS,专门用于管理播客、博客、照片等。
这些都是无头 CMS(Headless CMS),意味着你可以为应用程序自由选择前端。不要错过我们最近宣布赞助强大的框架 Astro 和 Tanstack,并在 Workers 文档中找到我们关于使用这些框架以及其他框架(包括 React + Vite)的完整指南。
开始使用
立即开始使用 Payload,点击下面的 "Deploy to Cloudflare" 按钮,它将生成一个功能齐全的 Payload 实例,包括自动绑定到你的 Worker 的 D1 数据库和 R2 存储桶。在 Payload 的模板仓库中可以找到 README 和更多详细信息。