在做流程图编辑器时遇到一个看似简单却坑点满满的需求:让用户用鼠标拖拽来平移画布。试了一圈方案后决定自己造轮子------结果不仅解决了问题,还顺便把组合键触发、方向键微调、边界限制、智能光标全做进去了。
|----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
|
|
|
|
|
先说结论
css
npm install use-canvas-drag
| 指标 | 数据 |
|---|---|
| 核心代码 | 326 行 TypeScript |
| Gzip 大小 | ~1KB |
| 运行时依赖 | 零(仅 peerDependency vue@3) |
| 支持功能 | 左键/右键/中键 + Shift/Ctrl/Alt/Meta 组合 + 方向键 + 边界限制 |
为什么不直接用现成的?
方案一:CSS overflow: auto
最简单的方案,但问题致命:
- ❌ 只能滚轮滚动,无法鼠标拖拽
- ❌ 无法区分左右中键
- ❌ 无法加组合键(比如 Shift + 右键才拖拽)
方案二:panzoom
老牌库,但:
import
panzoom(element, {
zoomSpeed: 1, // 我不需要缩放...
minZoom: 0.1, // 不需要...
maxZoom: 5, // 不需要...
})
- ~8KB gzip,大部分是缩放功能我不用
- API 设计偏传统,不适合 Vue3 Composition API
- 不支持自定义触发方式(想用右键拖?自己 hack 吧)
- 没有 TypeScript 类型提示
方案三:手写
当然可以,但你很快会遇到这些问题:
- mousedown / mousemove / mouseup 的事件绑定顺序?
- 鼠标移出容器后怎么处理?
- 怎么防止拖拽时选中文字?
- 右键菜单怎么阻止?
- 光标怎么切换?
- 边界限制怎么算?
- 方向键微调怎么做?
- ...
每个问题都不难,但合在一起就是一堆样板代码。
我的方案:useCanvasDrag
最简用法 ------ 5 行搞定
xml
<template>
<div ref="canvasRef" class="canvas"
@mousedown="handlers.onMouseDown"
@contextmenu="handlers.onContextMenu"
@keydown="handlers.onKeyDown">
<!-- 内容 -->
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useCanvasDrag } from 'use-canvas-drag'
const canvasRef = ref()
const { handlers } = useCanvasDrag({
container: () => canvasRef.value,
mode: 'right', // 右键拖拽
})
</script>
核心亮点一:灵活的触发规则引擎
这是整个库的灵魂设计。支持三种形式:
1️⃣ 快捷字符串
mode:
mode: 'right' // 右键拖拽
mode: 'middle' // 中键(滚轮点击)拖拽
2️⃣ 修饰键组合
mode:
mode: 'Ctrl+right' // 必须按住 Ctrl + 右键
mode: 'Alt+middle' // Alt + 中键
// 大小写不敏感!以下等价:
mode: 'shift+left' // ✅ 小写
mode: 'Shift+Left' // ✅ 混合大小写
mode: 'SHIFT+LEFT' // ✅ 全大写
为什么需要这个? 因为在实际项目中,左键通常用来选元素,右键可能弹出菜单。用组合键可以完美避免冲突!
3️⃣ 多规则并行 + 自定义对象
//
mode: ['left', 'shift+right']
// 含义:左键直接能拖,或者按住 shift + 右键也能拖
// 对象:完全自定义
mode: {
button: 0,
modifiers: { shift: true, ctrl: true }
}
// 含义:必须同时按住 Shift + Ctrl + 左键
🔬 规则引擎源码解析
ini
function parseTrigger(str: string): DragButtonConfig {
const lower = str.toLowerCase();
// 1. 纯按钮:'left' | 'right' | 'middle'
if (MouseButtons.includes(lower)) {
return MouseButton[lower];
}
// 2. 组合键:'Shift+left' | 'Ctrl+right'
if (str.includes('+')) {
const parts = str.split('+');
const modifiers: DragButtonConfig['modifiers'] = {};
let mouseButton: number | undefined;
for (const part of parts) {
const p = part.toLowerCase(); // ← 关键:统一转小写,容错
if (p === 'shift') modifiers.shift = true;
else if (p === 'ctrl') modifiers.ctrl = true;
else if (p === 'alt') modifiers.alt = true;
else if (p === 'meta') modifiers.meta = true;
else if (MouseButtons.includes(p)) {
mouseButton = MouseButton[p]?.button; // ← 找到目标按键
}
}
if (mouseButton !== undefined) {
return {
button: mouseButton,
// 优化:无修饰键时不传空对象,减少后续判断开销
modifiers: Object.keys(modifiers).length ? modifiers : undefined
};
}
}
// 3. 兜底:非法输入返回默认左键,不会崩溃
return MouseButton['left'];
}
三个设计细节:
- 容错优先 :全部
toLowerCase()处理,不管用户怎么写都能识别 - 精简输出 :没有修饰键时
modifiers为undefined而非{},减少后续if判断 - 安全兜底:非法字符串返回默认值而非抛错,生产环境更稳定
事件匹配层同样简洁有力:
const
return rules.value.some(rule => {
// 配置了哪些修饰键,就检查哪些(白名单模式)
if (rule.modifiers?.shift && !e.shiftKey) return false;
if (rule.modifiers?.ctrl && !e.ctrlKey) return false;
if (rule.modifiers?.alt && !e.altKey) return false;
if (rule.modifiers?.meta && !e.metaKey) return false;
// button 为 undefined 表示"任意按键都匹配"
return rule.button === undefined || rule.button === e.button;
});
};
用 some() 实现 OR 语义------只要命中一条规则就放行。
核心亮点二:推荐用法 ------ 修饰键组合模式 ⭐
这是 v0.6.0 最重要的使用建议: