☰
craft.js 中 `<Element />` 组件的完整指南:定义节点、构建 Canvas 与创建 Linked Nodes
2026/9/25 11:29:40 网站建设 项目流程
  • 前端

【免费下载链接】craft.js

🚀 A React Framework for building extensible drag and drop page editors

项目地址:https://gitcode.com/gh_mirrors/cr/craft.js
点击查看免费下载

<Element />是 craft.js(一个用于构建可扩展拖拽式页面编辑器的 React 框架)中定义编辑器 Node 的核心组件。本文以官方 API 文档 Element.md 为主体,深入讲解<Element />的全部 Props、在<Frame />与 User Component 中的两种使用场景、Linked Nodes 的创建机制,并结合packages/core源码揭示其底层实现原理与工程实践,帮助你用最小的成本搭建出可拖拽、可配置的页面编辑器。

<Element />是什么

在 craft.js 中,编辑器的一切内容都由Node表示:每个可拖拽、可配置的组件都对应一个 Node,Node 之间以树形结构组织。<Element />就是用来"定义某个用户元素的 Node"的组件——它本身不直接渲染业务 UI,而是把 JSX 描述转换为编辑器内部的状态树(NodeTree)。

从源码看,<Element />位于 packages/core/src/nodes/Element.tsx,其核心逻辑非常简洁:读取当前 Node 上下文后,在两种模式下工作:

  1. 当它被用作<Frame />的直接子元素时,它只是配置那个已经被 Frame 自动创建的 Node;
  2. 当它被用在 User Component 内部时,由于"原位没有 Node",它会新建一个 Linked Node——一个通过任意id与宿主 User Component 的 Node 建立关联的独立 Node。

这正是<Element />与普通 React 组件最大的区别:它是一个 Node 的声明与配置入口,而非单纯的 UI 容器。

Props 参考

<Element />接受的 Props 定义在 packages/core/src/nodes/Element.tsx 的ElementProps类型中,完整列表如下:

Prop类型说明
isReact.ElementType要渲染的用户元素类型。默认值为'div'
idString当<Element />在 User Component 内部创建时必须指定(用于创建 Linked Node)
canvasboolean若为true,将创建一个 Canvas Node(即可拖拽又可作为拖放目标)
customRecord<string, any>设置 Node 的custom属性(可被 User Component 当作附加 props 消费)
hiddenboolean设置 Node 的hidden属性;为true时隐藏该 Node
...elementPropsObject透传给is指定元素的其他 props

其中is、canvas、custom、hidden的默认值在源码中明确给出:

// packages/core/src/nodes/Element.tsx export const defaultElementProps = { is: 'div', canvas: false, custom: {}, hidden: false, };

另外,源码中还有一个易被忽略的映射关系——elementPropToNodeData,它定义了is与canvas两个 prop 与 Node 内部数据字段的对应:

// packages/core/src/nodes/Element.tsx export const elementPropToNodeData = { is: 'type', // 决定 Node 的 type canvas: 'isCanvas', // 决定 Node 是否是一个 Canvas 节点 };

也就是说,is与canvas本质上是 Node 数据(NodeData)中type与isCanvas字段的 JSX 层快捷写法。完整字段可见 packages/core/src/interfaces/nodes.ts 中的NodeData定义:props、type、name、displayName、isCanvas、parent、linkedNodes、nodes、hidden、custom等。

在<Frame />中配置 Node

<Frame />是编辑器可编辑区域的入口组件。它会对所有子元素自动创建 Node,因此当<Element />作为 Frame 的子元素出现时,它的作用只是"配置"这些将被创建的 Node 的值,而不是新建 Node。这一点在 Frame.tsx 源码中得到印证:Frame会取唯一的根子元素,通过query.parseReactElement(rootNode).toNodeTree(...)将 JSX 解析成 NodeTree,并把根节点 id 规范化为ROOT_NODE(见 constants.ts 中的ROOT_NODE = 'ROOT'),最后通过RenderRootNode渲染。

文档中的经典示例完整展示了三种节点的差异:

