首页
/ Payload + Remix 单仓示例:website 子应用的开发、构建与部署指南

Payload + Remix 单仓示例:website 子应用的开发、构建与部署指南

2026-09-05 12:47:35作者:江焘钦

本文围绕 Payload 开源仓库中 Remix 示例的 website 子应用 展开,完整讲解该示例在 pnpm monorepo 下的开发启动、生产构建、DIY 部署与 Tailwind CSS 样式配置流程,并结合仓库源码剖析 Vite 构建配置、SSR 入口与 Dockerfile 中的关键实现细节。读完后你能够独立在本地跑通 Payload 管理后台与 Remix 网站双服务,理解 build/serverbuild/client 产物的部署逻辑,以及 Local API 在 Remix 侧的接入方式。

示例在 monorepo 中的定位

examples/remix 示例的目标是演示如何在 Remix 框架中使用 Payload 的 Local API。它通过 pnpm monorepo 组织了两个应用,workspace 声明见 pnpm-workspace.yaml

  • payload 子包:Next.js 应用,承载 Payload 管理后台与 Payload 配置定义(集合、数据库适配器等);
  • website 子包:Remix 应用,通过 payload-app 这个 workspace 包导入 payload 配置,从而在服务端 loader/action 中直接调用 Local API,而不是走 REST 接口。

根目录 package.json 提供了两个聚合脚本:

"scripts": {
  "dev:payload": "cd payload && pnpm dev",
  "dev:website": "cd website && pnpm dev"
}

website 的 package.json 中可以看到关键的依赖关系:payload: "latest" 提供运行时能力,payload-app: "workspace:*" 把 payload 子包的配置以 workspace 依赖形式引入。payload 子包的 package.json 通过 exports 将入口指向 src/index.ts,该文件只做两件事:导出 config(payload 配置)与导出 payload-types 中的全部类型。因此 website 侧一句 import { config, Post } from 'payload-app' 即可同时拿到配置对象与生成的 TypeScript 类型。

开发环境启动(Development)

website 的 README 给出的开发命令为:

npm run dev

在 website 子包的 package.json 中它对应 remix vite:dev,即以 Vite 插件模式启动 Remix 的开发服务器(HMR + 路由约定)。

由于本示例是两个应用协同工作的,完整的启动流程按 examples/remix 的 README 为:

# 1. 复制两边各自的 .env.example 为 .env
cp ./payload/.env.example ./payload/.env
cp ./website/.env.example ./website/.env

# 2. 安装依赖
pnpm install

# 3. 启动 Payload(Next.js 管理后台 + Local API 宿主)
pnpm run dev:payload

# 4. 在另一个终端启动 Remix 网站
pnpm run dev:website

两个 dev 脚本分别在两个终端中运行:payload 侧执行 next devpayload 子包 scripts),website 侧执行 remix vite:dev。website 的 tsconfig.json 使用 moduleResolution: "Bundler"target: "ES2022"noEmit: true,并注明“Vite takes care of building everything, not tsc”,即类型检查与构建职责分离,tsc 只做 typecheck 脚本("typecheck": "tsc")使用。Node 版本要求见 website package.jsonengines 字段:>=20.0.0

数据流:Loader/Action 中调用 Local API

website 子应用之所以能消费 Payload 数据,关键在于 app/routes/_index.tsx 中的 loader 与 action 直接导入了 getPayload 与配置:

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)
}

action 中则根据表单是否携带隐藏字段 postId 走删除或新建分支,同样通过 payload.delete / payload.create 完成写操作。这说明在 Remix 的 Node 运行时环境中,Payload 的 Local API 与在 Next.js 中一样,是以“同进程调用”方式工作的,无需认证 cookie 或 REST 序列化。

被查询的 posts 集合定义在 payload.config.ts 中:仅含一个必填 title 文本字段,数据库为 mongooseAdapter(连接串来自 DATABASE_URL 环境变量),secret 来自 PAYLOAD_SECRET。该配置还包含一个 onInit 钩子:若 posts 集合为空则创建一条种子文档 'Post 1',因此首次启动后 website 首页列表不会为空。配置中的 typescript.outputFile 指向 payload-types.ts,即 website 侧 Post 类型的来源。

