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

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


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

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

③ 状态持久化

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

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

5. 方案对比

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

相关推荐
万少11 小时前
HarmonyOS 开发必会 5 种 Builder 详解
前端·harmonyos
橙序员小站13 小时前
Agent Skill 是什么?一文讲透 Agent Skill 的设计与实现
前端·后端
炫饭第一名15 小时前
速通Canvas指北🦮——基础入门篇
前端·javascript·程序员
王晓枫16 小时前
flutter接入三方库运行报错:Error running pod install
前端·flutter
符方昊16 小时前
React 19 对比 React 16 新特性解析
前端·react.js
ssshooter16 小时前
又被 Safari 差异坑了:textContent 拿到的值居然没换行?
前端
曲折16 小时前
Cesium-气象要素PNG色斑图叠加
前端·cesium
Forever7_16 小时前
Electron 淘汰!新的桌面端框架 更强大、更轻量化
前端·vue.js
Angelial16 小时前
Vue3 嵌套路由 KeepAlive:动态缓存与反向配置方案
前端·vue.js