Payload + Remix 实战:通过 Local API 在 Remix 应用中直接调用 Payload 的 Monorepo 示例
Payload 仓库在 examples/remix 下提供了一个完整的双应用 Monorepo 示例,其目标是演示如何在 Remix 框架中使用 Payload 的 Local API 直接操作数据,而无需经过 REST 或 GraphQL 接口。阅读本文后,你将掌握:如何用 pnpm Workspace 把 Next.js 管理的 Payload 应用与 Remix 网站组合到一个仓库中、Remix 的 loader/action 如何通过 getPayload({ config }) 完成查询/创建/删除,以及构建配置中处理 sharp 等原生依赖的关键细节。
示例总体架构:两个应用共享一份 Payload 配置
examples/remix/README.md 明确了该示例的定位:
The objective is to show to use the Local API with Remix framework. This is achieved through monorepo with 2 apps.
整体结构如下:
| 子应用 | 角色 | 技术栈 |
|---|---|---|
| payload/ | Next.js 管理的 Payload 应用:Admin 面板 + 所有 Collection 与 Payload 配置的定义方 | Next.js 15 + Payload 3 + MongoDB |
| website/ | Remix 站点:通过 pnpm workspace 依赖直接 import payload 的 config,在 loader/action 中调用 Local API | Remix 2.15 + Vite 5 + Tailwind 4 |
Workspace 由根目录的 pnpm-workspace.yaml 声明,包含 payload 与 website 两个包;根目录 package.json 则提供两个便捷脚本,分别进入对应子目录启动:
"dev:payload": "cd payload && pnpm dev",
"dev:website": "cd website && pnpm dev"
其中 payload 子应用的 pnpm dev 即 next dev(见 payload/package.json 的 scripts),website 子应用的 pnpm dev 即 remix vite:dev(见 website/package.json)。
Payload 侧:配置、Collection 与类型导出
数据模型
Payload 应用在 payload.config.ts 中定义了三个 Collection:
users:开启auth: true,作为 Admin 登录用户(见 Users.ts);media:upload: true的媒体上传集合,且read访问控制放开为() => true,允许公开读取(见 Media.ts);posts:核心演示集合,只有一个必填的title文本字段——Remix 网站的所有读写操作都围绕它展开。
配置同时指定了 mongooseAdapter(数据库连接串来自 DATABASE_URL)、lexicalEditor() 富文本编辑器,并在 typescript.outputFile 中指向 payload-types.ts 以生成类型。值得注意的还有 onInit 钩子:启动时用 payload.count 检查 posts 数量,若为 0 则 payload.create 一条标题为 Post 1 的文档,保证示例开箱即可看到数据——这本身也是 Local API 在服务端(onInit 钩子内)的典型用法。
关键设计:把 config 作为包导出给 website
payload/package.json 的 exports 字段把包入口直接指向 TypeScript 源码:
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts",
"default": "./src/index.ts"
}
}
而 src/index.ts 内容只有两行:
export { default as config } from './payload.config'
export * from './payload-types'
这意味着 website 侧只需声明 workspace 依赖,就能同时拿到类型化的 config 对象和自动生成的 Post 等类型:
// website/package.json
"payload": "latest",
"payload-app": "workspace:*"
从源码结构看,这是能成立的:Remix 由 Vite 驱动,vite dev/build 链路使用 esbuild 转译,可以直接消费 .ts 源码,因此无需先把 payload 应用单独编译再发布。
Website 侧:loader / action 中调用 Local API
整个示例最核心的代码是首页路由 website/app/routes/_index.tsx。它展示了 Local API 在 Remix 数据流(Data Loading / Mutation)三个环节中的用法。
loader:服务端查询列表
import { getPayload, PaginatedDocs } from 'payload'
import { config, Post } from 'payload-app'
export const loader = async () => {
const payload = await getPayload({ config })
const posts = await payload.find({ collection: 'posts', sort: 'createdAt' })
return Response.json(posts)
}
要点:
getPayload({ config })每次请求内即时初始化一个 Payload 实例,传入的正是 payload 应用导出的那份 config,因此查询走的是同一套数据库连接与 Collection 定义,且天然绕过了 Admin 的访问控制鉴权流程(Local API 运行于可信服务端);- 查询结果以
Response.json(posts)返回,在组件中通过useLoaderData<PaginatedDocs<Post>>()消费——泛型Post来自 payload 应用导出的payload-types.ts,实现端到端类型安全; - 该 loader 运行在服务端(Remix 的 server-only 执行环境),不会把数据库驱动暴露给浏览器。
action:创建与删除
export async function action({ request }: ActionFunctionArgs) {
const body = await request.formData()
const payload = await getPayload({ config })
const postId = body.get('postId') as string
if (postId) {
await payload.delete({ collection: 'posts', id: postId })
return Response.json({ message: 'Post deleted' })
}
const post = await payload.create({
collection: 'posts',
data: { title: (body.get('title') as string) || 'Untitled' },
})
return Response.json(post)
}
action 遵循 Remix 的“一个表单提交入口,按字段分流”的约定:若表单携带 postId(每个列表项的删除按钮通过隐藏 input 提交该字段)则执行 payload.delete;否则把 title 作为新文档执行 payload.create。组件端只使用 Remix 的 <Form method="post"> 原生表单提交,无 JavaScript 状态管理参与,页面重新走 loader 刷新数据。
其余入口文件
entry.server.tsx 与 entry.client.tsx 属于 Remix 标准 Vite 模板(通过 isbot 区分爬虫与浏览器请求,分别采用 onAllReady 或 onShellReady 流式渲染),与 Payload 集成无直接关系,了解即可。
构建要点:让 sharp 与 Payload 在 Remix SSR 中正常工作
website/vite.config.ts 中有两处对 Payload 集成至关重要:
ssr: {
external: ['sharp'],
// Reduces Docker image size
noExternal: process.env.NODE_ENV === 'production' ? [/.*/] : [],
},
optimizeDeps: {
exclude: ['sharp', 'file-type'],
},
ssr.external: ['sharp']与optimizeDeps.exclude: ['sharp', 'file-type']:sharp是 Payload 图片处理依赖(原生模块),必须保持为 Node 外部依赖而不能被 Vite 预打包/内联,否则原生绑定会失效;- 生产环境下
noExternal: [/.*/]把全部依赖 bundle 进 server 产物(注释说明这是参照 Remix 社区讨论减小 Docker 镜像体积的做法),payload 应用目录下的 Dockerfile 与 docker-compose.yml 则用于容器化部署这一侧。
另外,website 的 Vite 配置开启了 Remix 的 v3_singleFetch 等 future flags,vite-tsconfig-paths() 插件负责解析路径别名(tsconfig.json 中 ~/* 指向 ./app/*)。运行环境要求上,website 声明了 node >= 20.0.0,payload 应用声明了 node ^18.20.2 || >=20.9.0(见各自的 package.json engines 字段)。
完整搭建步骤
按照 examples/remix/README.md 的 Setup 章节,完整流程为 5 步(在 examples/remix 目录下执行):
# 1. 为 payload 应用复制环境变量文件
cp ./payload/.env.example ./payload/.env
# 2. 为 website 应用复制环境变量文件
cp ./website/.env.example ./website/.env
# 3. 安装全部 workspace 依赖
pnpm install
# 4. 启动 Next.js 管理的 Payload 应用(Admin 面板 + API)
pnpm run dev:payload
# 5. 另开一个终端启动 Remix 网站
pnpm run dev:website
其中 .env 需要提供的关键变量,从 payload.config.ts 的读取逻辑可以确认:
| 变量 | 用途 | 读取位置 |
|---|---|---|
PAYLOAD_SECRET |
Payload 签名密钥(JWT/Token 签名) | secret: process.env.PAYLOAD_SECRET || '' |
DATABASE_URL |
MongoDB 连接串 | mongooseAdapter({ url: process.env.DATABASE_URL || '' }) |
README 也提示了前提:本示例基于早期版本的 website 模板构建("built based on an old version of the website template"),适合作为 Local API + Remix 的最小参考实现,实际项目建议对照当前 Remix 版本迁移。
小结与延伸方向
这个示例给出的是一套清晰的分层模式:Payload 应用负责数据模型、Admin 后台与 REST/GraphQL 端点,而任意 Node 侧前端框架(此处为 Remix)通过 workspace 依赖共享 config 并用 getPayload({ config }) 在 loader/action 中直接调用 Local API。对仓库内其他非 Next.js 部署形态的读者,类似的“外部框架调用 Local API”思路同样适用于 docs/local-api/outside-nextjs.mdx 与 docs/local-api/overview.mdx 中描述的场景;若需要更完整的非 Next.js 后端参考,可继续浏览 examples/custom-server 等示例。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00