生产构建与 DIY 部署(Deployment)

website 的 README 给出的部署步骤分三段:

1. 构建生产包

npm run build

对应 package.json 中的 remix vite:build,产物输出到 build/ 目录。

2. 以生产模式运行

npm start

对应 remix-serve ./build/server/index.js,即使用 @remix-run/serve 托管服务端 bundle(build/server 内的 Node 服务端入口)。

3. DIY 部署要点

README 指出:如果你熟悉 Node 应用部署,Remix 内置的应用服务器已经可以直接用于生产环境,部署时需要携带 npm run build 的两类产物:

  • build/server —— 服务端代码 bundle,由 Node 进程执行;
  • build/client —— 客户端静态资源,由 HTTP 服务器直接分发。

即任何可以跑 Node 服务 + 静态文件的托管方案(自建服务器、容器等)均可承载该应用。

Vite 构建配置的关键细节

vite.config.ts 是该子应用构建行为的核心,值得逐条拆解:

export default defineConfig({
  plugins: [
    remix({
      future: {
        v3_fetcherPersist: true,
        v3_relativeSplatPath: true,
        v3_throwAbortReason: true,
        v3_singleFetch: true,
        v3_lazyRouteDiscovery: true,
      },
    }),
    tsconfigPaths(),
  ],
  ssr: {
    external: ['sharp'],
    // Reduces Docker image size
    noExternal: process.env.NODE_ENV === 'production' ? [/.*/] : [],
  },
  optimizeDeps: {
    exclude: ['sharp', 'file-type'],
  },
})
  • future flags:启用了全部 5 个 V3 未来特性(v3_singleFetch 等),并在文件顶部通过 declare module '@remix-run/node'v3_singleFetch 类型固化为 true,保证类型层面与运行时行为一致;
  • ssr.external: ['sharp']sharp 是原生模块且依赖 __dirname 定位动态库,必须保持 external 而不被打包,否则会在服务端运行时报错(Dockerfile 中的注释也印证了这一点);
  • ssr.noExternal:生产构建时把所有依赖内联进服务端 bundle(正则 [/.*/]),开发时则不内联。文件内注释说明其目的是减小 Docker 镜像体积——生产环境无需携带完整的 node_modules
  • optimizeDeps.exclude:开发模式下对 sharpfile-type 跳过 Vite 依赖预构建,避免原生/动态加载库被错误地转译。

