源码阅读-vue-multipane实现布局可拉伸

650 阅读3分钟

前言

从今天开始开启一个源码阅读的专栏,一方面想把工作中用到的组件进行深入理解,一方面想逐步提高写组件、写插件的能力。🤞🤞

今天想分享的是【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.vuemultipane-resizer.vue。前者是需要包裹在可拖拽容器的外层,作为其父容器,后者需要放置在需要拖拽元素的后一个兄弟元素位置上。

源码中的demo/目录,是作者提供的两个例子,支持横向和纵向可拖拽功能。

分析multipane

multipane.vue组件模板中只有一个div容器,监听mousedown事件,针对不同排列方式,绑定一组样式classnames,排列方式分为horizontalvertical。外部可以通过props传入需要哪一种排列。

isResizing判断是否处于拖拽模式;

cursor当在处于拖拽模式,判断排列方式,为鼠标箭头加上不同的样式;

userSelect 当在处于拖拽模式,鼠标不可以选中;

监听mousedown事件,从事件获取target(即multipaneResizer)、鼠标初始化位置(pageXpageY),注册mousemovemouseup事件,mousemove事件内部,判断排列方式计算水平 or 垂直方向上移动的距离,移动距离的offset计算公式是,mousemove事件中获取最新的pageXpageY,与初始鼠标位置做差值。

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重置,恢复默认模式。

注销mousemovemouseup事件。

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组件里更加简单,只有一个默认插槽,可以放置自定义的拖拽按钮。

尾声

到这里,这个组件就分析完成了,没有想象中那么难,组件灵活、实用。自我总结一下,写插件、组件重要的是思路,你要把自己想象成插件使用者,如何能让插件更加灵活、易用、可扩展、兼容性,也就是方案设计的重要性。

还有就是这篇文章只对源码中的重点代码做了分析,对整个项目的工程化没有做解析,下一篇的源码阅读会逐渐增加这些内容。💕