前言
从今天开始开启一个源码阅读的专栏,一方面想把工作中用到的组件进行深入理解,一方面想逐步提高写组件、写插件的能力。🤞🤞
今天想分享的是【vue-multipane】插件,具有实现布局可拉伸、可拖拽的能力。源码很简单,GitHub地址分享在这里vue-multipane。
以下内容会截取组件的部分代码片段。
内容
类图
classDiagram
vueMultipane <|-- multipane
vueMultipane <|-- multipaneResizer
class multipane{
+String layout
+Array classnames
+Boolean isResizing
+String cursor
+String userSelect
+slots default
+mousedown(target, pageX, pageY)
+paneResizeStart(pane, resizer, size)
+paneResize(pane, resizer, size)
+paneResizeStop(pane, resizer, size)
}
class multipaneResizer{
+slots default
}
以上用类图表示了这个插件的主要结构,在源码的src/目录里。分为multipane.vue和multipane-resizer.vue。前者是需要包裹在可拖拽容器的外层,作为其父容器,后者需要放置在需要拖拽元素的后一个兄弟元素位置上。
源码中的demo/目录,是作者提供的两个例子,支持横向和纵向可拖拽功能。
分析multipane
multipane.vue组件模板中只有一个div容器,监听mousedown事件,针对不同排列方式,绑定一组样式classnames,排列方式分为horizontal和vertical。外部可以通过props传入需要哪一种排列。
isResizing判断是否处于拖拽模式;
cursor当在处于拖拽模式,判断排列方式,为鼠标箭头加上不同的样式;
userSelect 当在处于拖拽模式,鼠标不可以选中;
监听mousedown事件,从事件获取target(即multipaneResizer)、鼠标初始化位置(pageX、pageY),注册mousemove与mouseup事件,mousemove事件内部,判断排列方式计算水平 or 垂直方向上移动的距离,移动距离的offset计算公式是,mousemove事件中获取最新的pageX、pageY,与初始鼠标位置做差值。
onMouseDown({ target: resizer, pageX: initialPageX, pageY: initialPageY }) {
if (resizer.className && resizer.className.match('multipane-resizer')) {
let self = this;
let { $el: container, layout } = self;
let pane = resizer.previousElementSibling;
let {
offsetWidth: initialPaneWidth,
offsetHeight: initialPaneHeight,
} = pane;
let usePercentage = !!(pane.style.width + '').match('%');
const { addEventListener, removeEventListener } = window;
const resize = (initialSize, offset = 0) => {
if (layout == LAYOUT_VERTICAL) {
let containerWidth = container.clientWidth;
let paneWidth = initialSize + offset;
return (pane.style.width = usePercentage
? paneWidth / containerWidth * 100 + '%'
: paneWidth + 'px');
}
if (layout == LAYOUT_HORIZONTAL) {
let containerHeight = container.clientHeight;
let paneHeight = initialSize + offset;
return (pane.style.height = usePercentage
? paneHeight / containerHeight * 100 + '%'
: paneHeight + 'px');
}
};
// This adds is-resizing class to container
self.isResizing = true;
// Resize once to get current computed size
let size = resize();
// Trigger paneResizeStart event
self.$emit('paneResizeStart', pane, resizer, size);
const onMouseMove = function({ pageX, pageY }) {
size =
layout == LAYOUT_VERTICAL
? resize(initialPaneWidth, pageX - initialPageX)
: resize(initialPaneHeight, pageY - initialPageY);
console.log(size,'mouseMove')
self.$emit('paneResize', pane, resizer, size);
};
const onMouseUp = function() {
// Run resize one more time to set computed width/height.
size =
layout == LAYOUT_VERTICAL
? resize(pane.clientWidth)
: resize(pane.clientHeight);
// This removes is-resizing class to container
self.isResizing = false;
removeEventListener('mousemove', onMouseMove);
removeEventListener('mouseup', onMouseUp);
self.$emit('paneResizeStop', pane, resizer, size);
};
addEventListener('mousemove', onMouseMove);
addEventListener('mouseup', onMouseUp);
}
}
从demo/中还可以看出来,宽度是配置百分比与具体像素值两种模式的,代码里也做了判断.
// 是否使用百分比布局
let usePercentage = !!(pane.style.width + '').match('%');
从源码中也看出来,前文中说到的,为什么multipane-resizer.vue要放置在被拖拽元素的后一个位置上。
let pane = resizer.previousElementSibling; // 获取multipaneResizer前一个兄弟元素
let {
offsetWidth: initialPaneWidth,
offsetHeight: initialPaneHeight,
} = pane;
在mouseup事件中,将pane的clientWidth(可视宽度/可视高度)赋值给pane,为什么要这么处理呢,因为在使用场景中,尽管侧边栏可拖拽,也需要设置最大宽度与最小宽度的限制,才能保证良好的用户体验。所以不管鼠标拖拽到多远多高,也会有最大限制😂
将isResizing重置,恢复默认模式。
注销mousemove和mouseup事件。
const onMouseUp = function() {
// Run resize one more time to set computed width/height.
size =
layout == LAYOUT_VERTICAL
? resize(pane.clientWidth)
: resize(pane.clientHeight);
// This removes is-resizing class to container
self.isResizing = false;
removeEventListener('mousemove', onMouseMove);
removeEventListener('mouseup', onMouseUp);
self.$emit('paneResizeStop', pane, resizer, size);
};
multipane-resizer.vue组件里更加简单,只有一个默认插槽,可以放置自定义的拖拽按钮。
尾声
到这里,这个组件就分析完成了,没有想象中那么难,组件灵活、实用。自我总结一下,写插件、组件重要的是思路,你要把自己想象成插件使用者,如何能让插件更加灵活、易用、可扩展、兼容性,也就是方案设计的重要性。
还有就是这篇文章只对源码中的重点代码做了分析,对整个项目的工程化没有做解析,下一篇的源码阅读会逐渐增加这些内容。💕