ByteNoteByteNote
react-rnd 实战:给编辑器加一根可拖拽的分栏线
字

字节笔记本

2026年10月7日 · 约 9 分钟读完

react-rnd 实战:给编辑器加一根可拖拽的分栏线

API中转
¥120

打开任何一个在线开发工具——CodeSandbox、各类 AI 编程助手的工作台、组件文档站的 Playground——几乎都是同一副骨架:左边写代码,右边看效果,中间一根分栏线,拖着就能重新分配两侧空间。这个交互用户早已习以为常,轮到自己实现才发现细节不少:分栏线要跟手、宽度要进状态、右侧面板要实时联动,还得和全屏、收起这些既有功能和平共处。最近给一个 HTML/Tailwind 实时预览编辑器加这根线时,我用了 react-rnd,本文把选型考量、集成写法和踩坑细节一次讲清。

一、需求与选型:一根线,四种画法

场景很典型:编辑器用 CodeMirror(@uiw/react-codemirror 封装,配 HTML 语法扩展与 VS Code 暗色主题),行号、代码折叠、括号匹配、自动补全等能力由 basicSetup 一并打开;预览是一个 iframe,把代码包进完整 HTML 文档写入,并引入 Tailwind CDN,代码一变即重写文档。现在只差一件事:拖动两栏之间的分隔线,自由调整编辑区与预览区的宽度比例。

动手前先把可选方案过一遍:

  • 原生 CSS 的 resize 属性:一行样式就能让元素可缩放,但手柄固定在右下角,无法带动相邻面板联动,交互观感粗糙,只适合快速演示。
  • react-split-pane:专做分割面板的老牌库,API 简洁,上手最快,但定制空间有限,想对齐自己的设计语言比较费劲。
  • allotment:微软开源的现代分割面板库,支持嵌套分割与多方向布局,功能最全,引入的概念也更多。
  • react-rnd:把 react-draggable 与 react-resizable 的能力合进一个组件,既能拖动又能缩放,自由度最高。

最后选了 react-rnd,理由有二:一是项目后续还打算做可拖拽的浮层面板,统一依赖能少一份心智负担;二是本次需求其实只用得上它一半的能力——缩放,拖动可以直接关掉。而「只用一半也顺手」恰恰说明它的 API 粒度足够细。反过来,如果产品只永远需要分栏、不需要任何自由布局,allotment 这类专职库语义更贴切,不必为了一个 props 多背一套拖拽能力。

二、只缩放、不拖拽:Rnd 的分栏姿势

分栏和浮层是两种布局诉求:浮层面板要能拖到任意位置,分栏线只需要沿一个方向缩放。react-rnd 对此给出的开关相当明确,核心配置一共四处:

jsx
const [editorSize, setEditorSize] = useState({
  width: '50%',
  height: '100%',
});

<Rnd
  size={{ width: editorSize.width, height: editorSize.height }}
  onResizeStop={(e, direction, ref, delta, position) => {
    setEditorSize({
      width: ref.style.width,
      height: ref.style.height,
    });
  }}
  enableResizing={{ right: true }}
  disableDragging={true}
  className="absolute left-0 top-0"
>
  <CodeMirror value={htmlCode} height="100%" />
</Rnd>

四个关键点:

  1. 受控尺寸。size 绑定 editorSize 状态;onResizeStop 里从 ref.style 读回拖拽后的实际宽高写回 state。注意是在停手时落账,而不是在 onResize 过程中高频 setState,渲染压力小得多。
  2. 单向缩放。enableResizing 只开 right,其余七个方向的手柄全部隐藏,用户只能拉右边缘,「分栏线」的语义由此确立;disableDragging 关掉整体拖动,编辑器不会被拖走。
  3. 绝对定位联动。预览层同样绝对定位,left 直接取 editorSize.width:
jsx
<div
  className="absolute right-0 top-0 h-full"
  style={{ left: editorSize.width }}
>
  <iframe ref={iframeRef} className="w-full h-full" />
</div>

编辑器拖多宽,预览区的起点就跟进多远,右侧剩余空间自动归预览所有,整个联动只有一行样式。

  1. 父容器给参照。两个子层都是绝对定位,外层容器必须 relative 且有确定高度(用 flex-1 撑满剩余视口),否则百分比尺寸会塌陷。

