Puck 高可定制、高性能无头页面编辑器框架
构建页面编辑器时,你是不是遇到过:
-
• UI 限制严重:现有编辑器 UI 固定,难以定制 -
• 组件系统僵化:无法灵活添加自定义组件 -
• 数据结构复杂:编辑器数据格式难以理解和扩展 -
• 拖拽逻辑复杂:自己实现拖拽、布局太复杂 -
• 性能问题:复杂页面渲染性能不佳 -
• 扩展性差:难以添加自定义功能和插件
Puck 来了:它是一个无头页面编辑器框架,拖拽式编辑、组件系统、数据驱动、完全可定制、性能优化,让页面编辑器开发变得超级简单且高效!
Puck 是什么?
✅ 无头设计:不提供 UI,完全自定义
✅ 拖拽编辑:内置拖拽功能
✅ 组件系统:灵活的组件注册机制
✅ 数据驱动:基于 JSON 数据结构
✅ 完全可定制:UI、行为、样式完全可控
✅ 性能优化:虚拟渲染,高性能
✅ TypeScript 支持:完整的类型定义
✅ 框架无关:支持 React、Vue 等
快速上手
1️⃣ 安装
npm install puck
# 或者
pnpm add puck
2️⃣ 基础用法
// App.tsx
import { Puck } from "puck";
// 定义组件
const components = {
HeadingBlock: {
render: ({ props }) =><h1>{props.text}</h1>,
config: {
fields: {
text: {
type: "text",
label: "标题文本",
},
},
},
},
TextBlock: {
render: ({ props }) =><p>{props.text}</p>,
config: {
fields: {
text: {
type: "textarea",
label: "文本内容",
},
},
},
},
};
// 初始数据
const initialData = {
content: [
{
type: "HeadingBlock",
props: {
text: "Hello Puck!",
},
},
{
type: "TextBlock",
props: {
text: "This is a demo of the headless page editor.",
},
},
],
};
export default function App() {
return (
<div style={{ height: "100vh" }}>
<Puck
config={{ components }}
data={initialData}
onPublish={async (data) => {
console.log("发布数据:", data);
}}
/>
</div>
);
}
3️⃣ 自定义 UI
import { Puck, usePuck } from "puck";
// 自定义侧边栏
const Sidebar = () => {
const { appState, dispatch } = usePuck();
return (
<div className="sidebar">
<h3>组件库</h3>
{Object.keys(appState.config.components).map((key) => (
<div
key={key}
draggable
onDragStart={(e) => {
e.dataTransfer.setData("puck-component", key);
}}
>
{key}
</div>
))}
</div>
);
};
// 自定义编辑器
const Editor = () => {
const { appState, dispatch } = usePuck();
return (
<div className="editor">
{appState.data.content.map((item, index) => (
<div key={index} className="editor-item">
{appState.config.components[item.type].render({
props: item.props,
})}
</div>
))}
</div>
);
};
export default function App() {
return (
<div style={{ display: "flex", height: "100vh" }}>
<Sidebar />
<Editor />
</div>
);
}
核心功能
组件系统
// 定义复杂组件
const CardBlock = {
render: ({ props }) => (
<div className="card">
<img src={props.image} alt={props.title} />
<h3>{props.title}</h3>
<a href={props.link}>了解更多</a>
</div>
),
config: {
fields: {
image: {
type: "image",
label: "图片",
},
title: {
type: "text",
label: "标题",
},
link: {
type: "text",
label: "链接",
},
},
},
};
// 定义嵌套组件
const ContainerBlock = {
render: ({ props, children }) => (
<div className="container" style={{ padding: props.padding }}>
{children}
</div>
),
config: {
fields: {
padding: {
type: "number",
label: "内边距",
},
},
},
root: true, // 允许包含子组件
};
// 注册组件
const components = {
CardBlock,
ContainerBlock,
};
拖拽功能
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
const DraggableEditor = () => {
const { appState, dispatch } = usePuck();
const onDragEnd = (result) => {
if (!result.destination) return;
const items = Array.from(appState.data.content);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
dispatch({
type: "setData",
data: {
...appState.data,
content: items,
},
});
};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="editor">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{appState.data.content.map((item, index) => (
<Draggable key={index} draggableId={String(index)} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{appState.config.components[item.type].render({
props: item.props,
})}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
预览模式
// 预览模式切换
const PreviewToggle = () => {
const { appState, dispatch } = usePuck();
const togglePreview = () => {
dispatch({
type: "setUi",
ui: {
...appState.ui,
previewMode: !appState.ui.previewMode,
},
});
};
return (
<button onClick={togglePreview}>
{appState.ui.previewMode ? "编辑模式" : "预览模式"}
</button>
);
};
// 预览渲染
const Preview = () => {
const { appState } = usePuck();
return (
<div className="preview">
{appState.data.content.map((item, index) => (
<div key={index}>
{appState.config.components[item.type].render({
props: item.props,
})}
</div>
))}
</div>
);
};
撤销重做
// 撤销重做功能
const UndoRedo = () => {
const { appState, dispatch } = usePuck();
const handleUndo = () => {
dispatch({ type: "undo" });
};
const handleRedo = () => {
dispatch({ type: "redo" });
};
return (
<div>
<button onClick={handleUndo} disabled={!appState.history.canUndo}>
撤销
</button>
<button onClick={handleRedo} disabled={!appState.history.canRedo}>
重做
</button>
</div>
);
};
Puck vs 其他方案?
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
最佳实践
组件设计:保持组件单一职责,提高复用性
数据结构:使用清晰的 JSON 结构,便于理解和扩展
UI 定制:根据需求完全自定义 UI,提供最佳用户体验
性能优化:使用虚拟渲染,优化复杂页面性能
类型安全:使用 TypeScript,提高代码质量
状态管理:合理使用 dispatch 和 appState,保持状态同步
撤销重做:充分利用撤销重做功能,提高编辑体验
数据持久化:定期保存数据,避免数据丢失
总结
Puck 是构建自定义页面编辑器的最佳选择!
它功能强大、使用简单、无头设计,支持拖拽编辑、组件系统、数据驱动、完全可定制、性能优化。不管是落地页编辑器、博客文章编辑器还是电商产品页面编辑器,都能轻松应对。如果你正在开发自定义页面编辑器项目,强烈推荐试试 Puck!
夜雨聆风