# 手把手教你实现“左右拖动布局”:打造丝滑的 Vue 分屏体验
在开发像考试系统、代码编辑器或者对比工具这类 Web 应用时,左右分屏且可自由拖动调整宽度的布局是一种非常常见且高频的需求。
效果演示
我们需要实现的效果如下:
- 页面分为“左侧内容区”、“中间拖动条”、“右侧内容区”。
- 用户按住中间的拖动条(Resizer)左右拖动,实时改变左右两边的宽度比例。
- 拖动过程中禁止文字选中,避免体验跳脱。
- 设置最小/最大宽度限制,防止某一边被挤压得看不见。
核心思路
我们的实现核心在于 Flex 布局 配合 百分比宽度。
-
布局:父容器使用
display: flex。 -
宽度控制:使用一个响应式变量
leftPercentage来控制左侧容器的宽度。右侧容器的宽度就是100% - leftPercentage。 -
交互逻辑:
-
mousedown:在拖动条上按下鼠标,标记开始拖动,并注册全局mousemove和mouseup事件。 -
mousemove:计算鼠标当前位置相对于父容器的百分比,动态更新leftPercentage。 -
mouseup:松开鼠标,移除事件监听,结束拖动。
-
代码实现
以下以 Vue 2 为例(Vue 3 原理完全相同,只是语法稍有区别)。
1. HTML 结构
结构非常简单,经典的“三明治”夹心结构。
<template>
<!-- 父容器 -->
<div ref="splitPane" class="split-pane-container">
<!-- 左侧面板 -->
<div class="left-pane" :style="{ width: leftPercentage + '%' }">
<div class="content">
<!-- 插槽或具体内容 -->
<slot name="left">Left Content</slot>
</div>
</div>
<!-- 拖动条 (Resizer) -->
<div class="resizer" @mousedown="startResize">
<!-- 可以放一个拖拽图标,增加可识别性 -->
<img src="@/assets/icon_handler.png" class="icon-handler" />
</div>
<!-- 右侧面板 -->
<div class="right-pane" :style="{ width: (100 - leftPercentage) + '%' }">
<div class="content">
<slot name="right">Right Content</slot>
</div>
</div>
</div>
</template>
2. CSS 样式
关键点在于 resize 的样式设置,以及 cursor: col-resize 提示用户可以左右拖动。
.split-pane-container {
display: flex;
height: 100vh; /* 或指定高度 */
overflow: hidden;
}
.left-pane, .right-pane {
overflow-y: auto; /* 内容溢出滚动 */
height: 100%;
}
/* 拖动条样式 */
.resizer {
width: 14px; /* 拖动条宽度 */
cursor: col-resize; /* 鼠标样式变为左右拖动箭头 */
background-color: #f5f7fa;
border-left: 1px solid #e4e7ed;
border-right: 1px solid #e4e7ed;
/* 居中内部图标 */
display: flex;
justify-content: center;
align-items: center;
/* 防止 Flex 压缩拖动条宽度 */
flex-shrink: 0;
transition: background-color 0.3s;
}
.resizer:hover {
background-color: #e6e8eb; /* 悬停高亮 */
}
.icon-handler {
width: 20px;
pointer-events: none; /* 防止拖动图片本身 */
user-select: none;
}
3. JavaScript 核心逻辑
这里是灵魂所在。特别需要注意的是事件监听必须绑定在 document 上,而不是 resizer 上。因为用户拖动过快时,鼠标可能会移出拖动条范围,如果绑定在 resizer 上会导致拖动断触。
export default {
data() {
return {
leftPercentage: 50, // 初始左侧宽度占比 50%
isResizing: false, // 是否正在拖动标志位
}
},
methods: {
// 1. 开始拖动
startResize() {
this.isResizing = true
// 添加全局事件监听
document.addEventListener('mousemove', this.doResize)
document.addEventListener('mouseup', this.stopResize)
// 关键优化:拖动时禁止选中文字,避免变蓝
document.body.style.userSelect = 'none'
document.body.style.cursor = 'col-resize' // 强制全局鼠标样式
},
// 2. 执行拖动
doResize(e) {
if (!this.isResizing) return
const splitPane = this.$refs.splitPane
if (!splitPane) return
// 获取父容器的位置信息
const containerRect = splitPane.getBoundingClientRect()
const containerWidth = containerRect.width
const containerLeft = containerRect.left
// 计算鼠标相对于父容器左侧的距离 (X轴)
const mouseX = e.clientX - containerLeft
// 转换为百分比
let newLeftPercentage = (mouseX / containerWidth) * 100
// 边界限制:建议设置 20% ~ 80%,防止某一边被完全遮挡
if (newLeftPercentage < 20) newLeftPercentage = 20
if (newLeftPercentage > 80) newLeftPercentage = 80
this.leftPercentage = newLeftPercentage
},
// 3. 结束拖动
stopResize() {
this.isResizing = false
// 移除事件监听
document.removeEventListener('mousemove', this.doResize)
document.removeEventListener('mouseup', this.stopResize)
// 恢复样式
document.body.style.userSelect = ''
document.body.style.cursor = ''
}
}
}
遇到的“坑”与优化点
1. 拖动卡顿与丢帧
问题:如果在
doResize 中做复杂的 DOM 操作,会导致拖动卡顿。
解决方案:我们只改变了一个响应式变量 leftPercentage,Vue 的 Diff 算法足够快。如果依然卡顿,可以使用 requestAnimationFrame 进行节流。
2. 鼠标移出拖动条失效
问题:鼠标拖得太快,离开了 .resizer 元素,拖动就停止了。
解决方案:如上代码所示,使用 document.addEventListener 监听 mousemove,确保鼠标在页面任何位置都能响应。
3. 文字选中干扰
问题:拖动时如果不小心选中了左右两边的文字,体验非常差,甚至会自动触发浏览器的原生拖拽。
解决方案:在 startResize 中设置 document.body.style.userSelect = 'none',拖动结束后恢复。
4. Iframe 遮挡问题(进阶)
问题:如果你的左右面板里嵌入了 <iframe>(例如显示 PDF 或外部网页),鼠标滑过 iframe 时,mousemove 事件会被 iframe 吞掉,导致拖动失效。
解决方案:在 startResize 时,给所有的 iframe 上面覆盖一层透明的 div,或者设置 pointer-events: none,拖动结束后恢复。
总结
通过简单的 Flex 布局和数十行 JS 代码,我们就能实现一个高性能、兼容性好的分屏组件。这个方案不依赖任何重型第三方库(如 split.js),非常适合不仅需要轻量,又需要高度定制 UI 的场景。