react-rnd 分栏布局:一个状态驱动两个面板

这背后值得多说一句停手才落账的原理。react-rnd 底层是 react-draggable 与 react-resizable 的合成,拖拽缩放过程中它直接操作 DOM 样式,不经过 React 的渲染管线,所以过程跟手流畅;onResizeStop 回调拿到的是真实 DOM 节点的 ref,从它的 style 里读出的就是最终值。这也解释了为什么不该在 onResize 里每帧 setState——那等于把流畅的命令式过程重新拖回 React 重渲染的慢车道。若确有实时联动预览的需求,正确姿势是对同步逻辑做节流,而不是放弃停手落账的模式。

停手落账:onResizeStop 把 DOM 终值写回 state

另外注意高度链条。Rnd 内部再包 CodeMirror 时,height="100%" 要求从外层容器到编辑器的每一层都有确定高度:外层 flex-1,中间层 h-full 且 flex flex-col,编辑器自身 flex-1。链条上断掉任何一环,编辑器就会塌成零高度,表现为「拖了分栏线但内容区不动」。

安装只有一步:npm install react-rnd,yarn 同理。

三、与全屏、隐藏预览共存

这类编辑器通常还带两个开关:全屏预览、显示或隐藏预览。集成时用条件渲染让三者互不干扰:isFullScreen 为真时整个卸载 Rnd,预览层 left 归零、铺满整行;隐藏预览时编辑器独占空间;重新打开时 Rnd 挂载回来,editorSize 还在 state 里,上次拖好的比例原样恢复。受控状态的另一个好处就在这里——界面显隐来回切换,用户的布局偏好不会丢。

卸载还是隐藏,是个值得权衡的取舍:卸载让 iframe 停止渲染、释放内存,代价是编辑器实例重建,光标位置与撤销历史会清零;对编辑状态敏感的产品,可以改用 CSS 控制显隐保留实例。本文的场景里预览开关切换频繁而编辑状态次要,条件渲染是更省心的选择。

四、踩坑提示

  • CodeMirror 对容器变化不敏感。编辑器实例的尺寸测量不是实时的,分栏拖动结束后容器宽高已变,必要时调用 editor.refresh() 让它重新计算,否则会出现行宽算错、滚动条残留这类怪象。
  • 尺寸单位要统一。初始值是 '50%' 这样的百分比,onResizeStop 读回的却是 '512px' 这样的像素字符串,同一个 state 里混两种单位,参与 left 计算时容易埋下隐性 bug,落账前统一成一种单位更稳妥。
  • 预览更新是整页级操作。代码一变就重写 iframe 文档,输入密集时会闪,给同步逻辑加两三百毫秒防抖,肉眼几乎无感。
  • 移动端别硬塞分栏。拖拽分栏在触屏上既难拖又占宽度,小屏应切换为上下堆叠布局,分栏线本质上是桌面端交互。
  • 让分栏线「可见」。Rnd 的缩放手柄贴在元素边缘,只有几个像素的热区,实际项目里要在右边缘画一条明显的竖线并配上 hover 变色,否则功能在、用户却找不到可拖的位置。

五、延伸:一根线背后的通用模式

「受控尺寸 + 单边缩放 + 邻居联动」这套模式还能继续生长:把 editorSize 持久化到 localStorage 或后端,用户下次打开还是自己调好的比例;面板再多一层,就得到三栏布局,状态从单个对象升级成布局树;要多面板嵌套分割,可以评估 allotment 这类专职方案。而选型的前提始终是需求本身——只需要调整大小还是也要拖动、要不要记住布局、移动端怎么降级,想清楚这几问,答案自然浮出水面。

分栏线是编辑器类产品最不起眼、又最经不起粗糙的交互。react-rnd 的价值在于把指针捕获、边界约束、受控同步这些底层细节封装成几个清晰的 props,让我们把精力留给真正的产品逻辑。下次你的工具里需要一根「跟手的线」,不妨从 enableResizing={{ right: true }} 开始。

相关文章

分享: