首页
/ Payload Live Preview 实战指南:基于 examples/live-preview 实现 Admin 面板内的前端应用实时预览

Payload Live Preview 实战指南:基于 examples/live-preview 实现 Admin 面板内的前端应用实时预览

2026-09-05 09:54:26作者:卓艾滢Kingsley

本文以 Payload 官方示例 examples/live-preview 为主体,完整讲解如何在 Payload 管理后台(Admin panel)中嵌入自己的 Next.js App Router 前端应用,实现“边打字、边预览”的 Live Preview 体验:你在后台编辑文档的每一处改动都会通过 window.postMessage 实时推送到前端应用,无需保存草稿或发布即可看到渲染结果。读完后,你将掌握 Live Preview 的两种接入方式(服务端组件刷新与客户端 Hook 订阅)、pages 集合中 admin.livePreview 配置写法,以及背后 @payloadcms/live-preview 包的消息通信实现。

Live Preview 是如何工作的

Live Preview 的原理是:Admin 面板在编辑页内渲染一个 iframe,加载你的前端应用;随后 Admin 面板通过 window.postMessage 事件与 iframe 内的应用通信——每当文档发生变化(草稿保存、自动保存或发布),Admin 都会发出一个新事件,你的前端应用监听这些事件并用收到的数据重新渲染自己。

从源码可以验证这条通信链路。@payloadcms/live-preview 包的 ready.ts 负责前端应用向父窗口“报到”:

// packages/live-preview/src/ready.ts
export const ready = (args: { serverURL: string }): void => {
  const { serverURL } = args

  if (typeof window !== 'undefined') {
    // This subscription may have been from either an iframe or a popup
    // i.e. `window?.opener` for popups, `window?.parent` for iframes
    const windowToPostTo: Window = window?.opener || window?.parent

    windowToPostTo?.postMessage(
      {
        type: 'payload-live-preview',
        ready: true,
      },
      serverURL,
    )
  }
}

注意两个细节:它同时兼容 iframe(window.parent)和弹窗(window.opener)两种宿主形态;postMessage 的第二个参数 serverURL 指定了目标 origin,保证消息只发给 Payload 服务器所在域。

反过来,前端应用识别 Admin 发来的文档事件的逻辑在 isDocumentEvent.ts

export const isDocumentEvent = (event: MessageEvent, serverURL: string): boolean =>
  event.origin === serverURL &&
  event.data &&
  typeof event.data === 'object' &&
  event.data.type === 'payload-document-event'

即通过校验 event.origindata.type === 'payload-document-event' 双重条件过滤出合法的文档变更事件,避免误处理其他来源的消息。

快速开始

