问题
在 vue 中我们拖拽效果一般都是用的 vuedraggable 组件实现的,但是当一个封装的组件内部用了 vuedraggable,然后想在其他地方用这个组件,要把组件内部的元素拖拽出来放到组件外部的一个 vuedraggable 区域,就如下图所示:
组件 A 和组件 B 都是已封装好的三方组件,并且内部都是用的 vuedraggable 来实现的拖拽。现在想要把组件 A 里的选项拖拽到组件 B 里面,会发现怎么都不会响应拖拽事件。
分析
vuedraggable 组件要让两块区域间能够相互拖拽,是需要设置相同的 group
的。但是我们已确认组件 A 和 组件 B 里的设置的 group
是完全一样的。但组件 B 就是无法响应拖拽事件。尝试将组件 A 和 组件 B 的代码直接放到一个工程里,而不是以组件的方式引入,发现拖拽功能就能正常使用了。
从上面的尝试分析,可以知道 vuedraggable 应该是没有支持跨组件的使用,或者是不是有什么属性配置了就可以支持跨组件使用了?我去翻看了 vuedraggable 的官方文档,并没有找到相关的说明或配置。但是从官方文档我们可以知道它是基于 Sortable.js 的,那我们继续翻看 Sortable.js 的官方文档。令人失望的是 Sortable.js 官方文档中也没有找到相关有用的信息。
到这里我们要解决这个问题只能去翻它的源码了。翻看 vuedraggable 源码,可以看到其内部其实就是在 mounted
的时候 new 了一个 Sortable 对象:
于是我们找到 Sortable 的构造函数:
js
function Sortable(el, options) {
this.el = el; // root element
this.options = options = _extends({}, options);
el[expando] = this;
// 默认配置
var defaults = {
...
};
for (var name in defaults) {
!(name in options) && (options[name] = defaults[name]);
}
...
if (options.supportPointer) {
on(el, 'pointerdown', this._onTapStart);
} else {
on(el, 'mousedown', this._onTapStart);
on(el, 'touchstart', this._onTapStart);
}
sortables.push(this.el); // Restore sorting
...
}
可以看到它给传进去的元素 el
绑定了 mousedown
事件,处理方法是 _onTapStart
,那我们在看下 _onTapStart
方法:
js
function _onTapStart(evt) {
var _this = this,
el = this.el,
options = this.options,
preventOnFilter = options.preventOnFilter,
type = evt.type,
touch = evt.touches && evt.touches[0] || evt.pointerType && evt.pointerType === 'touch' && evt,
target = (touch || evt).target,
originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0] || evt.composedPath && evt.composedPath()[0]) || target,
filter = options.filter;
// 一些对事件参数的判断逻辑
...
// 对 filter 处理
if (typeof filter === 'function') {
...
} else if (filter) {
...
}
this._prepareDragStart(evt, touch, target);
}
可以看到它里面最终是调用的 _prepareDragStart
方法:
js
function _prepareDragStart(evt, touch, target){
var _this = this,
el = _this.el,
options = _this.options,
ownerDocument = el.ownerDocument,
dragStartFn;
if (target && !dragEl && target.parentNode === el) {
rootEl = el;
dragEl = target;
parentEl = dragEl.parentNode;
nextEl = dragEl.nextSibling;
lastDownEl = target;
activeGroup = options.group;
Sortable.dragged = dragEl;
dragStartFn = function dragStartFn() {...} // 拖拽开始处理逻辑
on(ownerDocument, 'dragover', nearestEmptyInsertDetectEvent);
on(ownerDocument, 'mousemove', nearestEmptyInsertDetectEvent);
on(ownerDocument, 'touchmove', nearestEmptyInsertDetectEvent);
on(ownerDocument, 'mouseup', _this._onDrop);
on(ownerDocument, 'touchend', _this._onDrop);
on(ownerDocument, 'touchcancel', _this._onDrop);
dragStartFn();
}
}
可以看到它在拖拽开始时,会设置一下 dragEl
为当前的拖拽元素,然后会给 ownerDocument
绑定 dragover
和 mouseup
这两个拖拽移动和放置的事件。ownerDocument
是整个 html 文档,而我们的需求是跨组件,不是跨 iframe,所以到这里还是没有问题的。我们继续看 dragover
的处理函数 nearestEmptyInsertDetectEvent
:
js
function nearestEmptyInsertDetectEvent(evt) {
if (dragEl) {
evt = evt.touches ? evt.touches[0] : evt;
var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY);
if (nearest) {
// Create imitation event
var event = {};
for (var i in evt) {
if (evt.hasOwnProperty(i)) {
event[i] = evt[i];
}
}
event.target = event.rootEl = nearest;
event.preventDefault = void 0;
event.stopPropagation = void 0;
nearest[expando]._onDragOver(event);
}
}
}
可以看到它会通过 _detectNearestEmptySortable
方法去取最近的可放置区域。那是不是组件外的 vuedraggable 区域是无法取到的?我们加个断点调试发现确实取出来是空的。那问题肯定就在 _detectNearestEmptySortable
这个方法里面了,我们继续看这个方法:
js
function _detectNearestEmptySortable(x, y) {
var ret;
sortables.some(function (sortable) {
var threshold = sortable[expando].options.emptyInsertThreshold;
if (!threshold || lastChild(sortable)) return;
var rect = getRect(sortable),
insideHorizontally = x >= rect.left - threshold && x <= rect.right + threshold,
insideVertically = y >= rect.top - threshold && y <= rect.bottom + threshold;
if (insideHorizontally && insideVertically) {
return ret = sortable;
}
});
return ret;
}
可以看到它是去遍历了 sortables
这个数组,找到 (x,y)
这个坐标点所在的 sortable
元素。那这个 sortables
又是什么呢?再翻一下 Sortable.js 的源码,发现这是一个内部的全局变量:
并且在构造函数中将当前元素加了进去:
js
function Sortable(el, options) {
this.el = el; // root element
this.options = options = _extends({}, options);
el[expando] = this;
...
sortables.push(this.el); // Restore sorting
...
}
从前面分析 vuedraggable 组件的代码可以知道,我们每在页面上方一个 vuedraggable 组件,就会 new 一个 Sortable 实例,那就会往 sortables
这个数组里 push 一个 vuedraggable 组件的 dom 元素。但是为什么在同一个工程中的多个 vuedraggable 组件实例间能共用这个 sortables
数组,而以组件形式引入的就无法共用这 sortables
数组了呢?
其实这就涉及到 ES6 模块引用的作用域问题了。我们通过 import
命令去加载一个模块的时候,是会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。也就是说我们在同一个工程中,所有 import vuedraggable
组件的地方,在执行时都是读取的同一个模块文件,这个模块文件里的全局变量 sortables
就是公用的。
而以组件的方式引入后,组件内部的 vuedraggable 子组件就是内部的了,和外部工程就是隔离开的,所以无法公用。
既然问题找到了,那就要想办法能够让组件外部的 vuedraggable 实例能够注入进来。
解决方案
从上面的代码分析,我们可以知道我们的目标是要将外部的 vuedraggable 实例的 dom 元素注入到我们的组件内的 sortables
数组里。而注入 sortables
数组是要通过 new 一个 Sortable 实例来实现的。但是 Sortable 这个构造函数是在 vuedraggable 组件内部的,外部是无法直接获取到的,那我们要怎么来调用这个 Sortable 构造函数呢?
从上面我们分析 vuedraggable 组件代码时,我们看到它会把创建的 Sortable 实例赋值给 _sortable
变量:
于是我们就可以通过 vuedraggable 实例的 _sortable
属性来获取到 Sortable 实例,从而我们就可以通过 Sortable 实例的原型链获取到它的构造函数。
所以我们的方案就是在组件里对外提供一个注入方法,该方法传入一个外部的 vuedraggable 实例,利用这个 vuedraggable 实例来 new 一个 Sortable 实例出来:
js
function addDraggableItem(draggable) {
// 从外部传进的 draggable 实例里获取到它的 dom 元素和配置
const el = draggable.$el;
const options = draggable._sortable.options;
// 通过组件内部的 draggable 实例获取到 sortable 的构造函数
const innerDraggable = this.$refs['designArea'];
const sortableConstructor = innerDraggable._sortable.constructor;
// 创建 sortable 实例,将外部的 draggable 实例注入进来
new sortableConstructor(el, options);
}
使用时直接调用 addDraggableItem
这个方法即可:
js
// 自己工程中的 vuedraggable 实例
const draggable = this.$refs['draggable'];
// 组件实例
const componentA = this.$refs['componentA'];
// 将自己工程的 vuedraggable 实例注入组件中
componentA.addDraggableItem(draggable);