前言:实现一个可以手动拖拽宽度的侧边栏,标准的做法是实现一个垂直的"分割线手柄" 。要做到"丝滑"且不依赖任何库,我们需要处理好 mousedown、mousemove 和 mouseup 的三元组逻辑,并避开鼠标跳动 和文本选中的坑。
1. 核心结构:Flex 布局 + 分割线
最稳健的布局是使用 Flex。侧边栏固定宽度(或百分比),主内容区 flex: 1。
HTML
xml
<div class="container">
<aside id="sidebar" style="width: 260px;">侧边栏内容</aside>
<div id="resizer" class="resizer"></div>
<main>主内容区</main>
</div>
2. 核心代码实现
实现的核心在于:在 document 而不是 resizer 上监听移动事件。这样即使鼠标移动太快离开了手柄,拖拽也不会中断。
JavaScript
ini
const sidebar = document.getElementById('sidebar');
const resizer = document.getElementById('resizer');
resizer.addEventListener('mousedown', (e) => {
// 1. 记录初始状态
const startX = e.clientX;
const startWidth = parseInt(getComputedStyle(sidebar).width, 10);
// 2. 锁定全局状态:防止文本被选中,改变光标
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
const onMouseMove = (e) => {
// 3. 计算新宽度:初始宽度 + 移动距离
const newWidth = startWidth + (e.clientX - startX);
// 4. 边界约束 (Min/Max Width)
if (newWidth > 150 && newWidth < 600) {
sidebar.style.width = `${newWidth}px`;
}
};
const onMouseUp = () => {
// 5. 卸载监听,还原状态
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.body.style.cursor = 'default';
document.body.style.userSelect = 'auto';
};
// 监听全局事件
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
3. CSS 关键样式:提升"手感"
手柄不能太细,否则用户很难点中。秘诀是:视觉细,感应区宽。
CSS
css
.container {
display: flex;
height: 100vh;
}
.resizer {
width: 4px; /* 视觉宽度 */
cursor: col-resize;
background: #eee;
transition: background 0.2s;
/* 增加感应范围:利用负 margin 或透明边框 */
margin: 0 -2px;
position: relative;
z-index: 10;
}
.resizer:hover, .resizer:active {
background: #007bff; /* 激活时变色 */
width: 4px;
}
4. 性能与 UX 补丁
① 性能优化:requestAnimationFrame
如果侧边栏内部有复杂的 Echarts 图表或长列表,频繁更新 width 会导致掉帧。我们可以用 rAF 节流:
JavaScript
ini
let frame;
const onMouseMove = (e) => {
if (frame) cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
const newWidth = startWidth + (e.clientX - startX);
sidebar.style.width = `${newWidth}px`;
});
};
② 解决 Iframe 穿透问题
如果你的主内容区(Main)里有 iframe,鼠标移入 iframe 时 mousemove 会失效。
- 对策 :在
mousedown时,给所有的iframe加上pointer-events: none,并在mouseup时恢复。
③ 状态持久化
用户调整好宽度后,刷新页面通常希望保持。
- 对策 :在
onMouseUp中执行localStorage.setItem('sidebarWidth', newWidth),并在页面初始化时读取。
5. 方案对比
| 维度 | CSS resize 属性 | 原生 JS 拖拽 (推荐) |
|---|---|---|
| 样式自定义 | 极差,几乎无法修饰 | 高度自由,可实现各类手柄 |
| 交互范围 | 仅限右下角 | 全垂直条触发,符合 AI 应用习惯 |
| 边界控制 | 支持 min/max-width |
支持逻辑更复杂的动态边界 |
| 事件反馈 | 无 | 可监听 resize 结束触发图表重绘 |