以下是 examples/live-preview/README.md 中完整的 Quick Start 步骤:

  1. 运行命令从示例创建项目:

    • npx create-payload-app --example live-preview
  2. cp .env.example .env,复制示例环境变量。

  3. 确保 MongoDB 正在运行,且 DATABASE_URL 指向它(例如 mongodb://127.0.0.1/payload-example-live-preview)。

  4. pnpm devyarn devnpm run dev 启动服务器。

    • 提示 seed 数据库时按 y
  5. 打开 http://localhost:3000 访问首页。

  6. 打开 http://localhost:3000/admin 访问 Admin 面板。

    • 使用邮箱 demo@payloadcms.com、密码 demo 登录。

完成后,修改 ./src 下的任何代码都会即时反映在应用中。这个示例的关键特征:前端应用与 Payload 跑在同一个 Next.js 服务器上,因此可以直接复用同一套环境变量和 Local API。

集合配置:Users 与 Pages

示例的 payload.config.ts 注册了两个集合和一个全局对象:

// examples/live-preview/src/payload.config.ts
collections: [Pages, Users],
globals: [MainMenu],

Users 集合(认证)

users 集合启用了认证(auth: true),用于登录 Admin 面板,前端应用也基于它渲染带权限的页面。认证细节可参考 authentication 文档examples/auth 示例。

Pages 集合(启用 Live Preview)

pages 集合通过集合配置中的 admin.livePreview 属性开启 Live Preview。README 给出的简化写法是:

// ./src/collections/Pages.ts
{
  admin: {
    livePreview: {
      url: ({ data }) => `${process.env.PAYLOAD_URL}/${data.slug}`
    }
  }
}

而仓库中 Pages/index.ts 的实际实现更进一步——根据 slug 是否为 home 动态决定预览地址,并复用了全局 NEXT_PUBLIC_SERVER_URL

// examples/live-preview/src/collections/Pages/index.ts
admin: {
  defaultColumns: ['title', 'slug', 'updatedAt'],
  livePreview: {
    url: ({ data }) => {
      const isHomePage = data.slug === 'home'
      return `${process.env.NEXT_PUBLIC_SERVER_URL}${!isHomePage ? `/${data.slug}` : ''}`
    },
  },
  useAsTitle: 'title',
},
fields: [
  { name: 'title', type: 'text', required: true },
  {
    name: 'slug',
    type: 'text',
    admin: { position: 'sidebar' },
    hooks: { beforeValidate: [formatSlug('title')] },
    index: true,
    label: 'Slug',
  },
  richText(),
],
versions: {
  drafts: {
    autosave: {
      interval: 375,
    },
  },
},

这里有两点值得注意:

  • livePreview.url 返回的正是那个被 iframe 加载的页面地址。它接收当前表单数据({ data }),因此可以根据文档内容(如 slug)计算动态路由。
  • versions.drafts.autosave(自动保存间隔 375ms)是 Live Preview 体验的一部分:编辑过程中草稿被自动保存,每次保存都会触发一次 postMessage 事件,从而驱动前端刷新。read: () => true 的访问控制则允许匿名访客也能读取页面,保证前台公开可访问。

全局级别的 livePreview 还可以配置预览断点,示例中在 payload.config.ts 里注册了一个移动端断点,方便在 Admin 内按 375×667 尺寸预览:

admin: {
  livePreview: {
    breakpoints: [
      {
        name: 'mobile',
        height: 667,
        label: 'Mobile',
        width: 375,
      },
    ],
  },
},

前端接入:服务端方式(推荐)

如果你的前端框架支持 Server Components(如 React Server Components / Next.js App Router),建议采用服务端 Live Preview:它比客户端方式更简单、运行时性能更好。其工作机制是:每当文档发生变化(草稿保存、自动保存或发布),Admin 发出的 postMessage 事件触发前端调用一次服务端往返——在 Next.js 中就是调用 router.refresh(),让页面用 Local API 的最新数据重新水合 HTML。

Payload 为此提供了 RefreshRouteOnSave 组件,你只需把它“接线”到你框架的刷新函数上即可。

安装

npm install @payloadcms/live-preview-react

在页面中渲染

README 给出的 page.tsx 示例:

import { RefreshRouteOnSave } from './RefreshRouteOnSave.tsx'
import { getPayload } from 'payload'
import config from '../payload.config'

export default async function Page() {
  const payload = await getPayload({ config })

  const page = await payload.find({
    collection: 'pages',
    draft: true,
  })

  return (
    <Fragment>
      <RefreshRouteOnSave />
      <h1>{page.title}</h1>
    </Fragment>
  )
}

配套的 RefreshRouteOnSave.tsx

'use client'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation.js'
import React from 'react'

export const RefreshRouteOnSave: React.FC = () => {
  const router = useRouter()
  return <PayloadLivePreview refresh={router.refresh} serverURL={process.env.PAYLOAD_SERVER_URL} />
}

示例中的真实写法

examples/live-preview/src/app/(app)/[slug]/page.tsx 展示了更完整的实现:按路由参数 slug 查询文档(缺省为 home),查询时带 draft: true 以便预览草稿态内容,找不到文档则 notFound()

// examples/live-preview/src/app/(app)/[slug]/page.tsx
export default async function Page({ params: paramsPromise }: PageParams) {
  const { slug = 'home' } = await paramsPromise
  const payload = await getPayload({ config })

  const pageRes = await payload.find({
    collection: 'pages',
    draft: true,
    limit: 1,
    where: {
      slug: { equals: slug },
    },
  })

  const data = pageRes?.docs?.[0] as null | PageType

  if (data === null) {
    return notFound()
  }

  return (
    <Fragment>
      <RefreshRouteOnSave />
      <main className={classes.page}>
        <Gutter>
          <RichText content={data?.richText} />
        </Gutter>
      </main>
    </Fragment>
  )
}

同目录的 RefreshRouteOnSave.tsx/[slug]/RefreshRouteOnSave.tsx) 则把 router.refresh 包成箭头函数传入,serverURL 取自 NEXT_PUBLIC_SERVER_URL

