vue feedback 指令开发

功能需求

<template>
  <span v-feedback="{activeClass:'span-active'}"></span>
</template>
<style>
.span-active {
  background: #000;
}
</style>

当该span被点时,.span-active生效。此处span可以是任意的html元素或组件

以鼠标操作举例,所谓的点是指左键被按下,按下时样式生效,松开后失效

为什么要使用指令方式

1. 直接使用组件

<feedback :active="{activeClass: 'span-active'}">
  <span></span>
</feedback>

由于vue的组件必须包含一个根节点,实际上是

<div class="span-active">
  <span></span>
</div>

此时出现了尴尬,span本身是一个inline元素,加上feedback后变成了block,并且feedback被使用的时候并不知道slot进来的元素是什么,如果要知道势必又要增加复杂度。

2. 使用混合

<template>
  <span></span>
</template>
<script>
import feedback from '@/components/feedback.js'
export default {
  mixin: {
    feedback
  },
  data () {
    return {
      activeClassName: 'span-active'
    }
  }
}
</script>

mixin是当前使用的方式,但是对使用人员要求更高,需要同时知道两个组件的内部实现。

实现思路

该指令需要干以下几件事:

  1. 为绑定指令的组件添加点击的监听,包括mouseupmousedowntouchestarttouchendtouchcancel
  2. 当事件发生时,为元素添加相应的activeClass
  3. 当元素本身是disabled的时候,去除样式的响应,实际上是去除监听

初始化的时候,先获得绑定的元素,并且添加监听器。在监听到有关事件的时候,添加、删除样式

export default {
  bind (el, binding) {
    const className = binding.value.activeClass
    const disabled = !!binding.value.disabled
    if (!disabled) {
      el.addEventListener('mousedown', ()=> {el.classList.add(className)})
      el.addEventListener('touchstart',  ()=> {el.classList.add(className)})
      el.addEventListener('touchcancel',  ()=> {el.classList.remove(className)})
      el.addEventListener('touchend',  ()=> {el.classList.remove(className)})
      el.addEventListener('mouseup',  ()=> {el.classList.remove(className)})
    }
  }
}

到此时,一切都很美好。

在实际应用中,这个组件极有可能点击后发生了改变,比如从!disabled转为了disabled状态,需要我们对其进行进一步处理。这个看起来很简单,!disabled看起来并不需要处理,原来的事件应该保留。只有disabled的状况需要把原有的监听器去除掉。

export default {
  bind (el, binding) {
    const className = binding.value.activeClass
    const disabled = !!binding.value.disabled
    if (!disabled) {
      el.addEventListener('mousedown', ()=> {el.classList.add(className)})
      el.addEventListener('touchstart',  ()=> {el.classList.add(className)})
      el.addEventListener('touchcancel',  ()=> {el.classList.remove(className)})
      el.addEventListener('touchend',  ()=> {el.classList.remove(className)})
      el.addEventListener('mouseup',  ()=> {el.classList.remove(className)})
    }
  },
  componentUpdated (el, binding) {
    const className = binding.value.activeClass
    const disabled = !!binding.value.disabled
    if (disabled) {
      el.removeEventListener('mousedown', ()=> {el.classList.add(className)})
      el.removeEventListener('touchstart',  ()=> {el.classList.add(className)})
      el.removeEventListener('touchcancel',  ()=> {el.classList.remove(className)})
      el.removeEventListener('touchend',  ()=> {el.classList.remove(className)})
      el.removeEventListener('mouseup',  ()=> {el.classList.remove(className)})
    }
  }
}

运行后,发现监听器没有被去除掉,因为 removeEventListener 应该移除之前添加的监听器,因为使用了箭头函数,所以后面的监听器并不是之前的监听器。那我们把箭头函数换成统一的函数

function addClass (el, className) {
  el.classList.add(className)
}

function removeClass (el, className) {
  el.classList.remove(className)
}

export default {
  bind (el, binding) {
    const className = binding.value.activeClass
    const disabled = !!binding.value.disabled
    if (!disabled) {
      el.addEventListener('mousedown', addClass(el, className))
      el.addEventListener('touchstart',  addClass(el, className))
      el.addEventListener('touchcancel',  removeClass(el, className))
      el.addEventListener('touchend',  removeClass(el, className))
      el.addEventListener('mouseup',  removeClass(el, className))
    }
  },
  componentUpdated (el, binding) {
    const className = binding.value.activeClass
    const disabled = !!binding.value.disabled
    if (disabled) {
      el.removeEventListener('mousedown', addClass(el, className))
      el.removeEventListener('touchstart',  addClass(el, className))
      el.removeEventListener('touchcancel',  removeClass(el, className))
      el.removeEventListener('touchend',  removeClass(el, className))
      el.removeEventListener('mouseup',  removeClass(el, className))
    }
  }
}

报错了,想当然是不行的。addEventListener 的第二个参数

listener 必须是一个实现了 EventListener 接口的对象,或者是一个函数

如果只传入函数,那又产生了一个问题,addClassremoveCLass无法取得elclassName的。这里面还存在一个作用域的问题。最后想了一个办法,就是利用eventTarget来替代el,并且把className放在Dom上。

function addClass (event) {
  const el = event.currentTarget
  const className = event.currentTarget.getAttribute('data-active-class')
  el.classList.add(className)
}

function removeClass (event) {
  const el = event.currentTarget
  const className = event.currentTarget.getAttribute('data-active-class')
  el.classList.remove(className)
}

