乐于分享
好东西不私藏

Puck 高可定制、高性能无头页面编辑器框架

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 其他方案?

功能
Puck
GrapesJS
TinyMCE
CKEditor
无头设计
✅ 完全
⚠️ 部分
❌ 无
❌ 无
组件系统
✅ 灵活
✅ 灵活
⚠️ 有限
⚠️ 有限
数据驱动
✅ JSON
✅ HTML
✅ HTML
✅ HTML
拖拽编辑
✅ 内置
✅ 内置
❌ 无
❌ 无
UI 可定制
✅ 完全
⚠️ 部分
⚠️ 有限
⚠️ 有限
性能优化
✅ 虚拟渲染
⚠️ 中等
✅ 优化
✅ 优化
TypeScript 支持
✅ 完整
⚠️ 部分
✅ 完整
✅ 完整
学习曲线
⚠️ 中等
⚠️ 中等
✅ 平缓
✅ 平缓
轻量级
✅ 30KB
⚠️ 60KB
⚠️ 100KB
⚠️ 150KB

最佳实践

组件设计:保持组件单一职责,提高复用性
数据结构:使用清晰的 JSON 结构,便于理解和扩展
UI 定制:根据需求完全自定义 UI,提供最佳用户体验
性能优化:使用虚拟渲染,优化复杂页面性能
类型安全:使用 TypeScript,提高代码质量
状态管理:合理使用 dispatch 和 appState,保持状态同步
撤销重做:充分利用撤销重做功能,提高编辑体验
数据持久化:定期保存数据,避免数据丢失

总结

Puck 是构建自定义页面编辑器的最佳选择!

它功能强大、使用简单、无头设计,支持拖拽编辑、组件系统、数据驱动、完全可定制、性能优化。不管是落地页编辑器、博客文章编辑器还是电商产品页面编辑器,都能轻松应对。如果你正在开发自定义页面编辑器项目,强烈推荐试试 Puck!