tsconfigPaths() 插件使 Vite 能识别 tsconfig.json 中的 ~/* -> ./app/* 路径别名,与 tsc 类型检查行为对齐。

Tailwind CSS 样式方案(Styling)

website 的 README 在 Styling 一节说明:模板已预配置 Tailwind CSS 作为默认的起步样式方案,你也可以替换为任意其他 CSS 框架。

仓库中的实际配置分三层:

  1. postcss.config.js 仅注册了 @tailwindcss/postcss 一个插件——这是 Tailwind v4 的接入方式,不再需要 tailwind.config.js 中的 content 扫描配置:

    export default {
      plugins: {
        '@tailwindcss/postcss': {},
      },
    }
    
  2. app/tailwind.css 是全局样式入口,通过 v4 风格的 @import 'tailwindcss' 引入,并用 @theme 覆盖 --font-sans 字体栈(Inter 优先),同时用 @apply 设置 html/body 的明暗背景与 prefers-color-scheme: dark 适配;

  3. app/root.tsximport './tailwind.css' 将其接入文档树,并在 <head> 中通过 LinksFunction 声明 Inter 字体的 preconnect 与样式表引用。

页面组件(如 app/routes/_index.tsx)全部使用 Tailwind 原子类编写,并配合 dark: 前缀实现暗色模式(如 text-gray-800 dark:text-gray-100)。

SSR 运行时:entry.server 与 entry.client

website 保留了 Remix 模板完整的服务端渲染约定,理解这两个文件有助于把握 build/server 产物的运行时行为:

  • app/entry.server.tsx:默认的服务端入口。它用 isbot 判断请求 UA,把流量分为两条渲染路径——爬虫走 handleBotRequestonAllReady 时输出完整 HTML,等待所有内容就绪),浏览器走 handleBrowserRequestonShellReady 时即可流式输出 HTML shell,后续内容增量填充)。两条路径都使用 renderToPipeableStream + PassThrough 流式写入响应体,并设置 ABORT_DELAY = 5_000 毫秒作为渲染中止超时;流式过程中的错误会把状态码改为 500 并记录日志。文件头注释也提示:如果该文件被 remix reveal 之外的方式改动过,可用 npx remix reveal 重新生成;
  • app/entry.client.tsx:客户端水合入口,在 startTransition 内调用 hydrateRoot,以 <StrictMode><RemixBrowser /></StrictMode> 完成对 SSR 标记的接管。

这套机制保证了 Payload 数据在 loader 中查询后可以直接出现在首屏 HTML 中,而非等待客户端请求,这也是示例选择 Remix(而非纯 SPA 前端)配合 Local API 的意义所在。

容器化部署参考:Dockerfile 实践

虽然 website 的 README 只描述了 DIY 部署的两类产物,仓库中的 website/Dockerfile 给出了一个可直接参考的多阶段构建落地方式,与前述构建细节一一对应:

FROM node:22.14.0-alpine AS base

FROM base AS builder
RUN apk add --no-cache libc6-compat
RUN npm install -g pnpm@latest-10
COPY . .
ENV NODE_ENV=production
RUN pnpm install --no-frozen-lockfile
RUN pnpm --filter website build

FROM base AS runner
RUN npm install -g @remix-run/serve@latest
COPY --from=builder /app/website/build/ ./build

# Sharp 需单独安装,避免把整个 node_modules 复制进最终镜像
RUN npm install sharp@0.32.6

# Payload 运行时需要能打开配置文件,否则会出现文件打开错误
COPY --from=builder /app/website/package.json ./package.json
COPY --from=builder /app/payload/src/payload.config.ts ./payload.config.ts
ENV PAYLOAD_CONFIG_PATH=./payload.config.ts

EXPOSE 3000
ENV PORT=3000
ENV NODE_ENV=production
CMD HOSTNAME="0.0.0.0" remix-serve build/server/index.js

其中有三处与本文前述内容相互印证、值得注意:

  1. sharp@0.32.6 在 runner 阶段单独安装,与 vite.config.tsssr.external: ['sharp'] 的 external 策略配套——sharp 不能被内联进 bundle,生产运行时以 external 模块形式存在,因此镜像里必须能解析到它,但又不能把 builder 的完整 node_modules 带进来;
  2. 通过 PAYLOAD_CONFIG_PATH=./payload.config.ts 环境变量告知 Payload 运行时配置文件位置,并把 payload.config.ts 从 builder 阶段复制到 runner 镜像(注释指出这是避免 Payload “file opening errors” 的必要文件);
  3. 最终命令 remix-serve build/server/index.js 正是 README 中 npm startremix-serve ./build/server/index.js)的容器等价物,监听 3000 端口。

小结

围绕 website 子应用 这份文档,可以把整个示例的运行链路归纳为一条清晰的主线:

环节 命令/产物 源码依据
依赖安装 pnpm install(monorepo,workspace 链接 payload-app pnpm-workspace.yaml
本地开发 pnpm run dev:payload + pnpm run dev:website(即 remix vite:dev 根 package.jsonwebsite package.json
数据读写 loader/action 中 getPayload({ config }) 调用 find/create/delete app/routes/_index.tsx
生产构建 remix vite:buildbuild/server + build/client website package.json
生产运行 remix-serve ./build/server/index.js(Node >= 20,Docker 示例用 Node 22) website package.jsonDockerfile
样式 Tailwind v4:@tailwindcss/postcss + @import 'tailwindcss' + @theme postcss.config.jstailwind.css

需要说明的适用前提:该示例基于 Remix 2.x(@remix-run/* ^2.15.2)与 Tailwind v4 编写,数据库依赖 MongoDB(mongooseAdapter),DATABASE_URLPAYLOAD_SECRET 需自行配置;它是旧版 website 模板改造的示例(见 examples/remix/README.md 的说明),用于理解“非 Next.js 框架 + Payload Local API”的集成模式,而非 Payload 官方推荐的主流接入路径——仓库中 packages/next 仍是 Next.js 集成的一等公民。

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