基于原生JS与CSS变量实现可拖拽容器布局组件
本文介绍如何不依赖重型第三方库,利用原生JavaScript和CSS变量实现一个高性能的可拖拽容器布局组件。通过封装DragLine与DragContainer,结合Flex布局特性,实现多列宽度自定义调整,同时避免React状态更新导致的重复渲染,提升页面性能。
1. 前言
某日,产品经理提出一项需求:产品概览页采用三列布局结构,用户需能拖动列间分割线以自定义各列宽度,国际站用户对此类交互有常见需求。效果示意如下:

这看似简单,仅用开源拖拽库并在回调中调整容器宽度即可,几行代码便能完成。但作为资深前端开发者,我意识到若仅实现此功能,后续两列、三列乃至任意列数的布局均需重复开发,左侧菜单与右侧抽屉的拖拽功能亦需类似实现。因此,我决定将其封装为通用组件,以优雅方式解决此类问题。
2. 组件分析
无论是两列、三列布局,还是菜单或抽屉,其核心拖拽对象均为一条分割线。因此,首先需封装一个独立的拖拽线条组件,基于此组件再实现各类布局的宽度自定义功能将更为简便:

关于使用开源库还是自行实现,经权衡后选择后者。主要原因有二:一是现有第三方包体积较大,而业务组件作为项目必引资源,体积越小越好;二是该功能逻辑相对简单,自行实现代码可控性强,且可利用新特性优化性能。
3. DragLine
DragLine组件主要包含哪些功能?
- 内置拖拽能力,支持配置拖拽开始与结束的回调函数。
- 内置提示信息,可配置是否在首次渲染时默认显示“可拖拽”提示。
直接展示拖拽线条组件DragLine的代码实现:
// DragLine.js
import React, { useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import './index.scss';
const DragLine = ((props) => {
const {
gap = 16,
onMouseMove,
onMouseUp,
style = {},
tipKey,
defaultShowTip = false,
...rest
} = props;
const [visible, setVisible] = useState(defaultShowTip);
const ref = useRef(null);
const eventRef = useRef({});
const closeNavTips = () => {
localStorage.setItem(tipKey, 'true'); // 设置标记
setVisible(false); // 关闭弹窗
};
// 拖拽结束
const handleMouseUp = (e) => {
document.body.classList.remove('dragging');
onMouseUp && onMouseUp(e, ref.current);
document.removeEventListener('mousemove', eventRef.current.mouseMoveHandler, false);
document.removeEventListener('mouseup', eventRef.current.mouseUpHandler, false);
};
// 拖拽中
const handleMouseMove = (e) => {
onMouseMove && onMouseMove(e, ref.current);
};
// 开始拖拽
const handleMouseDown = () => {
closeNavTips();// 关闭拖拽提示框
document.body.classList.add('dragging');
eventRef.current.mouseMoveHandler = (e) => handleMouseMove(e);
eventRef.current.mouseUpHandler = (e) => handleMouseUp(e);
document.addEventListener('mousemove', eventRef.current.mouseMoveHandler, false);
document.addEventListener('mouseup', eventRef.current.mouseUpHandler, false);
};
const line = (
<div
ref={ref}
style={{
'--drag-gap': `${gap}px`,
...style,
}}
className={`drag-line ${visible ? 'active' : ''}`}
onMouseDown={handleMouseDown}
{...rest}
/>
);
return visible ? (
<Tooltip
open
placement="rightTop"
title={(
<div>
<div style={{ marginBottom: 4 }}>拖动这根线试试~</div>
<Button size="small" onClick={closeNavTips}>关闭</Button>
</div>
)}
>
{line}
</Tooltip>
) : line;
});
export default DragLine;
对应的CSS样式代码如下:
/* index.scss */
.drag-line {
width: 2px;
margin: 0 calc((var(--drag-gap, 16px) - 2px) / 2);
background: transparent;
cursor: col-resize;
&.active, &:hover {
background: blue;
}
}
.dragging {
user-select: none; // 内容不可选择
}
上述代码可直接运行,此处简要说明几点关键实现:
- JS文件第53行使用了CSS变量,对应CSS文件第4行,并通过
calc函数可实现复杂计算功能。 - JS文件第42行在拖拽时为body添加类名,对应CSS文件第14行,设置拖拽时body内容不可选中,防止用户无意选中内容造成困扰。
- 组件代码简洁,内部已封装拖拽能力及提示框,仅向业务方暴露少量API。可根据实际需求进一步封装,如线条宽度、提示内容及位置等。
4. DragContainer
基于DragLine基础组件,可轻松扩展任何需拖拽的上层组件。例如实现可拖拽的多列布局容器组件,直接展示DragContainer组件源码:
// DragContainer.js
import React, { useRef } from 'react';
import DragLine from '../DragLine';
import classnames from 'classnames';
import './index.scss';
const DragContainer = (props) => {
const {
className,
sceneKey,
minChildWidth = 150,
contentList = [],
gap = 16,
} = props;
const cls = classnames('drag-container', className);
const ref = useRef(null);
// 拖拽结束时,保存宽度信息
const onMouseUp = () => {
const widthList = contentList.map((_, i) => {
const child = ref.current.querySelector(`.item${i}`);
return `${child?.offsetWidth}px`;
});
localStorage.setItem(sceneKey, widthList.join('#'));
};
const onMouseMove = (event, node) => {
const index = parseInt(node.getAttribute('data-index'));
const leftElement = ref.current.querySelector(`.item${index}`);
const rightElement = ref.current.querySelector(`.item${index + 1}`);
// 拖动距离 = 分割线的位置 - 鼠标的位置
const dragOffset = node.getBoundingClientRect().left - event.clientX;
const newLeftChildWidth = leftElement.offsetWidth - dragOffset;
const newRightChildWidth = rightElement.offsetWidth + dragOffset;
if (newLeftChildWidth >= minChildWidth && newRightChildWidth >= minChildWidth) {
ref.current.style.setProperty(`--drag-childWidth-${sceneKey}-${index}`, `${newLeftChildWidth}px`);
ref.current.style.setProperty(`--drag-childWidth-${sceneKey}-${index + 1}`, `${newRightChildWidth}px`);
}
};
const contentData = [];
const localWidthList = localStorage.getItem(sceneKey)?.split('#') || []; // 获取本地已经保存的宽度信息
contentList.forEach((d, i) => {
contentData.push(
<div
key={`${sceneKey}_${i}`}
className={`container-item item${i}`}
style={{ flexBasis: `var(--drag-childWidth-${sceneKey}-${i}, ${localWidthList[i]})` }}
>{d}
</div>,
);
if (i < contentList.length - 1) {
contentData.push(
<DragLine
key={`${sceneKey}_dragline_${i}`}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
tipKey="draggableContainerFlag"
data-index={i}
defaultShowTip={i === 0}
gap={gap}
/>,
);
}
});
return (
<div ref={ref} className={cls}>
{contentData}
</div>
);
};
export default DragContainer;
对应样式文件如下:
/* index.scss */
.drag-container {
display: flex;
align-items: stretch;
width: 100%;
.container-item {
height: 100%;
overflow: hidden;
flex: 1; // 同比例放大缩小
}
}
DragContainer组件实现逻辑较为简单,基本思路如下:
- 遍历传入的contentList,若非最后一个子元素,则额外渲染一个
DragLine用于拖拽操作。 - 在拖拽线条的回调函数中,计算拖拽偏移量及左右子元素的新宽度,并将其设置到CSS变量中,从而实现拖拽时宽度的实时变化。代码未使用任何
React State,无需重复渲染整个组件,改变宽度直接通过CSS实现,性能更优。 - CSS文件第10行对Flex布局子元素设置
flex: 1,意为拖动浏览器窗口大小时,子元素宽度同比例放大缩小,实现宽度自适应。前提是子元素宽度不写死,需配合JS文件第53行的flexBasis属性共同使用。 - 上述代码同样可直接运行,有需要的同学可直接尝试。
5. 使用效果
在业务代码中使用DragContainer组件编写示例,使用简单,效果完美:
<DragContainer
sceneKey="overview-page"
contentList={[
<Card>111</Card>,
<Card>222</Card>,
<Card>333</Card>,
]}
/>

6. 总结
本文核心观点在于:接到需求后,应先学会分析与过滤。若是特定业务需求,直接实现即可;若是通用类需求,则需从组件开发角度思考,能否举一反三,通过组件开发覆盖更多场景与问题。此外,在功能实现方面总结以下几点:
- 应通过对比选择最合适的技术方案,例如简单拖拽功能完全可使用原生JS实现,而非引入体积庞大的第三方包。
- 容器宽度改变可直接修改CSS属性,而非使用React状态,以减少不必要的重复渲染。
- CSS变量技术是打通JS与CSS的一种有效手段。
- 熟练运用Flex布局相关属性,能以更优方案解决部分布局问题。
到此这篇关于基于JS实现一个可拖拽的容器布局组件的文章就介绍到这了,更多相关JS可拖拽容器布局组件内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!
JS在线运行环境怎么引入Vue、React等第三方库
2026-11-04 23:54:46
dva 中怎么发起异步请求?effects 配合 call/put 实战
2026-11-03 18:25:53
React和Vue中利用JS三元运算符进行条件渲染
2026-11-03 11:20:51
React脚手架config-overrides.js文件的配置方式
2026-10-31 07:13:03
React项目中实现吸顶导航栏的完整教程
2026-10-30 20:21:01







