首页/新闻资讯/正文详情

用 Craft.js 构建可扩展的 React 拖拽式页面编辑器:核心机制与源码级实战指南

发布时间:2026/9/25 9:01:37 来源:云帆数科 栏目:资讯中心
用 Craft.js 构建可扩展的 React 拖拽式页面编辑器:核心机制与源码级实战指南
前端【免费下载链接】craft.js A React Framework for building extensible drag and drop page editors项目地址https://gitcode.com/gh_mirrors/cr/craft.js点击查看免费下载Craft.js 是一个以 React 为基础的拖拽页面编辑器框架它把页面编辑器拆解为拖放系统 节点渲染/更新/移动机制让你完全按照自己的 UI 规范去搭建编辑器本身而不是被迫使用一套开箱即用、难以定制的现成编辑器。本文以仓库根目录 README.md 为主线结合 packages/core 的源码实现与 examples/basic 真实示例完整讲解 Editor / Frame / Element / useNode / useEditor 等核心 API 的用法、底层原理与可序列化状态机制读完你即可动手构建自己的 React 页面编辑器。1. 背景为什么页面编辑器需要一套模块化抽象页面编辑器Page Editor是提升用户体验的绝佳手段但亲手构建一个往往令人头疼。市面上已有一些自带完整用户界面和可编辑组件的编辑器库可一旦你想定制用户界面或改变其行为几乎必然要直接修改库本身。Craft.js 的解法是模块化modularising页面编辑器的构建模块它内置拖放系统drag-n-drop system并负责处理用户组件应当如何被渲染、更新和移动。基于它你可以构建出外观和交互完全由你定义的页面编辑器。这套思路的核心设计原则只有一条——Its just React它只是 React不需要复杂的插件系统编辑器的一切都可以像普通 React 前端应用一样从零设计编辑器自身的整个 UI工具面板、属性设置面板、顶栏等就是用 React 组件拼出来的你可以直接复用 React 生态中成千上万的现成组件。仓库中的两个示例恰好说明了这种灵活性examples/landing带 Tailwind 的现代风格编辑器与 examples/basic基于 Material UI 的经典三栏编辑器外观截然不同但它们都基于同一套 Craft.js 核心构建。相关概念与教程见仓库内文档核心概念、基础教程、API 参考。2. 快速上手用三段代码搭起编辑器骨架2.1 定义用户组件useNode 与连接器connectors一个最简单的用户组件只需要从craftjs/core引入useNode拿到connectors.connect与connectors.drag然后把二者绑定到 DOM 上import { useNode } from craftjs/core; const TextComponent ({ text }) { const { connectors: { connect, drag }, } useNode(); return ( div ref{(ref) connect(drag(ref))} h2{text}/h2 /div ); };这里connect让该 DOM 成为编辑器中的一个节点可被选中、显示指示器drag则让该 DOM 成为拖拽手柄。在源码 useInternalNode.ts 中可以看到这两个连接器通过wrapConnectorHooks绑定到具体的节点id上最终调用的是编辑器层的editorConnectors.connect(dom, id)/editorConnectors.drag(dom, id)即把组件自身与编辑器节点树中的某个节点建立关联。2.2 组装编辑器Editor Frame Element整个编辑器的 UI哪怕是顶栏都只是普通 React 组件可编辑区域由Editor与Frame圈定import React from react; import { Editor, Frame, Element } from craftjs/core; const App () { return ( div headerSome fancy header or whatever/header Editor {/* Editable area starts here */} Frame resolver{{ TextComponent, Container }} Element canvas is{TextComponent} textIm already rendered here / /Frame /Editor /div ); };resolver是把序列化后的组件名字映射回真实组件的注册表见 interfaces/editor.ts 中Resolver Recordstring, string | React.ElementTypeFrame内声明的 JSX 会被解析成一棵节点树渲染出来。仓库中的真实示例 examples/basic/pages/index.js 展示了更完整的骨架Editor resolver{...}里并排放置了顶栏Topbar、可编辑区Frame、左侧Toolbox组件库与右侧SettingsPanel属性面板其中根容器用Element canvas is{Container} ...声明为可放置区域并预置了Card、Button、Text等初始内容。3. 三大核心组件Editor、Frame、Element3.1 Editor编辑器上下文的提供者Editor是一个向子树提供编辑器上下文的 React 组件源码见 Editor.tsxexport const Editor ({ children, ...options }: EditorProps) { // 校验 resolver 必须是普通对象 if (options.resolver ! undefined) { invariant( typeof options.resolver object !Array.isArray(options.resolver) options.resolver ! null, ERROR_RESOLVER_NOT_AN_OBJECT ); } // ... 通过 useEditorStore 创建 store注入 EditorContext return ( EditorContext.Provider value{context} Events{children}/Events /EditorContext.Provider ); };从源码可知两件值得注意的事resolver 校验如果你传了resolver它必须是普通对象否则直接抛错——这保证了后续按名字查找组件的可靠性onNodesChange 订阅Editor内部订阅了序列化状态每当节点变化就调用context.query.getOptions().onNodesChange(query)Editor.tsx这是实现自动保存草稿的天然钩子normalizeNodes当发生setState、deserialize或任何node.data的修改时会调用你传入的normalizeNodes(draft, previousState, actionPerformed, query)用于在每次变更后规整节点树。Editor接受的完整Options定义在 interfaces/editor.ts默认值则集中在 store.tsx配置项类型默认值作用onRenderReact.ComponentType{ render }({ render }) render自定义节点渲染包装常用于给每个节点包裹高亮/选中框onBeforeMoveEnd(targetNode, newParent, existingParent) void() null节点移动落地前的回调可在此修改节点 propsonNodesChange(query) void() null节点变更回调配合序列化可实现自动保存resolverRecordstring, React.ElementType{}组件名字 → 组件映射表序列化/反序列化必需enabledbooleantrue是否启用编辑拖拽/选中能力indicator{ success, error, transition, thickness, className, style }{ error: red, success: rgb(98, 196, 98) }拖放指示器颜色与样式handlers(store) CoreEventHandlersnew DefaultEventHandlers({...})自定义事件处理器默认支持metaKey多选normalizeNodes(state, prevState, action, query) void() {}每次状态变更后的节点规整函数注意enabled是响应式同步的Editor中有专门的 effect 把options.enabled同步进内部 storeEditor.tsx。3.2 Frame定义可编辑区域并加载初始状态Frame是可编辑区域的边界组件源码见 Frame.tsx。它只做一次性的初始化内部用isLoaded.current防止重复加载有data/json时调用actions.history.ignore().deserialize(initialData)从序列化数据恢复整棵节点树没有时把唯一的 JSX 子元素交给query.parseReactElement(rootNode).toNodeTree(...)解析成节点树并将根节点 id 设为ROOT_NODE再addNodeTree写入状态。const Frame ({ children, json, data }: FrameProps) { const { actions, query } useInternalEditor(); if (!!json) { deprecationWarning(Frame json{...} /, { suggest: Frame data{...} /, }); } // ... };一个需要留意的迁移点Frame json{...}已废弃官方建议改用Frame data{...}。FrameProps支持children、json?: string、data?: string | SerializedNodes见 Frame.tsx其中data既可以是 JSON 字符串也可以是已反序列化的对象。3.3 Element声明式节点与可放置区域Element用于在 JSX 中声明一个节点。它的默认属性与映射规则定义在 Element.tsxexport const defaultElementProps { is: div, canvas: false, custom: {}, hidden: false, }; export const elementPropToNodeData { is: type, canvas: isCanvas, };即is决定节点的组件类型默认divcanvas决定该节点是否是可放置区域存入isCanvashidden决定是否隐藏custom存放任意自定义数据。Element还支持id指定固定节点 id——当Element处于某个节点上下文内部时它会通过linkedNodes机制挂载/复用同名链接节点Element.tsx这一机制也是后面容器组件内嵌可放置区域的底层支撑。迁移提示早期的Canvas /组件已废弃其实现Canvas.tsx只是包了一层Elementexport function CanvasT extends React.ElementType({ ...props }: CanvasPropsT) { useEffect(() deprecateCanvasComponent(), []); return Element {...props} canvas{true} /; }deprecateCanvasComponent会提示改用Element canvas{true} /。本文后续示例统一使用新的canvas写法但 README 中的旧Canvas写法在语义上等价。4. 控制组件的编辑方式useNode 与自定义编辑交互页面编辑器必然要允许用户编辑组件而 Craft.js 把如何编辑的控制权完全交给你。README 中的经典例子是点击组件后弹出 Modal通过输入框实时修改textpropimport { useNode } from craftjs/core; const TextComponent ({ text }) { const { connectors: { connect, drag }, isClicked, actions: { setProp }, } useNode((state) ({ isClicked: state.events.selected, })); return ( div ref{(dom) connect(drag(dom))} h2{text}/h2 {isClicked ? ( Modal input typetext value{text} onChange{(e) setProp(e.target.value)} / /Modal ) : null} /div ); };这里有三个关键点逐一对应源码collector 函数useNode((state) ({...}))是一个选择器只订阅你关心的节点状态这里是events.selected即是否被选中。useNode内部由 useInternalNode.ts 实现通过useInternalEditor收集对应节点的状态切片任何未订阅的字段变化都不会触发重渲染actions.setPropuseNode()返回的actions.setProp(cb, throttleRate?)会定向更新当前节点的data.propsuseInternalNode.ts。传入throttleRate时走history.throttle(rate).setProp(id, cb)把短时间内的多次变更合并进同一条历史记录避免撤销/重做被输入过程刷屏注意README 示例中setProp(e.target.value)直接传值而新版 API 推荐用回调形式setProp((props) (props.text value))setProp底层的实现即cb(state.nodes[id].data.props)见 actions.ts。同时useNode().setProp()这个旧入口本身也已废弃统一走actions.setProp()useNode.ts。借助同样的模式你很容易实现内容可编辑文本content editable或拖拽调整尺寸等现代页面编辑器能力。仓库示例 examples/basic/components/user/Text.js 就是一个完整范例选中后点击进入编辑态用react-contenteditable实时改文本并通过setProp(..., 500)做 500ms 节流const { connectors: { connect, drag }, selected, actions: { setProp }, } useNode((state) ({ selected: state.events.selected, dragged: state.events.dragged, })); // ... ContentEditable html{text} disabled{!editable} onChange{(e) setProp((props) (props.text e.target.value.replace(/\/?[^](|$)/g, )), 500) } /每个用户组件还可以通过静态的craft字段声明默认 props 与关联的设置面板组件Text.craft { props: TextDefaultProps, // { text: Hi, fontSize: 20 } related: { settings: TextSettings, // 供 SettingsPanel 渲染的属性编辑器 }, };5. 可拖拽区域构建支持嵌套拖放的容器组件假设我们要做一个 Container 组件它既能被拖进编辑器又能接收其他组件拖放进来。在 Craft.js 中只需要在组件内部放置一个canvas节点。README 的原始写法如下import { useNode } from craftjs/core; const Container () { const { connectors: { drag } } useNode(); return ( div ref{drag} Canvas iddrop_section {/* Now users will be able to drag/drop components into this section */} TextComponent / /Canvas /div ); };按当前版本应改写为Element canvas iddrop_section.../ElementCanvas只是它的别名见上文 3.3。isCanvas: true会让该节点成为可放置区域——Craft.js 的拖放判定droppable 判定、落点指示器计算都会围绕isCanvas节点展开例如查询 API 中的query.node(id).isCanvas()、移动校验isDroppable()等NodeHelpers.ts。真实的容器组件见 examples/basic/components/user/Container.jsContainer把connect与drag都绑定在根元素上children直接透传这样当它被标记为canvas节点时子节点就能被自由拖入、排序和移出export const Container ({ background, padding, children, ...props }) { const { connectors: { connect, drag } } useNode(); return ( Paper {...props} ref{(ref) connect(drag(ref))} style{{ margin: 5px 0, background, padding: ${padding}px }} {children} /Paper ); }; Container.craft { props: ContainerDefaultProps, // { background: #ffffff, padding: 3 } related: { settings: ContainerSettings }, };在 examples/basic/pages/index.js 中这个 Container 被用作根画布并嵌套了第二个Element canvas is{Container}验证了画布嵌套画布的能力。6. 可扩展的编辑器 APIuseEditor 读取与操作全局状态Craft.js 提供了一套表达能力很强的 API方便你直接读取、操作整棵节点树。README 中实现复制组件功能的例子如下import { useEditor, useNode } from craftjs/core; const Container () { const { actions: { add }, query: { createNode, node } } useEditor(); const { id, connectors: { drag, connect } } useNode(); return ( div ref{dom connect(drag(dom))} ... a onClick{() { const { data: { type, props } } node(id).get(); add( createNode(React.createElement(type, props)) ); }} Make a copy of me /a /div ); };这段代码背后对应的源码级事实useEditor()返回整个编辑器的actions、query、connectors与store并会自动过滤掉addLinkedNodeFromTree、setDOM、setNodeEvent、replaceNodes、reset等内部私有 actionuseEditor.tsxnode(id).get()返回该节点的完整数据对象含data.type、data.props等见 NodeHelpers.ts迁移提示query.createNode(reactElement)已废弃官方建议改用query.parseReactElement(reactElement).toNodeTree()query.tsx后者会从 JSX 解析出一整棵子树并完成组件名解析resolveComponentmergeTrees。6.1 actions可用的编辑器操作全集从 actions.ts 可以梳理出完整的操作集合action签名说明add(node, parentId?, index?)把单个节点加入指定父节点已废弃数组形式addNodeTree(tree, parentId?, index?)把一整棵节点树加入编辑器delete(selector)删除节点顶层/链接节点不可删会抛ERROR_DELETE_TOP_LEVEL_NODEmove(selector, newParentId, index)移动节点到新父节点的指定下标移动前会调用onBeforeMoveEnd并校验可放置性selectNode(selector?)设置选中事件传空则取消选中setOptions(cb)通过回调修改编辑器 optionssetProp(selector, cb)更新节点 props回调接收当前data.propssetCustom(selector, cb)更新节点data.custom自定义数据setHidden(id, bool)隐藏/显示节点setIndicator(indicator \| null)设置拖放指示器deserialize(input)从SerializedNodes或 JSON 字符串重建节点树replaceNodes/reset(nodes)/()整体替换节点树 / 重置所有状态内部方法通常经history.ignore()调用6.2 query可用的查询方法query提供了只读/派生信息query.tsxquery.serialize()导出节点树的 JSON 字符串query.getSerializedNodes()导出SerializedNodes对象query.node(id)返回某个节点的帮助器集合query.parseReactElement(jsx).toNodeTree()把 JSX 解析为节点树query.parseSerializedNode(json).toNode()把单个序列化节点解析为节点query.getDropPlaceholder(...)根据鼠标位置计算最佳落点与指示器query.getOptions()/query.getNodes()/query.getEvent(type)读取 options、全部节点、事件集合。query.node(id)返回的帮助器NodeHelpers.ts包括isCanvas()、isRoot()、isLinkedNode()、isDeletable()、isSelected()、isHovered()、isDragged()、ancestors(deep)、descendants(deep, includeOnly?)、linkedNodes()、childNodes()、isDraggable(onError)、isDroppable(...)等足以支撑图层面板、面包屑导航等高级 UI。7. 可序列化的状态保存与恢复完整编辑器编辑器的状态可以序列化成 JSON配合你选择的压缩方案存储。保存侧READMEconst SaveButton () { const { query } useEditor(); return a onClick{() console.log(query.serialize())}Get JSON/a; };加载侧READMEconst App () { const jsonString /* retrieve JSON from server */; return ( Editor Frame json{jsonString} ... /Frame /Editor ); };同样地Frame json{...}建议换成Frame data{...}。序列化细节可以从 serializeNode.tsx 中确认组件类型会被降级为{ resolvedName: 组件名 }字符串形式而不是函数引用——这正是resolver存在的原因undefined、null、函数类型的 props 会被剔除保证输出是纯 JSONserializeNode.tsx非字符串的children会被递归序列化成嵌套组件结构serializeNode.tsx。反序列化则由actions.deserialize完成JSON.parse字符串 → 兼容旧版DEPRECATED_ROOT_NODE→ 逐节点parseSerializedNode(...).toNode(...)→replaceNodes整体替换actions.ts。完整的前后端对接方案含存储建议可参考仓库文档 保存与加载指南。8. 内置历史记录撤销 / 重做与节流合并编辑器状态的管理依赖 packages/utils/src/History.ts 中基于 Immer patch 实现的时间线add(patches, inversePatches)追加一条历史同时截断未来分支throttleAdd(patches, inversePatches, throttleRate 500)默认 500ms 节流窗口内连续变更合并为同一条历史防止滑块拖动、输入框打字产生大量历史条目History.tsHISTORY_ACTIONS包含UNDO、REDO、THROTTLE、IGNORE、MERGE、CLEAR。在核心包中初始化、序列化恢复等非用户操作通过actions.history.ignore()包裹不进入历史栈如Frame的deserialize高频编辑如 Text/Container 示例中的滑块与输入通过setProp(cb, 500)走history.throttle(500)见 useInternalNode.ts一部分内部 actionsetDOM、setNodeEvent、selectNode、clearEvents、setOptions、setIndicator被显式列入ignoreHistoryForActions不产生历史记录store.tsx。useEditor()暴露的actions.history会过滤掉私有 action只保留undo()、redo()、ignore()、throttle()、merge()、clear()等公共能力useEditor.tsx顶栏的撤销/重做按钮参见 examples/basic/components/Topbar.js正是基于此实现。9. 适用人群什么时候该用什么时候不该用README 明确给出了边界应当使用 Craft.js如果你想按自己的 UI 规范设计页面编辑器——Craft.js 让你控制编辑器外观与交互的几乎每个方面你喜爱 React 生态——作为 React 框架你不仅能声明式地构建 UI还能在页面编辑器中复用成千上万的现成 React 组件。不应当使用 Craft.js如果你需要一个开箱即用的页面编辑器。Craft.js 是一层抽象编辑器 UI 需要你自己实现它本身不附带现成界面。这种情况下可以把 examples/basic 或 examples/landing 作为起点改造。10. 生态与设计来源craftjs/layers官方配套的 Photoshop 风格图层面板包源码与用法见 packages/layers/README.md其内部同样基于useEditor/useNode的查询 API 实现节点树的层级展示、显隐切换与重命名。设计灵感与借鉴Craft.js 的许多方面以react-dnd为参考并借用了部分工具函数但并未实际使用它元素定位逻辑借鉴自Grape.jsHTML 网页构建器use-methods这个 reducer 辅助 hook 也被 Craft.js 以修改版集成用于驱动编辑器 store 的状态更新。整体而言Craft.js 的价值在于把编辑器的能力抽象成可组合的 React 原语Editor提供状态容器Frame圈定可编辑区域Element声明节点与放置区域useNode让组件感知自身节点useEditor让你从任意位置操控全局而序列化与历史记录机制则为生产级落地保存、恢复、撤销铺平了道路。以此为基础你可以从零打造一个外观与交互完全属于你自己的拖拽式页面编辑器。赞分享前端【免费下载链接】craft.js A React Framework for building extensible drag and drop page editors项目地址https://gitcode.com/gh_mirrors/cr/craft.js点击查看免费下载相关推荐Craft.js 核心包实战指南用 React 构建可扩展的拖拽式页面编辑器Craft.js 核心包实战指南用 React 构建可扩展的拖拽式页面编辑器 Craft.js 是一个基于 React 的页面编辑器框架它把页面编辑器的构建前端Craft.js 概览用纯 React 构建可扩展的拖拽式页面编辑器Craft.js 概览用纯 React 构建可扩展的拖拽式页面编辑器 页面编辑器是提升产品用户体验的利器但自建一套往往工程量浩大。本文基于 Craft.js前端如何快速上手PointWorld500小时3D交互数据训练的机器人预测模型入门指南如何快速上手PointWorld500小时3D交互数据训练的机器人预测模型入门指南 想要掌握基于500小时3D交互数据训练的机器人预测模型吗PointWor上一篇用 Elm 实现 TodoMVC深入函数式架构、端口通信与本地持久化下一篇终极指南如何用UnrealPakViewer快速解决虚幻引擎Pak文件分析难题创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

