一起来搭建一个vue组件库(一):拖拽组件
.__drag_item{animation:shake.3s;}@keyframesshake{0%{transform:translate3d(-10%,0,0);}50%{变换:translate3d(10%,0,0);}100%{转换:translate3d(0,0,0);}}非常幸运的把我用过的、标准组件库里可能没有的组件封装成一个小小的组件库。没想到开始号断了好吧,vue-gn-components,接下来就是一步步丰富这个项目了~。期待大家的开始~,这也是我不断丰富这个组件库的动力!首先,首先要添加的是拖放组件。功能很简单,就是渲染的dom可以拖拽。至于具体的dom,这个component不管,用slot来承接,自己塞进去就行了。Vue组件按照用途(增加开发难度)可以分为三类:显示组件:通常为业务开发还原设计稿,页面显示信息,使用router切换的组件。业务组件:目前公司的业务包中提取的组件不是很通用。独立组件:不针对特定业务,如日期、表单等标准组件库中的组件,通用性强。vue组件的interface组件接口分为三种:props、自定义事件、slot。也就是告诉别人如何使用你的组件,所以这三件事必须在组件设计之初就规划好。用户习惯了你加功能,但不习惯你改界面。此拖放组件设计如下:DragWrap:设计成两个组件。最外层容器组件完成Dom移动等逻辑。DragItem:需要拖动的item,拖动的信息会被调度到容器组件中。data:接收一个数组,拖拽组件对应的渲染数据。拖动后Dom变了,原来渲染的数组也需要变。比如你可以告诉后台下次进来时根据变化的数据进行渲染。drag:如果不写namedslot,可以通过点击拖动整个被拖动的item,否则只有namedslot中的Dom才能控制整个被拖动的item。Step1.拖拽改变当前Dom的顺序。2.拖拽完成后,发送改变的数据。3、完成socket接口和交互。1.拖放改变当前Dom的顺序1.1首先了解拖放事件和属性h5拖放事件标记:这个很重要!!!不知道为什么很多人不谈拖放,也就是上面gif显示中黄色的原点,它的位移决定了拖拽事件的行为。点击开始拖动后,鼠标点击的位置就是标记。dragstart:↓当鼠标点击并移动时执行。↓drag:↓执行dragstart后,鼠标移动时持续触发。↓dragend:↓拖动行为结束时,即鼠标松开时触发。↓draenter:↓被拖动元素的标签进入一个Dom元素时触发,先触发。输入的Dom元素将触发此事件。↓dragover:被拖动元素的标签在传入的Dom元素上移动时触发,自身移动时也会触发。dragleave:↓被拖动的元素离开进入的Dom时触发。↓h5Draggable属性draggable:当一个元素要求可拖动时,需要设置为true,默认为false。选中的文字、图片、链接默认可以拖动。DataTransfer对象:该属性用于保存拖放数据和交互信息。现在不使用和忽略此组件。1.2组件编写通过以上对事件的理解,我们想了想,只需要监听三个事件dragstart,dragenter,dragend。开始拖的时候需要知道这个元素是谁,拖完之后去到哪个元素,最后一次拖的结束。因为每一个拖动的item都是一个组件,所以每次拖动都会触发这三个事件。于是我们写了下面的代码:stop="onDragend"//拖动结束时可拖动//可以拖动class="__drag_item">
可能有点乱,这里解释一下mixinEmitter,也是从iView抄来的,是组件库中经常用到的两个方法Injection,因为独立组件不会使用vuex或者bus来通信,所以跨组件通信必须有自己的show操作。这里先说明下vue自定义事件的原理。父组件通过this.$on向子组件的事件中心注册事件,子组件通过this.$emit在自己的事件中心触发事件,但是因为触发的事件是在父组件,至此父子之间的自定义事件通信就完成了。事实上,子组件完全是在发挥自己的作用。下面两个方法broadcast和dispatch的原理都是在当前组件中寻找目标组件的实例,但是一个是向下的,一个是向上的。然后使用this.$emit触发目标组件通过this.$on注册的事件,从而完成组件间的通信。他们查找组件的方式是通过组件定义的名称属性。functionbroadcast(componentName,eventName,params){this.$children.forEach(child=>{constname=child.$options.name;if(name===componentName){child.$emit.apply(child,[eventName].concat(params));}else{broadcast.apply(child,[componentName,eventName].concat([params]));}});}exportdefault{方法:{dispatch(componentName,eventName,params){letparent=this.$parent||这个.$root;letname=parent.$options.name;while(parent&&(!name||name!==componentName)){parent=parent.$parent;if(parent){name=parent.$options.name;}}if(parent){parent.$emit.apply(parent,[eventName].concat(params));}},broadcast(componentName,eventName,params){broadcast.call(this,componentName,eventName,params);}}};第一篇会啦啦点,写独立组件确实有很多需要先交代下。接下来,我们编写DragWrap组件的代码如下:会显示,不好看
这里有几点需要先注意,this.$on必须在this.$emit之前执行,因为必须先注册才能触发,否则事件会被触发还有父子组件的钩子的执行顺序。mounted表示子组件先执行,created表示父组件先执行。好吧,然后我们有拖放开始的元素和进入的元素,然后开始拖动并使用insertBefore交换它们的位置。但是这里有一点需要注意,就是要知道当前拖动的元素是向前拖动还是向后拖动,所以我们在DragWrap组件中添加如下代码:drag-wrap.vue...methods:{onDragenter(el){this.toDom=el;如果(this.fromDom===this.toDom){返回;}if(this.isPrevNode(this.fromDom,this.toDom)){//判断入口节点是否在起始节点前面this.$refs["wrap"].insertBefore(this.fromDom,this.toDom);//在入口节点之前插入起始节点}else{//否则在this.$refs["wrap"].insertBefore(this.fromDom,this.toDom.nextSibling)之后;//在入口节点的下一个兄弟节点之前插入起始节点}},isPrevNode(from,to){//to是否在from前面while(from.previousSibling!==null){if(from.previousSibling===到){返回真;}from=from.previousSibling;}}}...2。拖放结束后,发送更改的数据。写完上面的代码,现在可以根据我们期望的Dom位置来拖动和切换元素了,但这还不够,Dom的顺序已经改变了,我们需要知道对应的数据应该是什么样子的,否则,一旦刷新页面,就会是旧的方式也没有意义。2.1比较两棵Dom树大家还记得我们之前在created中定义的this.children=[],里面包含了拖拽组件的所有真实Dom元素,只是此时已经被拖拽打乱了。↓这时候我们需要知道Dom树真正的顺序是什么样子的,然后和被打乱的Dom进行比较,计算出对应的数组顺序被打乱了什么,所以我们在DragWrap组件中加入如下代码:-wrap.vue...方法:{onDragend(){if(!this.data.length)return;constrealDomOrder=[...this.$el.children].filter(child=>//获取真正的Dom树child.classList.contains("__drag_item"));this.getDataOrder(realDomOrder,this.children);//比较两棵树},getDataOrder(realList,dragAfterList){constorder=realList.map(realItem=>{//获取被打乱的Dom树对应的序号returndragAfterList.findIndex(dragItem=>realItem===dragItem);});constnewData=[];order.forEach((item,i)=>{//将原数组的数据根据??随机数赋值给新数组newData[i]=this.data[item];});this.$emit("watchData",newData);//新数组的顺序对应被打乱的Dom的序号,发送出去}}...3.完成槽接口和交互。3.1完成namedslot接口此时拖动整个drag-item组件的任意位置都可以拖动,但是有时候用户想定义可以触发拖动的位置,所以我们需要给用户这个接口,以及然后在DragItem中执行更改以下内容:"//如果设置了Namedslot,目前不能整体拖动:style="{cursor:!$slots.drag?'move':''}"//namedslot决定了该组件的交互手势class="__drag_item">
//提供命名插槽拖动
exportdefault{data(){return{isDrag:false};},mounted(){if(this.$slots.drag){//如果有一个命名槽定义了拖动this.setSlotAttr();}this.dispatch("DragWrap","putChild",this.$el);},methods:{setSlotAttr(){constslotVNode=this.$slots.default.find(//找到vnode的第一个有效节点vnode=>!vnode.data&&vnode.text!=="");constdragDom=slotVNode.elm.previousSibling;//命名slot对应的真实Domif(dragDom.previousSibling!==null){//规定namedslot中只能有一个根元素,否则会报错~throw"namedslot中只能有一个根节点~";}dragDom.addEventListener("mouseenter",()=>{//输入命名槽的Dom,设置为可拖动this.isDrag=true;});dragDom.addEventListener("mouseleave",()=>{//离开命名槽的Dom,设置它不可拖动this.isDrag=false;});dragDom.style.cursor="移动";//手势变成可动}}}不知道为什么,vue对应的默认slot可以直接获取到真实的Dom,而named的slot获取不到,有点坑~这里用这样一个获取方式不雅,slotVNode.elm.previousSibling,亲测不影响使用。那么我们规定命名槽中只能有一个根元素,否则下面设置的属性只能对一个元素起作用。3.2交互交换Dom位置时,左右会有10%的抖动~