客户端实时预览
如果你的前端应用支持 Next.js App Router 等 Server Components,我们建议设置服务端 Live Preview。
使用 Live Preview 时,Admin Panel 会在文档每次变更时发出一个新的 window.postMessage
事件。你的前端应用可以监听这些事件并相应地重新渲染。
如果你的前端应用使用 React 或 Vue 构建,可以使用 Payload 提供的 useLivePreview
钩子。未来将官方支持所有其他主流框架如 Svelte。如果你现在使用这些框架,仍然可以利用 Payload 提供的底层工具自行集成 Live Preview。更多信息请参阅构建自定义钩子。
默认情况下,所有钩子都接受以下参数:
Path | Description |
---|---|
serverURL * | Payload 服务器的 URL |
initialData | 文档的初始数据。变更时实时数据会被合并进来 |
depth | 获取关系的深度。默认为 0 |
apiRoute | 在 routes.api 中定义的 API 路由路径。默认为 /api |
* 星号表示该属性为必填项
并返回以下值:
Path | Description |
---|---|
data | 合并了初始数据的文档实时数据 |
isLoading | 表示文档是否正在加载的布尔值 |
如果你的前端与必填字段紧密耦合,应确保当这些字段被移除时 UI 不会崩溃。例如,如果你渲染类似 data.relatedPosts[0].title
的内容,当第一个相关文章被移除时页面会崩溃。为避免这种情况,请在 UI 中使用条件逻辑、可选链或默认值。例如 data?.relatedPosts?.[0]?.title
。
确保 depth
参数与初始页面请求的深度完全匹配非常重要。depth 属性用于填充超出 ID 的关系和上传字段。更多信息请参阅深度查询。
框架支持
Live Preview 功能可与任何支持原生 window.postMessage
API 的前端框架配合使用。Payload 默认官方支持以下主流框架:
如果你的框架不在列表中,仍可使用 Payload 提供的底层工具集成 Live Preview 功能。了解更多详情。
React
如果你的前端应用使用客户端渲染的 React(如 Next.js Pages Router),可以使用 Payload 提供的 useLivePreview
钩子。
首先安装 @payloadcms/live-preview-react
包:
npm install @payloadcms/live-preview-react
然后在 React 组件中使用 useLivePreview
钩子:
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import { Page as PageType } from '@/payload-types'
// 在服务端组件中获取页面数据,传递给客户端组件,再通过钩子传递
// 钩子将接管后续工作,保持预览与你的更改同步
// `data` 属性将包含文档的实时数据
export const PageClient: React.FC<{
page: {
title: string
}
}> = ({ page: initialPage }) => {
const { data } = useLivePreview<PageType>({
initialData: initialPage,
serverURL: PAYLOAD_SERVER_URL,
depth: 2,
})
return <h1>{data.title}</h1>
}
重要提示: 如果使用 React Server Components,我们强烈建议改用服务端 Live Preview 方案。
Vue
如果你的前端应用使用 Vue 3 或 Nuxt 3 构建,你可以使用 Payload 提供的 useLivePreview
组合式函数。
首先,安装 @payloadcms/live-preview-vue
包:
npm install @payloadcms/live-preview-vue
然后,在你的 Vue 组件中使用 useLivePreview
钩子:
<script setup lang="ts">
import type { PageData } from '~/types';
import { defineProps } from 'vue';
import { useLivePreview } from '@payloadcms/live-preview-vue';
// 在父组件中获取初始数据或使用异步状态
const props = defineProps<{ initialData: PageData }>();
// 该钩子会接管后续工作,保持预览与你的更改同步。
// 只有当从 Admin UI 的预览视图查看时,`data` 属性才会包含文档的实时数据。
const { data } = useLivePreview<PageData>({
initialData: props.initialData,
serverURL: "<PAYLOAD_SERVER_URL>",
depth: 2,
});
</script>
<template>
<h1>{{ data.title }}</h1>
</template>
构建自定义钩子
无论你使用什么前端框架,都可以利用 Payload 提供的底层工具来构建自己的钩子。
首先,安装基础包 @payloadcms/live-preview
:
npm install @payloadcms/live-preview
这个包提供了以下函数:
路径 | 描述 |
---|---|
subscribe | 订阅 Admin Panel 的 window.postMessage 事件,并调用提供的回调函数。 |
unsubscribe | 取消订阅 Admin Panel 的 window.postMessage 事件。 |
ready | 向 Admin Panel 发送 window.postMessage 事件,表示前端已准备好接收消息。 |
isLivePreviewEvent | 检查 MessageEvent 是否来自 Admin Panel 且是 Live Preview 事件(即防抖的表单状态)。 |
subscribe
函数接收以下参数:
路径 | 描述 |
---|---|
callback * | 每次文档变更时都会调用该回调函数,并传入 data 。 |
serverURL * | Payload 服务器的 URL。 |
initialData | 文档的初始数据。变更时会将实时数据合并进来。 |
depth | 要获取的关系深度,默认为 0 。 |
利用这些函数,你可以基于任意前端框架构建自己的钩子:
import { subscribe, unsubscribe } from '@payloadcms/live-preview'
// 构建自定义钩子时,使用 `subscribe` 函数订阅 Live Preview 事件
// 它处理了以下所有事项:
// 1. 监听 `window.postMessage` 事件
// 2. 合并初始数据和当前表单状态
// 3. 填充关系和上传内容
// 4. 调用 `onChange` 回调并传入结果
// 你的钩子还应该:
// 1. 通知 Admin Panel 何时准备好接收消息
// 2. 处理 `onChange` 回调结果来更新 UI
// 3. 在卸载时取消订阅 `window.postMessage` 事件
以下是上述 useLivePreview
React 钩子的底层实现示例:
import { subscribe, unsubscribe, ready } from '@payloadcms/live-preview'
import { useCallback, useEffect, useState, useRef } from 'react'
export const useLivePreview = <T extends any>(props: {
depth?: number
initialData: T
serverURL: string
}): {
data: T
isLoading: boolean
} => {
const { depth = 0, initialData, serverURL } = props
const [data, setData] = useState<T>(initialData)
const [isLoading, setIsLoading] = useState<boolean>(true)
const hasSentReadyMessage = useRef<boolean>(false)
const onChange = useCallback((mergedData) => {
// 当变更发生时,`onChange` 回调会被调用并传入合并后的数据
// 将合并数据存入状态以触发 React 重新渲染 UI
setData(mergedData)
setIsLoading(false)
}, [])
useEffect(() => {
// 监听来自 Admin Panel 的 `window.postMessage` 事件
// 当变更发生时,`onChange` 回调会被调用并传入合并后的数据
const subscription = subscribe({
callback: onChange,
depth,
initialData,
serverURL,
})
// 订阅后,向 Admin Panel 发送 `ready` 消息
// 这表示前端已准备好接收消息
if (!hasSentReadyMessage.current) {
hasSentReadyMessage.current = true
ready({
serverURL,
})
}
// 组件卸载时取消订阅 `window.postMessage` 事件
return () => {
unsubscribe(subscription)
}
}, [serverURL, onChange, depth, initialData])
return {
data,
isLoading,
}
}
构建自定义钩子时,请确保参数和返回值与文档开头列出的保持一致。这将保证所有钩子遵循相同的 API 规范。
示例
要查看实际演示,请参考官方的 Live Preview 示例。那里提供了一个完整的 Next.js App Router 前端集成示例,该前端与 Payload 运行在同一服务器上。
故障排除
关系和/或上传字段未填充
如果你的前端应用使用了关系字段或上传字段,并且前端应用运行在与 Payload 服务器不同的域名上,你可能需要配置 CORS 以允许两个域名之间的请求。这包括运行在不同端口或子域上的站点。同样,如果你将资源保护在用户认证之后,可能还需要配置 CSRF 以允许在两个域名之间发送 cookies。例如:
// payload.config.ts
{
// ...
// 如果你的站点运行在与 Payload 服务器不同的域名上,
// 这将允许在两个域名之间发起请求
cors: [
'http://localhost:3001' // 你的前端应用
],
// 如果你将资源保护在用户认证之后,
// 这将允许在两个域名之间发送 cookies
csrf: [
'http://localhost:3001' // 你的前端应用
],
}
编辑文档后关系字段和/或上传文件消失
这可能是因为你在初始请求和/或 useLivePreview
钩子中设置了不正确的 depth
参数,或者两者不匹配。请确保 depth
参数设置为正确的值,并且在两个地方完全一致。例如:
// 初始请求
const { docs } = await payload.find({
collection: 'pages',
depth: 1, // 确保设置为适合你应用的深度
where: {
slug: {
equals: 'home',
},
},
})
// 钩子
const { data } = useLivePreview<PageType>({
initialData: initialPage,
serverURL: PAYLOAD_SERVER_URL,
depth: 1, // 确保与初始请求的深度一致
})
Iframe 拒绝连接
如果你的前端应用设置了 内容安全策略 (CSP) 阻止 Admin Panel 加载你的前端应用,iframe 将无法加载你的站点。要解决这个问题,你可以在 CSP 中通过设置 frame-ancestors
指令将 Admin Panel 的域名加入白名单:
frame-ancestors: "self" localhost:* https://your-site.com;