从vue组件三大核心概念出发,写好一个组件【实战篇】-写一个抽屉组件

前言

上周写了一篇如何写好一个 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;
}
复制代码

总结

  • 用到了不少 Element 的方法(eg:closest,matches),平时不多接触
  • CSS 真难写,做为一个写后台的,不常常写 CSS 的表示好难,这里费了最多的功夫
  • 实践了本身以前写好一个组件的文章,知易行难,还需努力
  • 一开始本身可能很难想全组件须要什么配置,能够文档先行,先想好作什么怎么作

参考文章

关于我

一个一年小前端,关注个人微信公众号,和我一块儿交流,我会尽我所能,而且看看我能成长成什么样子吧。交个朋友吧!

相关文章
相关标签/搜索