上周写了一篇如何写好一个 vue 组件,算是理论篇吧,可是知易行难,这周我就本身写一个组件,来实践一下本身的文章css
本文章以抽屉组件为例子,有很差或者不完善的地方,欢迎在评论区指出或者提 PR,项目地址,项目 demo 地址html
咱们想象一下用户会如何使用咱们的组件,它可能须要哪些自定义的功能,好比内容的宽度,控件的位置,抽屉的位置,控件样式自定义等等,可能的交互好比:点击控件/鼠标悬浮打开抽屉,点击抽屉外部收起抽屉等等,接着咱们判断一下哪些是须要暴露给外部组件的,哪些是属于组件内部的状态,尽量的作到这个组件职责单一,且遵循最少知识原则。从这些个角度出发,来编写咱们的代码前端
这个组件有个通用的名字,叫抽屉(Drawer),组件结构分为控件和内容两部分。如图:vue
+-----------------------+
| |
+-------+ |
| | |
| | |
| | content |
controls | |
| | |
| | |
| | |
+-------+ |
| |
+-----------------------+
复制代码
不以规矩,不成方圆。HTML 有语义化标签,CSS 有 BEM 规范,这些帮助咱们写出结构清晰的 HTML 架构(ps:布局部分使用语义化标签还挺适合的,这种局部小组件仍是 div 一把梭了)。组件 HTML 结构以下:git
<div class="drawer-container"> <div class="drawer"> <div class="controls__container" ref="controls__container"> <ul class="controls"> <li>xxx</li> </ul> </div> <div class="content"></div> </div> </div> 复制代码
咱们拿贴在右侧的抽屉举例(实际代码与它不彻底相同):github
咱们定义好抽屉的大小,并将其 postion 设置为 fixed,使用 top,right 属性,将其固定在右侧。由于抽屉默认是收起的,而后经过 translate 将其移除可视区。数组
.drawer { width: '300px'; height: '100vh'; position: fixed; top: 0; right: 0; transform: 'translate(100%,0)'; } 复制代码
抽屉展开的代码也很简单,在经过 translate 将其移回来微信
.drawer__container--show .drawer { transform: translate(0); } 复制代码
经过负值将控件从抽屉内容区移出来markdown
.controls__container { position: absolute; left: -40px; } 复制代码
抽屉组件支持了 mouseover 和 click 事件,开发的时候,遇到一个比较麻烦的问题:当抽屉以 mouseover 触发,鼠标移到控件上的时候,抽屉会很鬼畜的打开收起打开收起。(由于鼠标在控件上,mouseover 事件不断的被触发,致使抽屉的打开和收起)架构
面对这种状况,我一开始就想到了防抖和节流。但其实直接拿来用是不适合的
防抖的原理:你尽管触发事件,可是我必定在事件触发 n 秒后才执行,若是你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内再也不触发事件,我才执行。
防抖因为是在一个事件触发 n 秒以后才执行,致使组件有一种反应慢的感受。
节流的原理:若是你持续触发事件,每隔一段时间,只执行一次事件。
其执行事件是异步的,那么当我打开抽屉,而后将鼠标移到抽屉外(移到抽屉外会关闭抽屉),由于抽屉的打开和关闭都是由show
变量控制。若是使用节流,会致使异步执行打开抽屉的函数,致使抽屉关闭以后又开起。
节流通常是指事件在一段时间内执行。咱们这里不妨换一种思路,对show
值进行节流,你也能够把它理解成一种锁。那么当show
值变化后,咱们锁住show
值,n 秒内不容许修改,n 秒后才能够修改。即控制住了抽屉不会在短期内迅速开合。咱们使用计算属性实现以下:
// this.lock 初始值为undefine // 开闭抽屉的函数经过对lockedShow进行赋值,不会直接操做show lockedShow: { get() { return this.show; }, set(val) { if (this.lock) { return; } else { this.lock = setTimeout(() => { // 200毫秒以后解除锁 this.lock = undefined; }, 200); this.show = val; } } } 复制代码
这里咱们经过 Element.closest() 方法用来获取点击的祖先元素(Element.closest:匹配特定选择器且离当前元素最近的祖先元素,也能够是当前元素自己)。若是匹配不到,则返回 null。
closeSidebar(evt) { const parent = evt.target.closest(".drawer"); // 点击抽屉之外部分,即匹配不到,parent值为null if (!parent) { this.show = false; } } 复制代码
全局监听点击事件
window.addEventListener('click', this.closeSidebar) 复制代码
我一开始的作法是,组件挂载的时候,全局监听点击事件,组件销毁时移除点击事件。但咱们能够作的更好,当 controls 被点击时,添加点击事件,收起抽屉的时候,移除点击事件。减小全局监听的 click 事件。
除了点击事件,咱们也顺便支持一下 hover 的操做。鼠标移出收起的操做和点击抽屉外部分收起的代码相同。
经过e.type
判断是点击事件仍是鼠标移入事件。
toggleDrawerShow(e) { if (e.type === "mouseover" && this.triggerEvent === "mouseover") { // do some thing } if (e.type === "click" && this.triggerEvent === "click") { // do some thing } } 复制代码
使得控件彻底贴合内容区,不会由于控件的内容变化,好比控件内容为 show 和 hidden,因为切换的时候,两个单词长度不同,而使得控件显示不彻底,或者脱离内容区。
这种状况咱们可使用 JavaScript 动态计算。由于常常用到,仍是封装成一个函数吧。仍是拿右侧抽屉举例子:
updateControlLayout() { // 获取控件的宽高 const rect = this.$refs['controls'].getBoundingClientRect() if (this.position === 'right') { // 从新设置偏移量 this.$refs['controls'].style['left'] = `-${rect.width}px` } } 复制代码
主要是蒙层的显影,以及抽屉的开合。CSS动画贝塞尔曲线了解一下,笔者本身也了解很少,感兴趣能够本身去看。
transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1); 复制代码
当内容过长的时候,打开抽屉的时候,滚动条还在。所以咱们须要在抽屉打开的时候打开滚动条。代码也很好写,给document.body
添加overflow:hidden
属性。
这里有一个小小的坑。原先的 css 是置于 scope 里面的,若是想要把这个属性添加到 body 上,是不成功的。把 scoped 去了便可。
<style>
.hidden_scoll_bar{
overflow: hidden;
}
</style>
复制代码
每一个人都有本身独特的审美,否则也不会出现那么多的 UI 库了。做为一个组件的设计者,很难预设不少种样式让每个使用组件的人都满意。不如把本身定义的控件做为插槽的后备内容,用户能够很方便的使用control
的具名插槽覆写控件。
<li v-for="(control,idx) in controlItems" class="control" :class="'control-'+idx" :key="idx" > <template v-if="show"> // 提供用户自定义插槽所须要的信息(控件是否展现,控件的信息) <slot name="control" v-bind:drawer="{drawerShow:show,control}" >{{control.hidden}}</slot > </template> <template v-else> <slot name="control" v-bind:drawer="{drawerShow:show,control}" >{{control.show}}</slot > </template> </li> 复制代码
由于抽屉支持在上下左右四个方向上放置,不一样方向上定义的偏移方向都不一样。所以须要定义不一样的 css 类。经过传入的 position 值,利用 css 的级联特性应用样式
<div class="drawer__container" :class="[positionClass,{'drawer__container--show':show}]" ></div> 复制代码
data() { return { show: false, positionClass: this.position }; }, 复制代码
// 定义右侧的drawer,其他方向上的同理
// 经过css的级联,对不一样方向上的drawer添加不一样的样式
.right .drawer {
height: 100vh;
width: 100%;
transform: translate(100%, 0);
top: 0;
right: 0;
}
复制代码
抽屉组件内部的状态没有被暴露出去,用户可能有点击控件,不打开抽屉而去作其余事情的需求。所以咱们须要提供一个钩子,经过 prop 将函数openDrawer
传入,openDrawer
控制是否抽屉被打开。
点击控件,开合抽屉的实现,利用了事件委托,将 click 事件,mouseover 事件直接挂载到了class=controls
的 ul 元素上,为了方便识别目标li
元素,给每个 li 元素添加 :class="'control-'+idx"
<ul class="controls" @click="toggleDrawerShow" @mouseover="toggleDrawerShowByMouseover" > <li v-for="(control,idx) in controlItems" class="control" :class="'control-'+idx" :key="idx" > <!-- xxx --> </li> </ul> 复制代码
// 开合抽屉的函数 openDrawerByControl(evt) { const onOpenDraw = this.openDrawer; if (!onOpenDraw) { this.lockedShow = true; return; } // 获取到目标阶段指向的函数 const target = evt.target; //获取到代理事件的元素 const currentTarget = evt.currentTarget; // 咱们给openDraw传入target,currentTarget两个参数,具体由父组件决定onOpenDraw如何实现 this.lockedShow = onOpenDraw(target, currentTarget); } 复制代码
父组件传入的函数以下,关于事件委托的知识感受能够应用在这里,笔者作一个示例,让class='control-0'
的元素不能点击。
咱们使用 Element.matches 匹配.control-0
类,其能够像 CSS 选择器作更加灵活的匹配。但由于 li 元素里面可能还有其余元素,因此须要不断向上寻找其父元素,直到匹配到咱们事件委托的元素为止
openDrawer(target) { let shouldOpen = true; // 仅遍历到最外层 while (!target.matches(".controls")) { // 判断是否匹配到咱们所须要的元素上 if (target.matches(".control-0")) { shouldOpen = false; break; } else { // 向上寻找 target = target.parentNode; } } return shouldOpen; } 复制代码
一个一年小前端,关注个人微信公众号,和我一块儿交流,我会尽我所能,而且看看我能成长成什么样子吧。交个朋友吧!