import {Craft, Frame, Element} from "@craftjs/core"; const App = () => { return ( <div> <h2>My App!</h2> <Craft resolver={{MyComp}}> <h2>My Page Editor</h2> <Frame> <Element is="div" canvas> // 定义根 Node,可拖放 <h2>Drag me around</h2> // type 为 h2 的 Node,可拖拽 <MyComp text="You can drag me around too" /> // type 为 MyComp 的 Node,可拖拽 <Element is="div" style={{background: "#333" }} canvas> // type 为 div 的 Canvas Node,可拖拽且可拖放 <p>Same here</p> // 不是 Node,不可拖拽 </Element> </Element> </Frame> </Craft> </div> ) }

结合上面的例子,可以总结出以下规律:

  • 作为根节点的<Element is="div" canvas>定义了整个编辑区的根 Node(ROOT),它是一个 Canvas 节点,作为拖放目标;
  • h2、MyComp等 JSX 会被自动解析为对应 type 的 Node,默认可拖拽;
  • 嵌套的<Element is="div" style={{background: "#333"}} canvas>会创建一个新的 Canvas Node——style等其余 props 会透传给内部的div元素;
  • 最内层的<p>只是普通 DOM 元素,由于它不是用户组件也没有被<Element>包裹,不会成为 Node,因此不可拖拽。

关于canvas属性的补充:旧版本中对应的<Canvas />组件已被标记为废弃。在 Canvas.tsx 中可以看到,它只是简单地渲染<Element {...props} canvas={true} />,并打印弃用提示(suggest<Element canvas={true} />)。因此新代码请统一使用<Element canvas />写法。

在 User Component 中定义 Linked Nodes

当<Element />用在 User Component(用户自定义组件)内部时,情况变得不同:此时"原位并没有已存在的 Node",所以<Element />必须新建一个 Linked Node。Linked Node 本质上是一个通过任意id与"包含它的 User Component 的 Node"关联起来的新 Node。

const Hero = () => { return ( <div> <h3>I'm a Hero</h3> <Element id="drop" is={Container} canvas> <h3>Hi</h3> </Element> </div> ) }

⚠️在 User Component 内部使用<Element />必须指定idprop。

这条约束并非口头约定,而是有运行时断言保证的:在 Element.tsx 中,Element初始化时会执行invariant(!!id, ERROR_TOP_LEVEL_ELEMENT_NO_ID),对应的错误信息定义在 constants.ts:

A <Element /> that is used inside a User Component must specify an `id` prop, eg: <Element id="text_element">...</Element>

Linked Node 的创建流程

从源码看,<Element />在 User Component 内部渲染时会走如下逻辑(Element.tsx):

  1. 通过useInternalNode()拿到当前所在 Node 的nodeId与inNodeContext;
  2. 检查宿主 Node 的data.linkedNodes[id]是否已存在且 type 与 JSX 中的is一致——若一致,则直接渲染这个已有的 Linked Node(保证幂等,避免重复创建);
  3. 否则,把Element的 JSX 通过query.parseReactElement(...).toNodeTree()解析为 NodeTree;
  4. 调用actions.history.ignore().addLinkedNodeFromTree(tree, nodeId, id),以跳过历史记录的方式(history.ignore())把新的 NodeTree 挂载到宿主 Node 的linkedNodes[id]上;
  5. 最后渲染<NodeElement id={linkedNodeId} />(见 NodeElement.tsx,它内部通过RenderNodeToElement真正输出组件)。

addLinkedNodeFromTree是 Editor 内部方法(源码中明确注明"Only used internally by the component"),定义于 actions.ts。它会先校验宿主 Node 存在,若linkedNodes[id]已有旧节点则先递归删除,再通过addNodeTreeToParent(tree, parentId, { type: 'linked', id })把新树的根节点 id 写入parent.data.linkedNodes[id](见 actions.ts)。

数据结构层面,linkedNodes是NodeData上的一个字段:linkedNodes: Record<string, NodeId>(见 nodes.ts),即以id为键、Linked Node id 为值的映射。查询侧也有配套的linkedNodes()helper(见 NodeHelpers.ts),供query.node(...)使用。

Linked Nodes 在真实示例中的用法

在 examples/basic/components/user/Card.js 中可以看到 Linked Nodes 的典型组合用法——用两个带id的 Canvas Element 把 Card 组件拆成"上区只允许文本、下区只允许按钮"的两个可拖放区域:

export const Card = ({ background, padding = 20, ...props }) => { return ( <Container {...props} background={background} padding={padding}> <Element canvas id="text" is={CardTop}>const Hero = () => { const { css } = useNode(node => ({ css: node.data.custom.css })); return ( <div style={css}> <h3>I'm a Hero</h3> <Element id="drop" is={Container} canvas> <h3>Hi</h3> </Element> </div> ) } Hero.craft = { custom: { css: { background: "#eee" } } }

关键点在于useNode的收集函数:node.data.custom.css正是从 Node 数据中读取custom字段。custom在UserComponentConfig中被定义为Record<string, any>(见 nodes.ts),因此它完全由你自由定义结构。

通过<Element />覆盖运行时值

若想在调用组件时实际设置这些值,通过<Element />的customprop 即可:

<Frame> <Element is={Hero} custom={{ css: { background: "#ddd" } }} /> </Frame>

这样,Hero 组件渲染时useNode取到的css将是{ background: "#ddd" },覆盖了craft.custom中定义的默认值{ background: "#eee" }。这一机制让编辑器外部也能以声明式方式配置组件数据,与运行时的属性面板(Settings Panel)读写形成互补。

补充:除了读取,你还可以在组件内部通过useNode返回的actions.setCustom(cb)来更新Node 的custom数据(见 useInternalNode.ts),它同样支持可选的throttleRate节流参数,便于拖拽等高频交互场景下控制更新频率。

隐藏节点:hidden属性

<Element />的hiddenprop 对应 Node 数据中的hidden字段(NodeData.hidden: boolean)。当它为true时,该 Node 会被隐藏。

隐藏的生效位置在渲染层:RenderNodeToElement渲染前会通过useInternalNode((node) => ({ hidden: node.data.hidden }))收集hidden状态,若为true则直接返回null,即不渲染任何 DOM(见 RenderNode.tsx)。

这与"组件不存在"的区别在于:Node 及其子节点仍然保留在编辑器状态树中,只是不显示。该字段也可通过useNode提供的actions.setHidden(bool)在运行时切换(见 useInternalNode.ts),适合实现"显示/隐藏某个区块"这类编辑功能。

底层原理:Element 如何被解析为 Node

要真正理解<Element />,还需要知道它背后的解析链路。<Frame />与<Element />内部的 Linked Node 创建,最终都会调用query.parseReactElement(jsx).toNodeTree(...):

  • parseNodeFromJSX(parseNodeFromJSX.tsx)负责把 React 元素(或字符串)转换为单个 Node:它提取element.type作为 Node 的type,把element.props展开后作为 Node 的props;字符串会被包装为<Fragment>元素再处理;
  • toNodeTree在此基础上递归遍历子元素,将整棵 JSX 树转换为NodeTree({ rootNodeId, nodes }结构,见 nodes.ts);
  • 转换完成后,Node 会被写入编辑器状态(state.nodes[id]),并维护parent、nodes、linkedNodes等树形关系(见 actions.ts 中的addNodeTreeToParent迭代逻辑)。

由于解析时需要根据type查找组件,所有在 JSX 中使用到的用户组件都必须注册进<Craft resolver={{...}}>(示例中的resolver={{MyComp}}即为此用途);否则会触发ERROR_NOT_IN_RESOLVER错误(见 constants.ts)。

实践小结

围绕<Element />的几个关键结论:

  1. 配置节点:作为<Frame />子元素时,<Element is={Comp} canvas custom={{...}} hidden />用于配置被自动创建的 Node;
  2. 创建 Linked Node:在 User Component 内部使用<Element id="xxx" ...>会创建与宿主组件关联的独立 Node,id必填,否则抛ERROR_TOP_LEVEL_ELEMENT_NO_ID;
  3. Canvas Node:canvas决定节点是否同时作为拖放目标(droppable),对应 Node 数据的isCanvas字段;旧<Canvas />组件已废弃,请使用<Element canvas />;
  4. 自定义数据:custom提供 Node 级自定义数据存储,配合craft.custom默认值与useNode的node.data.custom读取、setCustom更新,形成完整闭环;
  5. 隐藏节点:hidden让 Node 保留在状态树中但跳过渲染,可通过setHidden动态切换。

掌握了这些之后,无论是搭建从零开始的页面编辑器,还是为既有复合组件划分多个可编辑子区域,<Element />都是你手中最重要的声明式工具。结合本文提到的 Element.tsx、Frame.tsx、actions.ts、interfaces/nodes.ts 等源码继续阅读,可以进一步深入 craft.js 的节点模型与状态管理设计。

  • 前端

【免费下载链接】craft.js

🚀 A React Framework for building extensible drag and drop page editors

项目地址:https://gitcode.com/gh_mirrors/cr/craft.js
点击查看免费下载
上一篇:XHS-Downloader终极指南:三步搞定小红书无水印批量下载的完整解决方案
下一篇:小红书无水印下载神器:5分钟快速上手XHS-Downloader终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询