// examples/live-preview/src/app/(app)/[slug]/RefreshRouteOnSave.tsx
export const RefreshRouteOnSave: React.FC = () => {
  const router = useRouter()
  return (
    <PayloadLivePreview
      refresh={() => router.refresh()}
      serverURL={process.env.NEXT_PUBLIC_SERVER_URL || ''}
    />
  )
}

组件的源码实现

RefreshRouteOnSave 组件本身在 packages/live-preview-react/src/RefreshRouteOnSave.tsx,它的行为可以逐行拆开看:

// packages/live-preview-react/src/RefreshRouteOnSave.tsx(节选)
export const RefreshRouteOnSave: React.FC<{
  apiRoute?: string
  depth?: number
  refresh: () => void
  serverURL: string
}> = (props) => {
  const { apiRoute, depth, refresh, serverURL } = props
  const hasSentReadyMessage = useRef<boolean>(false)

  const onMessage = useCallback(
    (event: MessageEvent) => {
      if (isDocumentEvent(event, serverURL)) {
        if (typeof refresh === 'function') {
          refresh()
        } else {
          console.error('You must provide a refresh function to `RefreshRouteOnSave`')
        }
      }
    },
    [refresh, serverURL],
  )

  useEffect(() => {
    // 监听 message 事件
    window.addEventListener('message', onMessage)

    if (!hasSentReadyMessage.current) {
      hasSentReadyMessage.current = true
      ready({ serverURL })
      // refresh after the ready message is sent to get the latest data
      refresh()
    }

    return () => {
      window.removeEventListener('message', onMessage)
    }
  }, [serverURL, onMessage, depth, apiRoute, refresh])

  return null
}

从源码结构看,它做了四件事:挂载时监听 message 事件;用 isDocumentEvent 校验来源与事件类型,命中后调用你传入的 refresh();首次挂载时发送 ready 消息通知 Admin 面板预览已就绪,并立即 refresh() 一次以拉取最新数据;组件渲染为 null(纯副作用组件,不输出 DOM)。hasSentReadyMessage 引用保证了 StrictMode 或 effect 重跑时不会重复发送 ready。

服务端方式只适用于支持 Server Components 的前端框架。如果你的前端是 Next.js Pages Router、React Router、Vue 3 等纯客户端框架,请看下一节的客户端方式。

前端接入:客户端方式

React:useLivePreview Hook

客户端方式不需要每次刷新整条路由,而是直接订阅 postMessage 事件并就地更新数据。React 用户可使用 @payloadcms/live-preview-react 提供的 useLivePreview Hook:

npm install @payloadcms/live-preview-react
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import { Page as PageType } from '@/payload-types'

// 在服务端组件中取数,作为 initialData 传入客户端组件,
// Hook 会接管后续同步;`data` 属性始终是文档的最新实时数据
export const PageClient: React.FC<{
  page: { title: string }
}> = ({ page: initialPage }) => {
  const { data } = useLivePreview<PageType>({
    initialData: initialPage,
    serverURL: PAYLOAD_SERVER_URL,
    depth: 2, // 确保与请求 initialPage 时的 depth 一致
  })

  return <h1>{data.title}</h1>
}

Hook 的完整实现见 packages/live-preview-react/src/useLivePreview.ts。它接受 initialDataserverURL、可选的 depthapiRoute 与自定义 requestHandler,返回 { data, isLoading }isLoading 可在 postMessage 数据尚在合并时用于条件渲染加载态,避免旧数据闪烁;Hook 内部同样会发送 ready 消息,并在卸载时 unsubscribe

框架无关的底层工具

对于 Vue、Svelte 等其他框架,可以基于 @payloadcms/live-preview 包自行构建 Hook:

npm install @payloadcms/live-preview
import { subscribe, unsubscribe } from '@payloadcms/live-preview'

// 自行构建 Hook:
// 1. 订阅 `window.postMessage` 事件
// 2. 将初始页面数据与传入的表单状态合并
// 3. populate 关联关系与上传文件

subscribe.ts 的实现印证了上述注释:

