使用 @payloadcms/payload-cloud 插件:为 Payload Cloud 接入 S3 文件存储、Resend 邮件与上传缓存
【免费下载链接】payloadPayload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.项目地址: https://gitcode.com/GitHub_Trending/pa/payload
本指南以开源仓库中packages/payload-cloud/README.md为核心,系统讲解 Payload 官方云插件@payloadcms/payload-cloud的能力与接入方式。该插件将你的 Payload 实例与 Payload Cloud 托管的资源打通:媒体文件写入由 Cloudflare CDN 加速的 S3 存储、邮件通过 Resend SMTP 交付、上传响应自动带缓存头并支持变更时主动清理缓存。读完你将掌握插件安装配置、可选参数、本地联调所需的环境变量,以及其底层通过 collection hooks、upload handlers 与任务调度(jobs)协作的实现机制。
一、插件是什么
@payloadcms/payload-cloud是 Payload 的官方云插件(源码位于 packages/payload-cloud,包名@payloadcms/payload-cloud),它的定位是把 "本机/自建 Payload" 升级为 "跑在 Payload Cloud 上" 时的资源连接层。README 明确了它提供的三项核心能力:
- 文件存储(File storage):Payload Cloud 提供由 Cloudflare 作为 CDN 的 S3 文件存储,插件扩展 Payload 的 upload 集合,使所有媒体文件保存在 S3 中而非本地磁盘。
- 邮件投递(Email delivery):开箱即用的邮件投递服务,由 Resend 驱动。
- 上传缓存(Upload caching):默认对所有 upload 集合提供缓存,同样经由 Cloudflare CDN 加速并处理缓存失效。
除这三项外,从源码src/plugin.ts可以看到插件还会向配置注入一个隐藏的 global(payload-cloud-instance)并接管config.jobs.autoRun,用于保证定时任务只在多实例部署中的单个实例上运行。README 中 "Future enhancements" 提到后续还会增加API CDN——动态缓存 API 请求、并在资源更新时自动 purge。
需要注意的前提:这是一个云托管配套插件,只有在 Payload Cloud 注入的环境变量齐全时才会真正生效(详见下文"执行开关"),本地普通 Payload 项目即使引入该插件也不会被改变行为。
二、快速接入:安装与最小配置
2.1 安装
在 Payload 项目中安装(README 以 yarn 为例,仓库本身使用 pnpm workspace 管理,pnpm add同理):
yarn add @payloadcms/payload-cloud该包以payload为 peerDependency,且依赖@aws-sdk/*、amazon-cognito-identity-js、nodemailer与@payloadcms/email-nodemailer(见package.json),安装时会一并引入。
2.2 在 Payload config 中启用
import { payloadCloudPlugin } from '@payloadcms/payload-cloud' import { buildConfig } from 'payload' export default buildConfig({ plugins: [payloadCloudPlugin()], // rest of config })入口src/index.ts暴露了三个导出:payloadCloudPlugin(插件主入口)、createKey(构造 S3 对象键)、getStorageClient(获取已认证的 S3 客户端),后两者一般只被插件内部调用,也可供二次开发复用。
2.3 执行开关:什么时候插件真正生效
README 明确指出:"This plugin will only execute if the required environment variables set by Payload Cloud are in place. If they are not, the plugin will not execute and your Payload instance will behave as normal."
源码src/plugin.ts第一道关卡就是:
if (process.env.PAYLOAD_CLOUD !== 'true') { return config // 原样返回,什么都不改 }也就是说,只有在PAYLOAD_CLOUD=true的环境中,文件存储、邮件、上传缓存、jobs 接管才会被注入;在本地普通开发环境(未设该变量)中,插件是一个"空转"的 no-op,[plugin.spec.ts](https://link.gitcode.com/i/1392a3acde632d3ab978dca8a07c9a04)中的 "should return unmodified config"测试用例正是对这一行为的验证。这一点对排查"为什么插件好像没生效"非常有帮助。
2.4 关于自定义邮件 transport 的优先级
README 有一则重要 NOTE:如果 Payload config 里已经配置了带 transport 的 email,它优先于 Payload Cloud 的邮件服务。源码src/email.ts中对应逻辑是:当检测到args.config.email已存在时打印一条提示日志并直接返回已有 email 配置,而不会用 Resend 覆盖它;同时测试用例 "should not modify existing email transport" 也锁定了这一行为。如果你确认要使用 Payload Cloud 邮件,应在插件选项中显式传email: false并自行清理 config 中的 email 设置。
三、文件存储:从本地磁盘到 S3
启用存储后,插件遍历所有带upload配置的 collection,做三件事(源码见src/plugin.ts的 storage 分支):
- 关闭本地落盘:
upload.disableLocalStorage: true; - 追加 S3 上传/删除 hook:
beforeChange上传、afterDelete删除; - 追加静态文件 handler:把文件 URL 的请求代理到 S3 读取(原 collection 自定义 handler 会被保留在前面);
- 全局开启临时文件:
config.upload.useTempFiles: true,配合大文件场景使用。
3.1 上传:beforeChange hook
beforeChange(src/hooks/beforeChange.ts)在写入数据库前把文件并发推送到 S3:
- 通过
getIncomingFiles收集主文件及所有 Payload 生成的尺寸变体(data.sizes+req.payloadUploadSizes),因此原图与缩略图会全部上传; - 文件对象键由
createKey生成,形如:
${identityID}/${PAYLOAD_CLOUD_ENVIRONMENT}/${collectionSlug}/${filename}即身份ID / 环境 / 集合名 / 文件名的结构,天然做到不同项目、不同环境之间的对象隔离;
- 使用
@aws-sdk/lib-storage的Upload做分片并行上传(源码注释说明默认 queueSize=4、partSize=5MB,即最多缓冲约 20MB),并注册httpUploadProgress事件输出 debug 日志,便于观察大文件进度。
3.2 删除:afterDelete hook
afterDelete(src/hooks/afterDelete.ts)在文档删除后遍历doc.filename以及doc.sizes中所有变体的文件名,逐一deleteObject。注意这里的sizes数据来自文档快照(hook 参数doc),与上传侧的变体文件一一对应。
3.3 读回:static handler 与缓存头
上传集合的访问 URL 请求最终落到src/staticHandler.ts的 handler:
- 用同一个
createKey拼出键,getObject从 S3 取回对象体与元数据; - 响应头携带
Content-Type、Content-Length、ETag,并在缓存启用时附加Cache-Control: public, max-age=<maxAge>(maxAge 默认 86400 秒,见下节); - 对
image/svg+xml额外注入Content-Security-Policy: script-src 'none',防止 SVG 内嵌可执行脚本,这是值得注意的安全细节; - 错误处理覆盖
NoSuchKey与AccessDenied(源码注释说明:AWS SDK 找不到键时会尝试底层s3:ListBucket,而桶策略禁止该操作,因此 AccessDenied 往往意味着"对象不存在"),二者均返回 404;其余错误返回 500。开启debug选项时日志会携带完整错误对象,便于排障。
3.4 本地文件存储的认证与访问
插件通过 AWS Cognito 换取临时凭证访问 S3。getStorageClient(src/utilities/getStorageClient.ts)会缓存 S3 client 与 Cognito session,仅当 session 失效(!session.isValid())时才调用refreshSession重新认证:先用用户名/密码在 Cognito User Pool 登录拿到 ID Token,再以该 token 作为身份池logins换取临时凭证,最后以PAYLOAD_CLOUD_BUCKET_REGION构造 S3 client。因此在本地想直接读写云端文件资源时,下面这组环境变量必须齐全(README 原样给出,也是代码中实际读取的变量名):
PAYLOAD_CLOUD=true PAYLOAD_CLOUD_ENVIRONMENT=prod PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID= PAYLOAD_CLOUD_COGNITO_USER_POOL_ID= PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID= PAYLOAD_CLOUD_PROJECT_ID= PAYLOAD_CLOUD_BUCKET= PAYLOAD_CLOUD_BUCKET_REGION= PAYLOAD_CLOUD_COGNITO_PASSWORD=其中PAYLOAD_CLOUD_PROJECT_ID、PAYLOAD_CLOUD_COGNITO_PASSWORD、PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID是强校验项——缺失时getStorageClient会直接throw(见src/utilities/getStorageClient.ts底部),从而中止上传/删除/读取操作。
补充说明:这些值由 Payload Cloud 平台分配,本地开发时属于"联调配置",不在本仓库内生成;上述表格用于说明插件读取哪些变量及其用途。
四、邮件投递:Resend 开箱即用
4.1 工作原理
当满足PAYLOAD_CLOUD=true、且环境变量PAYLOAD_CLOUD_EMAIL_API_KEY与PAYLOAD_CLOUD_DEFAULT_DOMAIN均存在时,插件会调用payloadCloudEmail构建@payloadcms/email-nodemailer适配器,transport 指向 Resend:
nodemailer.createTransport({ auth: { pass: apiKey, user: 'resend' }, host: 'smtp.resend.com', port: 465, secure: true, })默认发件人(可被插件选项覆盖,见第六节)为:
defaultFromName缺省值:'Payload CMS';defaultFromAddress缺省值:存在自定义域时取cms@<第一个自定义域>,否则取cms@<defaultDomain>。
apiKey或defaultDomain缺失时函数会直接抛错;而 email 分支在 plugin 层额外加了条件判断,只有两者齐全才会真正注入 Resend 适配器,这与[plugin.spec.ts](https://link.gitcode.com/i/1392a3acde632d3ab978dca8a07c9a04) 的 "should allow PAYLOAD_CLOUD_EMAIL_* env vars to be unset"测试一致。
4.2 From Domain:必须是你有权限的域名
README 强调:邮件from地址必须来自你有权限的域名。Payload Cloud 会自动将你部署用的域名(对应process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN)加入白名单;如果你配置了自定义域名,这些域名同样会被加入白名单。尝试从一个你无权使用的域名发送邮件将不会成功。
自定义域名如何被识别?源码src/email.ts会扫描所有以PAYLOAD_CLOUD_EMAIL_DOMAIN_开头、且不以API_KEY结尾的环境变量,将其值收集为自定义域名列表(并打印日志确认),例如:
PAYLOAD_CLOUD_EMAIL_DOMAIN_1=news.example.com PAYLOAD_CLOUD_EMAIL_DOMAIN_2=marketing.example.com五、上传缓存:默认 24 小时 + 变更自动失效
Payload Cloud 通过 Cloudflare CDN 为 upload 集合提供缓存,staticHandler中输出的Cache-Control: public, max-age=86400就是默认 24 小时缓存的表现形式。
5.1 默认行为与失效机制
README 说明了两点默认行为:
- 默认对所有 upload 集合缓存 24 小时(
maxAge = 86400秒); - 当某条 upload 记录被更新或删除时,缓存会自动失效。
从实现看,"失效"分为两层:
staticHandler靠maxAge让 CDN/浏览器在指定时间内直接命中缓存;- 变更时由
src/hooks/uploadCache.ts中注入的afterChange/afterDeletehook 触发一次cache purge:向插件配置的 API endpoint(默认https://cloud-api.payloadcms.com)POST/api/purge-cache,body 携带{ cacheKey: PAYLOAD_CLOUD_CACHE_KEY, filepath: doc.url, projectID: PAYLOAD_CLOUD_PROJECT_ID },让 Cloudflare 精确清理该文件对应的缓存条目。
值得注意的实现细节:
- purge 仅在
payloadAPI !== 'local'时执行(本地 API 调用不会触发网络 purge),且update/delete操作为 fire-and-forget(void purge(...)),不阻塞主流程; - purge 需要额外的环境变量
PAYLOAD_CLOUD_CACHE_KEY——plugin.ts中cachingEnabled的判定正是uploadCaching !== false && !!process.env.PAYLOAD_CLOUD_CACHE_KEY,因此没有PAYLOAD_CLOUD_CACHE_KEY时,缓存相关 hook 与静态 handler 上的 Cache-Control 头都不会启用; doc.url为空时会记录一条 error 日志并提前返回,避免无效 purge。
六、可选项:按需关闭或精细化缓存
如果你不需要某项云特性,插件支持整体或局部关闭(README 中的两种配置形式如下,默认全部开启)。
6.1 整体关闭某一能力
payloadCloudPlugin({ storage: false, // Disable file storage email: false, // Disable email delivery uploadCaching: false, // Disable upload caching })types.ts中PluginOptions的类型定义进一步明确了可配置项与默认值:
| 选项 | 类型 | 默认 | 说明 |
|---|---|---|---|
storage | false \| undefined | 开启(传false关闭) | 关闭后插件不再修改任何 upload collection |
email | { defaultFromAddress, defaultFromName, skipVerify? } \| false | 开启 | 关闭或自定义默认发件人;skipVerify透传给 nodemailer 适配器 |
uploadCaching | { maxAge?, collections? } \| false | 开启(86400s) | 关闭或精细化配置(见 6.2) |
enableAutoRun | boolean | true | 是否接管config.jobs.autoRun(见第七节) |
debug | boolean | false | 是否输出额外调试日志,并将完整 AWS 错误写入日志 |
endpoint | string | https://cloud-api.payloadcms.com | 标记为内部开发用途的 API endpoint 覆盖项 |
6.2 上传缓存的精细化配置
README 提供了按集合覆盖缓存的完整示例:顶层maxAge是全体默认值,集合名 keyed 对象中既可以单独设置maxAge(单位秒,优先级最高),也可以用enabled: false对该集合关闭缓存:
payloadCloudPlugin({ uploadCaching: { maxAge: 604800, // Override default maxAge for all collections collection1Slug: { maxAge: 10, // Collection-specific maxAge, takes precedence over others }, collection2Slug: { enabled: false, // Disable caching for this collection }, }, })对照staticHandler的实现逻辑可以精确看到优先级链:
maxAge初始化为 86400;- 若顶层配置了
maxAge,则覆盖全体默认; - 若该集合在
collections中有配置,则collCacheConfig.maxAge进一步覆盖(对应注释 "Collection-specific maxAge, takes precedence over others"); - 只有
collections[slug].enabled !== false且存在PAYLOAD_CLOUD_CACHE_KEY时才会输出Cache-Control头。
七、附带能力:Jobs 定时任务只在单实例执行
README 未展开、但源码完整实现的一个附带能力是Jobs 单实例运行保障(见src/plugin.ts)。云环境通常多副本部署,若每个副本都执行 cron 会导致任务重复。插件通过"隐藏 global + 实例标识"机制协调:
- 向 config 注入 slug 为
payload-cloud-instance的 hidden global(admin.hidden: true,字段仅一个必填instance文本); - 改写
config.jobs.autoRun:第一个触发者会生成 24 位随机字符串(generateRandomString,字母数字全集)写入该 global,并设置PAYLOAD_CLOUD_JOBS_INSTANCE环境变量;后续shouldAutoRun会findGlobal校验自己是否仍是当前持有者,不是则清空变量并拒绝运行; - 未配置
jobs.autoRun时返回默认 cron job:{ cron: '* * * * *', limit: 10, queue: 'default' };已有 autoRun 则包装原逻辑(函数则 await 后返回其结果)。若已有shouldAutoRun,插件不会覆盖它。
[plugin.spec.ts](https://link.gitcode.com/i/1392a3acde632d3ab978dca8a07c9a04)中的 "should always set global instance identifier"测试验证了 global 的注入与字段结构。若你不想让插件触碰 jobs 配置,可设置enableAutoRun: false。
八、测试验证与源码导读
本仓库对插件行为有较完整的 vitest 测试,集中在packages/payload-cloud/src/plugin.spec.ts与packages/payload-cloud/src/email.spec.ts,它们把上文各结论固化成了可回归验证的用例:
- 未处于 Payload Cloud 环境(未设
PAYLOAD_CLOUD=true)时,返回未经修改的 config; - 处于云端环境时,默认启用云存储(验证
config.upload.useTempFiles === true); storage: false/email: false可正常关闭对应能力;- 默认邮件 transport 指向
smtp.resend.com;email依赖的两个环境变量可同时缺席(此时不注入邮件); - 已存在 email transport 时不会覆盖(打印提示日志);
- 自定义
defaultFromName/defaultFromAddress生效。
对想深入源码的读者,建议按以下顺序阅读:
- 配置注入:
packages/payload-cloud/src/plugin.ts(执行开关、storage/email/jobs 三块注入逻辑) - 类型契约:
packages/payload-cloud/src/types.ts(全部 PluginOptions 与默认值注释) - 存储三件套:
src/hooks/beforeChange.ts、src/hooks/afterDelete.ts、src/staticHandler.ts - 缓存失效:
src/hooks/uploadCache.ts - 云认证:
src/utilities/getStorageClient.ts、src/utilities/refreshSession.ts、src/utilities/authAsCognitoUser.ts
九、上线前自查清单
最后,把本指南的关键前提汇总成一份可操作的 checklist:
- 确认运行环境注入了
PAYLOAD_CLOUD=true,否则插件整体不生效(这是 README 明确的执行边界); - 文件存储要求提供第一组 Cogntio/S3 相关环境变量(含必填的
PAYLOAD_CLOUD_PROJECT_ID、PAYLOAD_CLOUD_COGNITO_PASSWORD、PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID); - 若需要 CDN 缓存及变更自动失效,必须额外提供
PAYLOAD_CLOUD_CACHE_KEY; - 邮件功能要求
PAYLOAD_CLOUD_EMAIL_API_KEY与PAYLOAD_CLOUD_DEFAULT_DOMAIN同时存在,且from域名必须在你有权限的域名白名单内;不要忘记自有config.email会优先于 Payload Cloud 邮件服务; - 多副本部署下 Jobs 单实例运行默认开启,如不希望插件接管 autoRun 请设置
enableAutoRun: false; - 在普通本地项目(非 Payload Cloud 托管)中引入该插件是安全的——它只会静默返回原 config,不会产生副作用。
<输出文章>
【免费下载链接】payloadPayload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.项目地址: https://gitcode.com/GitHub_Trending/pa/payload
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考