首页
/ Payload + Remix 实战:通过 Local API 在 Remix 应用中直接调用 Payload 的 Monorepo 示例

Payload + Remix 实战:通过 Local API 在 Remix 应用中直接调用 Payload 的 Monorepo 示例

2026-09-05 23:16:01作者:虞亚竹Luna

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 声明,包含 payloadwebsite 两个包;根目录 package.json 则提供两个便捷脚本,分别进入对应子目录启动:

"dev:payload": "cd payload && pnpm dev",
"dev:website": "cd website && pnpm dev"

其中 payload 子应用的 pnpm devnext dev(见 payload/package.json 的 scripts),website 子应用的 pnpm devremix vite:dev(见 website/package.json)。

Payload 侧:配置、Collection 与类型导出

数据模型

Payload 应用在 payload.config.ts 中定义了三个 Collection:

  • users:开启 auth: true,作为 Admin 登录用户(见 Users.ts);
  • mediaupload: 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.jsonexports 字段把包入口直接指向 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)
}

要点:

  1. getPayload({ config }) 每次请求内即时初始化一个 Payload 实例,传入的正是 payload 应用导出的那份 config,因此查询走的是同一套数据库连接与 Collection 定义,且天然绕过了 Admin 的访问控制鉴权流程(Local API 运行于可信服务端);
  2. 查询结果以 Response.json(posts) 返回,在组件中通过 useLoaderData<PaginatedDocs<Post>>() 消费——泛型 Post 来自 payload 应用导出的 payload-types.ts,实现端到端类型安全;
  3. 该 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.tsxentry.client.tsx 属于 Remix 标准 Vite 模板(通过 isbot 区分爬虫与浏览器请求,分别采用 onAllReadyonShellReady 流式渲染),与 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 应用目录下的 Dockerfiledocker-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.mdxdocs/local-api/overview.mdx 中描述的场景;若需要更完整的非 Next.js 后端参考,可继续浏览 examples/custom-server 等示例。

登录后查看全文
热门项目推荐
相关项目推荐