Keil5安装STM32芯片支持包全攻略:在线与离线安装及常见问题
Keil5安装STM32芯片支持包全攻略:在线与离线安装及常见问题

1. 为什么STM32开发绕不开芯片支持包刚拿到Keil5的安装包,兴冲冲装完打开,新建工程时却发现器件列表里只有ARM自家的几个内核,连STM32F103都找不到——这个场景几乎每个STM32新手都经历过。问题不在Keil5本身,而在于它只是一个通用… · 2026/9/25 9:01:19

Mycat 1.6.7.1部署实战:分库分表与读写分离配置指南
Mycat 1.6.7.1部署实战:分库分表与读写分离配置指南

简介:这是 Mycat 1.6.7.1 在 CentOS7 下的 Linux 发行压缩包,面向需要搭建分布式数据库中间件的中高级开发与运维人员,用于解决大规模数据存储时的分库分表、读写分离与水平扩展问题。包体共95个文件,整体16.74MB,以42… · 2026/9/25 9:01:19

π型衰减器设计与工程实现:从阻抗匹配到高频PCB布局的完整指南
π型衰减器设计与工程实现:从阻抗匹配到高频PCB布局的完整指南

第一次听射频师傅说要给频谱仪前面串一个衰减器,我是有点抗拒的。当时实验室里所有人都在想方设法把线缆和接头插损做小,怎么轮到测试链路就得特意往里面塞一个"吸信号"的器件?后来一只功率计在过载的一瞬间给出一条歪掉的曲线&… · 2026/9/25 9:01:19

程序员用AI写AI代码:TaoToken统一Key接入Copilot的settings.json配置与验证
程序员用AI写AI代码:TaoToken统一Key接入Copilot的settings.json配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 9:43:30

网络安全应急演练实战:从ATTCK场景设计到自动化处置剧本
网络安全应急演练实战:从ATTCK场景设计到自动化处置剧本

简介:这份文档资料面向政府机构、企事业单位的安全管理人员及专业应急处理人员,系统讲解网络安全应急响应预案的培训与演练方法,帮助组织在遭遇网络攻击、数据泄露等突发事件时做到临危不乱、快速处置。内容围绕演练目的、预案培训、实战演练… · 2026/9/25 9:43:24

系统安全与网络安全:双线防御的落地实践与衔接技巧
系统安全与网络安全:双线防御的落地实践与衔接技巧

简介:《计算机系统安全与计算机网络安全》是一份PDF格式的学习参考资料,定位面向计算机专业学生、网络管理员及网络安全入门者,用于建立计算机系统安全与网络安全的基础知识框架。资源包仅包含1个PDF文件,大小约1.07MB&#xff0c… · 2026/9/25 9:43:24

红蜘蛛管控系统深度卸载与网络无感禁用指南
红蜘蛛管控系统深度卸载与网络无感禁用指南