function feedback (el, className, option) {
  el.setAttribute('data-active-class', className)
  if (option === 'add') {
    el.addEventListener('mousedown', addClass)
    el.addEventListener('touchstart', addClass)
    el.addEventListener('touchcancel', removeClass)
    el.addEventListener('touchend', removeClass)
    el.addEventListener('mouseup', removeClass)
  } else if (option === 'remove') {
    el.removeEventListener('mousedown', addClass)
    el.removeEventListener('touchstart', addClass)
    el.removeEventListener('touchcancel', removeClass)
    el.removeEventListener('touchend', removeClass)
    el.removeEventListener('mouseup', removeClass)
  }
}
export default {
  bind (el, binding) {
    const className = binding.value.activeClass
    const disabled = !!binding.value.disabled
    if (!disabled) {
      feedback(el, className, 'add')
    }
  },
  componentUpdated (el, binding) {
    const className = binding.value.activeClass
    const disabled = !!binding.value.disabled
    const oldDisabled = !!binding.oldValue.disabled
    if (oldDisabled !== disabled) {
      if (disabled) {
        feedback(el, className, 'remove')
      } else {
        feedback(el, className, 'add')
      }
    }
  },
  unbind (el, binding) {
    const className = binding.value.activeClass
    feedback(el, className, 'remove')
  }
}

优化及未尽事宜

  1. disabled是不是可以不通过指令传入
  2. 把监听和添加样式从el转到vnode

通过webpack对css压缩后所引起的重名问题

起因

我们维护了两个组件库,一套用于PC,另一套用于Mobile。有一个使用方同时引入了这两个库,这时候突然发现了一个诡异的现象:

一个tag组件凭空旋转起来,事实上没有给这个tag添加任何动画效果

发现

直接从页面表现来看,很难发现问题所在。我们进行了以下步骤进行排查

  1. 通过chrome的animate面板,看执行的是什么动画。发现其动画名为a
  2. 通过vue控制台/源码,发现其使用了vuetransition组件,并且其transition使用了PC组件库提供的fade动画。

分析

为什么fade动画会变成了旋转?我们在编译的css文件中寻找tanslate@keyframes关键词,发现

@keyframes a {
    translate:rotate(1turn)
}
@keyframes a {
    0% {
        opacity:0;
    }
    100% {
        opacity:1;
    }
}

有2个名为a的动画,那么问题就清楚了,是两个动画冲突了。但是怎么会都叫a的呢。我们不太可能这么随意的起名。源码中也没有找到名为a的动画。那么有一种可能,就是在打包的时候被改名了。

找到Vue本身提供的webpack.prod.conf.js,发现里面是这样的:

new OptimizeCSSPlugin({
  cssProcessorOptions: config.build.productionSourceMap
    ? { safe: true, map: { inline: false } }
    : { safe: true }
})

而我们用来发布包的webpack.pack.conf.js里面是这样的:

new OptimizeCSSPlugin()

说明pack脚本是按照最大压缩方式进行压缩的,会对css进行改名,如果两个库都采用同样的方式进行压缩,自然会产生命名冲突。vue官方的配置很明显考虑到了这一点,所以直接采用了避免冲突的「安全」模式。

解决

css压缩模式换为safe并重新打包,问题解决。

NPM 撤回已发布的版本

npm unpublish $PackageName@$version

背景

之前写了一个UI组件库ui-nuclear-mobile,其v1.0.8版本上发现了一个css错误。此错误修复后,更新版本至v1.0.9。本以为到此万事大吉,结果发现在更新v1.0.9的过程中,引入了其他的bug。发现有一个使用方已经更新到了v1.0.9,只好让其回退到v1.0.8

决策

为了防止线上有问题的v1.0.9不会被更多的人使用,有两个办法:

  1. 快速修复好bug,发布新的版本
  2. 撤回v1.0.9,修改好之后重新发布

如果选择1,定位bug和修复bug都需要时间。在这段时间内v1.0.9极有可能被使用方更新,之后再修复存在不确定性。整体时间周期和2方案没有本质性的差别。最终选择了方案2

npm unpublish ui-nuclear-mobile@1.0.9

反思

后续引入的bug,应该在被Merge进开发分支时被发现。说明测试和代码review还存在着很多不足。后续的事实证明,该bug是可以被测试出来的。

流程控制依然是安全生产中一个比较重要的环节,该事件引以为戒

昆虫有趋光性吗

概念解释

1. 向光性

Phototropism向性的一种,特指「植物」。有正向负向。比如说「茎」是正向光性,而「根」是负向光性

2. 趋光性

Phototaxis是一种生物对光靠近或远离的趋性,主要指「植物」和「自养生物」。

飞蛾的趋光行为的实际解释

飞蛾之类具有复眼的夜间飞行的昆虫,常被误认为具有趋光性,这是一个认知偏误。虽然飞蛾在实际行为上表现为趋向光源飞行,事实上这是因为人造光源(如:火把,电灯)的光线与自然光线(阳光,月光,星光)不同,前者呈放射状,后者接近平行光。自然状态下通过保持平行光线的夹角修正自己的飞行路线,保持直线飞行。而人造光源呈现放射状,干扰了飞蛾的判断,与光源保持锐角飞行的飞蛾就表现成螺旋状飞向光源。

推荐阅读: 昆虫为什么不会因趋光性齐刷刷地奔向太阳?

白天只有太阳作为光源的情况

太阳光源

夜晚人造光源的情况

人造光源

紫外线灭蚊灯有用吗?

没有什么卵用

推荐阅读: 灭蚊灯放家里使用有效吗

总结

有些你认为理所应当的事情,未必是事实