// packages/live-preview/src/subscribe.ts(节选)
export const subscribe = <T extends Record<string, any>>(args: {
  apiRoute?: string
  callback: (data: T) => void
  depth?: number
  initialData: T
  requestHandler?: CollectionPopulationRequestHandler
  serverURL: string
}) => {
  const { apiRoute, callback, depth, initialData, requestHandler, serverURL } = args

  // 清除内部缓存,避免导航之间残留上一次的订阅状态
  resetCache()

  const onMessage = async (event: MessageEvent) => {
    const mergedData = await handleMessage<T>({
      apiRoute, depth, event, initialData, requestHandler, serverURL,
    })
    callback(mergedData)
  }

  if (typeof window !== 'undefined') {
    window.addEventListener('message', onMessage)
  }

  return onMessage
}

返回的 onMessage 句柄即 unsubscribe 的参数,保证订阅只存在一次、卸载时能干净移除。

数据合并与服务端回填

事件里携带的是表单原始状态,其中的关系字段还是 ID。mergeData.ts 负责向 Payload 服务器发起回填请求,把关系与上传文件“populate”成完整对象后再与 initialData 深度合并。其默认请求处理器值得注意:

// packages/live-preview/src/mergeData.ts(节选)
const defaultRequestHandler: CollectionPopulationRequestHandler = ({
  apiPath, data, endpoint, serverURL,
}) => {
  const url = `${serverURL}${apiPath}/${endpoint}`

  return fetch(url, {
    body: JSON.stringify(data),
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-Payload-HTTP-Method-Override': 'GET',
    },
    method: 'POST',
  })
}

要点:请求体包含 incomingDatadepthlocaleflattenLocales: false;URL 由 apiRoute(默认 /api)拼上 globals/{slug}{collectionSlug}/{id} 端点构成;使用 POST + X-Payload-HTTP-Method-Override: GET 头实现语义上的 GET 查询(适合跨域代理场景),并带 credentials: 'include' 以携带会话 Cookie。该函数还支持传入自定义 requestHandler 拦截请求(例如经中间件路由)。

开发:Seed 脚本

启动时示例附带了 seed 脚本,用于快速搭建一个可用的基础数据库。可以随时运行 pnpm seed 重新播种;如果不想在每次 dev 时自动播种,把 package.jsondev 脚本里的 pnpm seed 移除即可(当前 dev 脚本为 pnpm seed && next devseed 实际执行 payload migrate:fresh)。

seed.ts 会创建:

  • 一个邮箱 demo@payloadcms.com、密码 demo 的用户;
  • 一个首页(slug 为 home,含示例富文本)和一个 example-page 示例页;
  • 两个版本的首页(一个已发布、一个草稿),用于演示草稿/版本机制;
  • 一个 main-menu 全局导航(Home、Example Page、Dashboard 三个入口)。

注意:seed 是破坏性操作,它会 drop 当前数据库并从模板重建。只在启动新项目或能接受数据丢失时执行。

生产构建与部署

在生产环境运行 Payload 需要构建并服务 Admin 面板:

  1. 运行 pnpm build(或 npm run build)执行 next build,生成包含生产级 admin bundle 的 .next 目录(示例中对应的脚本为 cross-env NODE_OPTIONS=--no-deprecation payload build)。
  2. 运行 pnpm start(或 npm run start)以生产模式运行 Node 并服务 Payload。

部署方面,README 建议使用 Payload Cloud 一键部署,或选择自托管,可参考 deployment 文档

小结

这个示例完整覆盖了 Live Preview 的四个关键位置,形成一条可追溯的链路:

环节 位置 职责
集合配置 examples/live-preview/src/collections/Pages/index.ts admin.livePreview.url 决定 iframe 加载地址;versions.drafts.autosave 驱动保存事件
全局配置 examples/live-preview/src/payload.config.ts admin.livePreview.breakpoints 定义预览断点
服务端渲染 examples/live-preview/src/app/(app)/[slug]/page.tsx payload.find({ draft: true }) + RefreshRouteOnSave 触发 router.refresh()
通信层 packages/live-preview-react/src/RefreshRouteOnSave.tsxpackages/live-preview/src/ready.ts ready 报到、isDocumentEvent 校验、subscribe/mergeData 合并回填

掌握这套模式后,你可以为任意支持 Server Components 的前端(或基于底层工具自建 Hook 的任意框架)接入 Payload 的实时预览能力。更多配置细节可继续阅读 live-preview 文档目录前端接入文档服务端接入文档

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