Payload Live Preview 实战指南:基于 examples/live-preview 实现 Admin 面板内的前端应用实时预览
本文以 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.origin 与 data.type === 'payload-document-event' 双重条件过滤出合法的文档变更事件,避免误处理其他来源的消息。
快速开始
以下是 examples/live-preview/README.md 中完整的 Quick Start 步骤:
-
运行命令从示例创建项目:
npx create-payload-app --example live-preview
-
cp .env.example .env,复制示例环境变量。 -
确保 MongoDB 正在运行,且
DATABASE_URL指向它(例如mongodb://127.0.0.1/payload-example-live-preview)。 -
用
pnpm dev、yarn dev或npm run dev启动服务器。- 提示 seed 数据库时按
y。
- 提示 seed 数据库时按
-
打开
http://localhost:3000访问首页。 -
打开
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。它接受 initialData、serverURL、可选的 depth、apiRoute 与自定义 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',
})
}
要点:请求体包含 incomingData、depth、locale 与 flattenLocales: 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.json 中 dev 脚本里的 pnpm seed 移除即可(当前 dev 脚本为 pnpm seed && next dev,seed 实际执行 payload migrate:fresh)。
seed.ts 会创建:
- 一个邮箱
demo@payloadcms.com、密码demo的用户; - 一个首页(slug 为
home,含示例富文本)和一个example-page示例页; - 两个版本的首页(一个已发布、一个草稿),用于演示草稿/版本机制;
- 一个
main-menu全局导航(Home、Example Page、Dashboard 三个入口)。
注意:seed 是破坏性操作,它会 drop 当前数据库并从模板重建。只在启动新项目或能接受数据丢失时执行。
生产构建与部署
在生产环境运行 Payload 需要构建并服务 Admin 面板:
- 运行
pnpm build(或npm run build)执行next build,生成包含生产级 admin bundle 的.next目录(示例中对应的脚本为cross-env NODE_OPTIONS=--no-deprecation payload build)。 - 运行
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.tsx、packages/live-preview/src/ready.ts | ready 报到、isDocumentEvent 校验、subscribe/mergeData 合并回填 |
掌握这套模式后,你可以为任意支持 Server Components 的前端(或基于底层工具自建 Hook 的任意框架)接入 Payload 的实时预览能力。更多配置细节可继续阅读 live-preview 文档目录、前端接入文档 与 服务端接入文档。
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