Craft.js 可拖拽页面编辑器实战:examples/basic 示例运行与源码级拆解 前端【免费下载链接】craft.js A React Framework for building extensible drag and drop page editors项目地址https://gitcode.com/gh_mirrors/cr/craft.js点击查看免费下载examples/basic是 Craft.js 官方 Basic 教程basic-tutorial的最终成品一个功能完整、可直接运行的页面编辑器示例支持从工具箱拖拽新建组件、在画布内自由移动元素、通过属性面板编辑选中组件、开关编辑模式以及序列化 / 反序列化整个编辑器状态。读完本文你将掌握该示例的启动方式、整体架构Editor / Frame / Element 的分工、User Component 的编写范式useNode、connectors、craft 静态配置以及撤销重做、状态保存等进阶能力的实现方式。示例概览Basic demo 是什么examples/basic/README.md对该示例的定位只有一句话它是官方 basic-tutorial 教程的最终结果This example is the end result of the tutorial。换言之这个目录不是零散的功能演示而是把教程中每一步的代码完整落地的产物。目录内包含examples/basic/pages/index.js编辑器应用的唯一页面组装 Editor、Frame、Toolbox、SettingsPanel、Topbarexamples/basic/components/user/四个可编辑的用户组件Button、Text、Container、Cardexamples/basic/components/三个编辑器外壳组件Toolbox、SettingsPanel、Topbarexamples/basic/package.json、examples/basic/next.config.js基于 Next.js 15 React 19 MUI v6 的工程配置。该示例同时提供在线演示环境见 README 的 live demo 说明读者可以边看效果边对照源码。其覆盖的功能点与教程章节一一对应功能关键文件对应教程小节用户组件User Componentscomponents/user/Text.js、Button.js、Container.js、Card.jsUser Components可拖放区域与规则Card.js 中的 CardTop / CardBottomDefining Droppable regions从工具箱拖拽新建components/Toolbox.jsImplementing the Toolbox属性编辑面板components/SettingsPanel.jsSettings Panel开关 / 撤销重做 / 序列化components/Topbar.jsTopbar快速运行 examples/basicREADME 给出的启动方式是标准的 Node 工作流。由于本示例已经位于当前仓库内你只需进入示例目录安装依赖并启动即可示例使用workspace:*引用 monorepo 中的craftjs/core包见 examples/basic/package.json因此直接从仓库根目录安装依赖即可解析本地包# 如果你需要先将仓库克隆到本地以当前仓库地址为例 git clone https://gitcode.com/gh_mirrors/cr/craft.js # 进入示例目录 cd craft.js/examples/basic # 安装依赖 npm install # 启动开发服务器 npm startnpm start实际执行的是next dev -p 3002见 package.json 的 scripts 定义即 Next.js 开发服务器运行在3002 端口——注意这不是默认的 3000访问时请使用http://localhost:3002。启动后你会看到标题为 Basic Page Editor 的编辑器页面左侧是可编辑画布右侧是工具箱Toolbox与属性面板Settings Panel。依赖方面示例在 package.json 中声明了以下与编辑器功能直接相关的库craftjs/coreworkspace:*编辑器核心通过 monorepo 工作区直接引用本仓库 packages/core 的源码react-contenteditable实现 Text 组件的行内内容编辑react-colorful提供 Container / Card 背景色的十六进制取色器copy-to-clipboard与lzutf8Topbar 中复制 / 加载编辑器状态所需的剪贴板与压缩/解压能力。示例的总体架构Editor、Frame、Element 的分工打开 pages/index.js整个编辑器被组织成一个清晰的三层结构Editor resolver{{ Card, Button, Text, Container, CardTop, CardBottom }} Topbar / Grid container spacing{5} Grid size{8} Frame Element canvas is{Container} padding{5} background#eeeeee>export const Text ({ text, fontSize, textAlign, ...props }) { const { connectors: { connect, drag }, selected, actions: { setProp }, } useNode((state) ({ selected: state.events.selected, dragged: state.events.dragged, })); const [editable, setEditable] useState(false); useEffect(() { if (selected) return; setEditable(false); }, [selected]); return ( div {...props} ref{(ref) connect(drag(ref))} onClick{() selected setEditable(true)} ContentEditable html{text} disabled{!editable} onChange{(e) setProp((props) (props.text e.target.value.replace(/\/?[^](|$)/g, )), 500) } tagNamep style{{ fontSize: ${fontSize}px, textAlign }} / /div ); };关键点useNode收集器useNode((state) ({...}))接收一个函数从 Node 状态中订阅events.selected是否被选中和events.dragged是否被拖动从而驱动组件响应编辑器事件行内编辑react-contenteditable实现所见即所得的文本编辑。编辑只在组件已被选中后再次点击才开启双击语义取消选中则自动退出编辑态onChange里用正则剔除粘贴进来的 HTML 标签并把结果写回 propssetProp的第二个参数示例传入500或1000见 Button / Container 中的用法用于控制高频更新场景如拖拽滑块下的更新频率具体接口定义可参考 useNode。ButtonMaterial 组件 属性配置Button.js 把 Craft.js 的 connector 挂到 MUI 按钮上export const Button ({ size, variant, color, text, ...props }) { const { connectors: { connect, drag } } useNode(); return ( MaterialButton ref{(ref) connect(drag(ref))} size{size} variant{variant} color{color} {...props} {text} /MaterialButton ); };ref{(ref) connect(drag(ref))}的写法值得单独解释教程 Enabling Drag and Drop 一节connect告知 Craft.js 这个 DOM 元素代表整个 Button 组件如果该组件的 Node 是 Canvas它同时定义了可投放区域drag为该 DOM 添加拖拽事件处理当组件是某个 Canvas 的直接子节点时用户拖动它就会移动整个 Button。两者嵌套在同一个 ref 回调里是可识别 可拖拽的最小标准写法。Container可投放容器 颜色/内边距编辑Container.js 用 MUIPaper承载子节点并通过useNode读取background、padding两个 props 供设置面板使用。它的特殊之处在于同时作为根画布index.js 中最外层Element canvas is{Container}和普通组件出现因此它既可以被拖拽connect(drag(ref))又因为其 Node 是 Canvas 而可投放子元素——这正是前面 Node / Canvas 规则的直接体现。Card组合组件与两个受约束的可投放区域Card.js 是最复杂的用户组件它由 Container 组合而成内部划分出两个区域——上半区只允许放入 Text下半区只允许放入 Buttonexport const CardTop ({ children, ...props }) { const { connectors: { connect } } useNode(); return ( div {...props} ref{connect} classNametext-only style{{ /* ... */ }} {children} /div ); }; CardTop.craft { rules: { canMoveIn: (incomingNodes) incomingNodes.every((incomingNode) incomingNode.data.type Text), }, };这里有两个实战要点User Component 内部也可以创建 Canvas 节点Card 内部的两个区域同样用Element canvas idtext is{CardTop}声明注意User Component 内部的Element必须显式指定id教程对此有明确提示。这样每个区域是独立 Node有自己的投放规则rules.canMoveIn约束投放内容canMoveIn接收试图移入的节点列表多选拖拽时可能是多个通过检查incomingNode.data.type是否等于目标组件来放行或拒绝。同理CardBottom只接受 Button。这种限制拖入类型的规则就是页面编辑器中实现结构约束的标准手段。此外CardTop/CardBottom只挂connect而不挂drag因为这两个区域本身不作为拖拽对象教程明确说明加上 drag handler 毫无意义——这是只为需要的元素挂 connector的最佳实践。craft 静态配置props、related、rules每个用户组件都通过静态属性Component.craft声明元数据教程 Setting default props 一节Text.craft { props: { text: Hi, fontSize: 20 }, // 默认 props related: { settings: TextSettings }, // 相关组件属性面板 // rules: { canDrag: ... } // 可选拖拽规则 };props默认属性。教程强调这并非严格必需但当我们要在设置面板里通过 Node 读取组件 props 时如node.data.props.fontSize没有默认值就可能拿到null因此建议始终定义related相关组件与用户组件共享同一个 Node 上下文。示例把TextSettings、ButtonSettings、ContainerSettings注册为settings供属性面板在任意位置渲染下一节详述rules拖拽 / 投放规则。教程还示范了canDrag: (node) node.data.props.text ! Drag这类条件拖拽限制。Toolbox把组件从外部拖进画布Toolbox.js 展示了与useNode相对的编辑器级 hook——useEditorexport const Toolbox () { const { connectors } useEditor(); return ( MaterialButton ref{(ref) connectors.create(ref, Button textClick me sizesmall /)} variantcontained Button /MaterialButton ); };connectors.create教程 Implementing the Toolbox 一节接收两个参数第一个是拖拽源 DOM 的 ref第二个是拖入画布后要创建的 JSX 元素。用户从工具箱按钮拖出时Craft.js 会解析第二个参数中的 JSX通过 parseNodeFromJSX 这类内部工具将其转换为节点树并在放下位置创建新的组件实例。需要注意的是要创建可投放的容器时应像示例中的 Container 条目那样用Element is{Container} canvas /包裹目标 JSX否则拖进去的是一个普通不可投放的容器被create引用的组件同样必须在resolver中注册否则无法正确反序列化。Settings Panel选中节点与相关组件渲染SettingsPanel.js 通过useEditor的收集器从编辑器全局状态中找出当前选中节点const { actions, selected, isEnabled } useEditor((state, query) { const currentNodeId query.getEvent(selected).last(); let selected; if (currentNodeId) { selected { id: currentNodeId, name: state.nodes[currentNodeId].data.name, settings: state.nodes[currentNodeId].related state.nodes[currentNodeId].related.settings, isDeletable: query.node(currentNodeId).isDeletable(), }; } return { selected, isEnabled: state.options.enabled }; });关键实现点选中状态来自事件集合state.events.selected是一个集合Set因为 Craft.js 支持按住 meta 键多选多个 Node。示例用query.getEvent(selected).last()取最后选中的节点这里query是第二个收集器参数提供基于编辑器状态的查询 API相关组件在此异地渲染selected.settings正是Component.craft.related.settings中注册的那个组件如TextSettings。由于相关组件与用户组件共享 Node 上下文React.createElement(selected.settings)就能把属性编辑 UI 渲染到面板里而无需把编辑 UI 硬编码进用户组件内部——这正是教程中把Text组件内联设置移出画布、迁入右侧面板的实现手段删除前的可删除性检查query.node(currentNodeId).isDeletable()先判断节点是否可删除只有可删除时才渲染 Delete 按钮再调用actions.delete(selected.id)。教程特别提醒对不可删除的 Node 执行 delete 会报错所以必须先查询再操作。各用户组件的设置组件展示了不同的编辑控件范式TextSettings用 MUISlider调fontSizestep{7},min{1},max{50}ButtonSettings三组RadioGroup分别控制sizesmall / medium / large、varianttext / outlined / contained和colordefault / primary / secondaryContainerSettingsreact-colorful的HexColorPicker取背景色 Slider调 padding两者都通过setProp((props) (props.xxx value), 500)写回Card由于与 Container 属性一致直接复用ContainerSettings/ContainerDefaultProps见 Card.js 的craft定义这是组合即复用的典型示范。Topbar编辑开关、撤销重做与状态序列化Topbar.js 集成了编辑器的三个运维级能力也是 README 所指教程的收尾章节const { actions, query, enabled, canUndo, canRedo } useEditor((state, query) ({ enabled: state.options.enabled, canUndo: state.options.enabled query.history.canUndo(), canRedo: state.options.enabled query.history.canRedo(), }));编辑开关actions.setOptions((options) (options.enabled value))切换整个编辑器的可用状态。关闭后画布退出编辑模式事件处理被禁用SettingsPanel 也因isEnabled为 false 而不再渲染这是预览态 / 编辑态切换的官方做法撤销 / 重做query.history.canUndo()/canRedo()查询历史栈状态actions.history.undo()/redo()执行操作。示例用返回值控制按钮的disabled序列化与加载query.serialize()把整棵节点树序列化为 JSON示例用lzutf8压缩 copy-to-clipboard复制到剪贴板Copy current state按钮Load按钮则反向lz.decompress(lz.decodeBase64(stateToLoad))后用actions.deserialize(json)恢复状态。这一复制状态 / 粘贴恢复的闭环正是教程末尾指向的 Save and Load 指南的落地版也是页面编辑器持久化的核心链路。总结从examples/basic这个成品示例中可以提炼出一套完整、可复用的 Craft.js 页面编辑器搭建方法论分层组装Editor resolver建立上下文 →Frame托管可编辑区域 →Element canvas声明可投放的 Canvas 节点组件范式用户组件通过useNode的connect/dragconnector 接入 DOM用craft.props/related/rules声明默认属性、设置面板与拖拽规则约束机制canMoveIn/canDrag等规则让编辑器可以精确控制什么能放进哪里、什么能被拖动编辑器级能力useEditor提供全局状态、历史撤销重做、序列化 / 反序列化与启用开关这些构成了保存、加载、预览的完整闭环。整个过程中拖拽系统的底层实现由 Craft.js 承担开发者只需关注组件如何响应事件、如何被编辑——正如教程结尾所言写这些组件与写普通 React 组件并无二致。延伸阅读本示例的逐行实现教程basic-tutorial状态保存 / 加载压缩与恢复的完整指南save-loadNode 与 Canvas 的核心概念nodesEditor 与 hooks 的 API 参考Editor、useEditor、useNode、Element、Frame编辑器核心源码packages/core/src/editor、packages/core/src/nodes、packages/core/src/events赞分享前端【免费下载链接】craft.js A React Framework for building extensible drag and drop page editors项目地址https://gitcode.com/gh_mirrors/cr/craft.js点击查看免费下载相关推荐使用 Craft.js 构建可拖拽页面编辑器基础实战教程使用 Craft.js 构建可拖拽页面编辑器基础实战教程 导读 本教程将带你从零开始用 Craft.js https://link.gitcode.com/前端抖音下载器完整指南三步批量下载视频音乐效率提升90%抖音下载器完整指南三步批量下载视频音乐效率提升90% 你是否经常在抖音上看到精彩内容想要保存却无从下手面对官方限制和繁琐操作普通用户往往只能望而却步。今前端Impeccable Live 的 Monorepo 子项目定位机制一套根清单如何让你永不改错文件Impeccable Live 的 Monorepo 子项目定位机制一套根清单如何让你永不改错文件 在多仓库Monorepo里运行 ImpeccableAI 技能前端CLIdsh-plugin上一篇OBS多平台同步直播指南obs-multi-rtmp让一次编码同时推流到多个平台下一篇Mac微信防撤回插件WeChatIntercept保姆级安装教程一条命令守住撤回的消息创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考