1. 红蜘蛛不是“普通软件”,而是一套深度驻留的教室管控系统很多人第一次面对红蜘蛛(3000soft Red Spider)时,下意识把它当成一个双击就能关掉的普通教学软件——点右上角、任务栏右键退出、甚至进任务管理器结束进程,… · 2026/9/25 9:43:24

CTMS系统架构设计:从状态机到合规审计的落地指南
CTMS系统架构设计:从状态机到合规审计的落地指南

简介:CTMS 系统架构说明是一份面向客户与开发者的技术文档,旨在解决 CTMS 系统部署前的容量规划、性能评估与数据安全等关键问题。内容覆盖系统架构(一般型与扩充型)与软件架构分层,说明两种架构的适用场景——一般型适… · 2026/9/25 9:43:24

别把 CTF-All-In-One 当书读:它的正确用法是索引加训练闭环
别把 CTF-All-In-One 当书读:它的正确用法是索引加训练闭环

简介:《ctf-all-in-one.pdf》是一份面向CTF与网络安全学习者的系统化知识手册,覆盖从基础入门到高阶实战的完整链条。文档从CTF赛事形式与规则讲起,依次深入Linux/Web基础、逆向工程、密码学和Android安全等知识模块,并重点拆解Pw… · 2026/9/25 9:43:18

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码