- 前端
【免费下载链接】craft.js
🚀 A React Framework for building extensible drag and drop page editors
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、Topbar;examples/basic/components/user/:四个可编辑的"用户组件"(Button、Text、Container、Card);examples/basic/components/:三个编辑器外壳组件(Toolbox、SettingsPanel、Topbar);examples/basic/package.json、examples/basic/next.config.js:基于 Next.js 15 + React 19 + MUI v6 的工程配置。
该示例同时提供在线演示环境(见 README 的 live demo 说明),读者可以边看效果边对照源码。其覆盖的功能点与教程章节一一对应:
| 功能 | 关键文件 | 对应教程小节 |
|---|---|---|
| 用户组件(User Components) | components/user/Text.js、Button.js、Container.js、Card.js | User Components |
| 可拖放区域与规则 | Card.js 中的 CardTop / CardBottom | Defining Droppable regions |
| 从工具箱拖拽新建 | components/Toolbox.js | Implementing the Toolbox |
| 属性编辑面板 | components/SettingsPanel.js | Settings Panel |
| 开关 / 撤销重做 / 序列化 | components/Topbar.js | Topbar |
快速运行 examples/basic
README 给出的启动方式是标准的 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/core(workspace:*):编辑器核心,通过 monorepo 工作区直接引用本仓库 packages/core 的源码;react-contenteditable:实现 Text 组件的行内内容编辑;react-colorful:提供 Container / Card 背景色的十六进制取色器;copy-to-clipboard与lzutf8:Topbar 中"复制 / 加载编辑器状态"所需的剪贴板与压缩/解压能力。
示例的总体架构: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) } tagName="p" style={{ fontSize: `${fontSize}px`, textAlign }} /> </div> ); };关键点:
useNode收集器:useNode((state) => ({...}))接收一个函数,从 Node 状态中订阅events.selected(是否被选中)和events.dragged(是否被拖动),从而驱动组件响应编辑器事件;- 行内编辑:
react-contenteditable实现所见即所得的文本编辑。编辑只在组件已被选中后再次点击才开启(双击语义),取消选中则自动退出编辑态;onChange里用正则剔除粘贴进来的 HTML 标签,并把结果写回 props; setProp的第二个参数:示例传入500或1000(见 Button / Container 中的用法),用于控制高频更新场景(如拖拽滑块)下的更新频率,具体接口定义可参考 useNode。
Button:Material 组件 + 属性配置
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,下半区只允许放入 Button:
export const CardTop = ({ children, ...props }) => { const { connectors: { connect } } = useNode(); return ( <div {...props} ref={connect} className="text-only" style={{ /* ... */ }}> {children} </div> ); }; CardTop.craft = { rules: { canMoveIn: (incomingNodes) => incomingNodes.every((incomingNode) => incomingNode.data.type === Text), }, };这里有两个实战要点:
- User Component 内部也可以创建 Canvas 节点:Card 内部的两个区域同样用
<Element canvas id="text" 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——useEditor:
export const Toolbox = () => { const { connectors } = useEditor(); return ( <MaterialButton ref={(ref) => connectors.create(ref, <Button text="Click me" size="small" />)} variant="contained" > 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调fontSize(step={7},min={1},max={50});ButtonSettings:三组RadioGroup分别控制size(small / medium / large)、variant(text / outlined / contained)和color(default / primary / secondary);ContainerSettings:react-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-load
- Node 与 Canvas 的核心概念:nodes
- Editor 与 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
相关推荐
使用 Craft.js 构建可拖拽页面编辑器:基础实战教程
使用 Craft.js 构建可拖拽页面编辑器:基础实战教程 导读 本教程将带你从零开始,用 Craft.js https://link.gitcode.com/
前端抖音下载器完整指南:三步批量下载视频音乐,效率提升90%
抖音下载器完整指南:三步批量下载视频音乐,效率提升90% 你是否经常在抖音上看到精彩内容想要保存却无从下手?面对官方限制和繁琐操作,普通用户往往只能望而却步。今
前端Impeccable Live 的 Monorepo 子项目定位机制:一套根清单如何让你永不改错文件
Impeccable Live 的 Monorepo 子项目定位机制:一套根清单如何让你永不改错文件 在多仓库(Monorepo)里运行 Impeccable
AI 技能前端CLIdsh-plugin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考