原生 JS 侧边栏缩放:从 DOM 监听到底层优化

41 阅读2分钟

前言:实现一个可以手动拖拽宽度的侧边栏,标准的做法是实现一个垂直的“分割线手柄” 。要做到“丝滑”且不依赖任何库,我们需要处理好 mousedownmousemovemouseup 的三元组逻辑,并避开鼠标跳动文本选中的坑。


1. 核心结构:Flex 布局 + 分割线

最稳健的布局是使用 Flex。侧边栏固定宽度(或百分比),主内容区 flex: 1

HTML

<div class="container">
  <aside id="sidebar" style="width: 260px;">侧边栏内容</aside>
  <div id="resizer" class="resizer"></div>
  <main>主内容区</main>
</div>

2. 核心代码实现

实现的核心在于:document 而不是 resizer 上监听移动事件。这样即使鼠标移动太快离开了手柄,拖拽也不会中断。

JavaScript

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

.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

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,鼠标移入 iframemousemove 会失效。

  • 对策:在 mousedown 时,给所有的 iframe 加上 pointer-events: none,并在 mouseup 时恢复。

③ 状态持久化

用户调整好宽度后,刷新页面通常希望保持。

  • 对策:在 onMouseUp 中执行 localStorage.setItem('sidebarWidth', newWidth),并在页面初始化时读取。

5. 方案对比

维度CSS resize 属性原生 JS 拖拽 (推荐)
样式自定义极差,几乎无法修饰高度自由,可实现各类手柄
交互范围仅限右下角全垂直条触发,符合 AI 应用习惯
边界控制支持 min/max-width支持逻辑更复杂的动态边界
事件反馈可监听 resize 结束触发图表重绘