vue

简单轮子:Toast 组件

★课程简介

★需求分析

◇注意点

★初步实现

◇注意点

效果:

y2R5PLyMrS

★更多需求

◇自动关闭

芳芳的代码实现起来是很少的,但这是他尝试了很多遍的结果

做法:

  1. 定义两个属性——autoClose(默认true)、autoCloseDelay(默认5s)
  2. 在mounted里边,设定定时器
  3. 5s后执行定时器里的callback,即执行close方法——remove()$destroy()

◇不要自动关闭

做法:

  1. UI:作为装饰用的竖线div
  2. 如果用户传了closeButton(需要给个text和callback),那么就会展示这个按钮UI;如果用户点击了按钮,那么就执行 onClickClose

自动关闭和主动关闭是共存的,默认是50s后就自动关闭,而自动关闭前,用户可以主动去关闭它,而点击关闭,就可以调用开发者给的callback。

总之,整体来看,我们都是围绕用户会怎么使用,然后不停地去想办法解决这个需求!

整体代码没有超过30行

一个提问而来的需求:

用户传过来的text是有标签的,如需要加粗……

1563698822888

<slot></slot>是个单独特性,它可无法使用 v-html,即无法这样 <slot v-html></slot>


★完善功能

◇我想传有HTML标签的字符串

经过权衡,折中的选择了一个办法

  1. 使用一个假的slot,即不用 <slot></slot>
  2. 传HTML是一个比较危险的办法,所以让用户自行决定传不传HTML,因此我们只需要添加一个props即可!默认是不解析HTML的

◇关闭二字竖着显示

1563704862384

父元素是个flex,所以子元素搞成是默认不缩的 flex-shrink:0;

◇文字很多会很丑

1563705779447

造成这种现象的原因是「高度不能自适应」

解决字的容纳问题:

  1. 永远不要写死高度,可以改成是 min-height

然而这导致了另外一个bug,那就是子元素的 height:100%;(那根线)咩有办法根据父元素算了。不要问为什么会这样,它就是如此你有啥办法呢?

可以使用绝对定位,但是这线的位置变了。

所以使用JavaScript姿势。

主要用到 $refs这个属性

我们在mounted里边获取父元素的高度,然而这存在时间差的问题,即这是异步的,于是我们用了 $nextTick()这个办法拿到了父元素高度:

1563706572281

this.$refs.wrapper.style :返回一个 CSSStyleDeclaration 对象,表示元素的 内联style 属性(attribute),但 忽略任何样式表应用的属性 。 通过 style 可以访问的 CSS 属性列表。

所以你是无法读取到元素的height值的,我们一般通过这个属性来向DOM上加上内联的css属性,而不是修改自己原来写好的css属性

➹:demo

似乎是没有动态创建组件,并咩有出现异步情况!反正如果出现时间差的问题,那就异步执行一下呗!

➹:HTMLElement.style - Web API 接口参考 - MDN

➹:javascript - js中动态修改style属性的问题 - SegmentFault 思否

总之,min-height并不是说它是有高度的!所以儿子为百分比单位高度是不知道爸爸有多高的!

由于这个方法太tricky了,所以显得并不是很好呀!即看懂,所以我们应该重构了。

◇重构

说白了是封装成两个函数呗——

◇如何加padding?

为提示信息追加一个div,然后再加上padding,不然,如果你加到父元素身上的话,那根线是会有间隙。

其实这想想也是很有道理的:

1563721447497

我发现字体变大了,父容器的高度也变高了,但是那根线却咩有随着变高到一定程度,说白了,mounted执行一遍就GG了。

1563721803122

总之,CSS是最没有逻辑的,所以请不要问我为什么,多写多练就好了。

◇效果

弹出3s后自动关闭:

Fy5cs3bp1l

主动关闭(为了效果明显,我设置自动关闭为6s,总之,如果你在6s之内不点「关闭」按钮,那么6s一到也会自动关闭):

lX8QisSVVq

★重构

根据用例图对比我们已经实现的功能!

◇让toast出现在上中下

  1. 添加postion属性
  2. 通过postion的值去修改toast的样式

总共10行代码,而CSS代码严格上来说不能算是代码,而template的可以算是代码!

效果:

U1hfjtrkAa

◇用户持续点击,toast该咋办呢?

**①如果已经有一个toast,那就把之前的给干掉,然后再创建新的**

  1. 如何知道之前的在哪儿?——找到你创建Toast的代码

  2. 重构了创建Toast的代码——创建函数,提取形参,解析赋值形参

  3. 如何知道这是新创建的Toast?

    1563759784219

效果:

JkaN6cGQhP

commit:实现了只能有一个toast的功能

实现多个toast的之后再做,因为目前来看这性价比不高,而实现动画的则性价比更高!

**②实现动画,因为toast的弹出和消失是很突兀的**

  1. 直接使用CSS实现即可!

  2. 如何做进入的动画?

    1. 声明一个动画然后加到toast上边,然后写上初始的状态和结尾的状态
  3. 使用transform会出现跳跃现象!

    bX2jEHGIoQ

额为的优化:

由于我们第一次出现toast时是不需要close的,所有我们搞了个回调,即第一次出现不会去close。

webstorm有个功能是,你在提交的时候,如果做了两个不相关的操作,是可以把它们俩独立分开来提交的,即由一次提交变为两次提交:

1563761142102

跳跃现象出现的原因:

1563761327511

解决姿势:

  1. 可以不用 transform:translateX(-50%);来做居中,但这是最好的居中方式,如果换一种的话,得浪费很多脑细胞去想怎么居中
  2. 不要用CSS来做这个动画
  3. 还有其它办法吗?——使用两个div,外面居中,里面做动画

在添加一层div的时候,如果你没有写子代选择器是没有问题的!但是使用了flex会GG—— &的好处可以让你在嵌套div的时候,直接复制粘贴即可,而不需要修改选择器。

芳芳在编程的时候有个习惯是「这是对的吗?」,(你要做出判断选择吗?)不需要,直接说「当它是对的先,如果出bug了,那就当它错呗!」

有人说到「可以在动画的两个帧上,都加上 transform:translateX(-50%);

我们并不知道为toast加动画时,它是怎么居中的。

总之,如果按照这种写法的话,那么你就得永远背下一个规则:「我每次加动画的时候,都得加上 transform:translateX(-50%); 」,你必须背下来,不然这就是bug了。

而如果像出现这种「必须背下来,不背下来就会出现bug的代码」,那么这个代码本身就是bug。

总之,不要去背这样的东西!

或许有些东西需要去背?比如内联元素不对齐的问题,然后vertical之类的……

或许这个动画出现的场景并不多,即性价比不高,背下来也就是bug咯!

还是出现了bug——测试toast居中的情况,没有真正的居中,这个你可以把vh的高度减小即可看出来:

1563762492889

可见,这是toast的上边沿居中

解决姿势:

1563762669493

总之,你在修改 `.warpper`的样式时,需要你去看原来的样式,毕竟这是互相影响的。

测试top/middle/bottom的效果:

1563763658623

**③优化toast为bottom时,左下角和右下角是方的,而这是根据设计图而来的**

**④再优化,为top的toast应该是从上往下出现的,而不是像为bottom的toast从下往上出现的**

关于动画的时间,我们需要做一个变量,一般它的值建议是300ms或者是250ms。

效果:

4RxjMFqvyw

commit:优化三种动画

★测试用例

◇测出来了一个bug——传到秒数没有传进去

1563773961394

1s后,触发vm的close方法,而close里边有个 this.$emit('close'),所以 close事件被执行了,此时toast元素已经 remove

可见,这代码技巧性十足啊!

本来这代码是这样的:

1563774091843

◇因测试而改造参数

传了autoClose,又得传autoCloseDelay有点冗余了,能不能只写一个参数呢?即autoClose即表示要自动关闭,也表示多少秒后自动关闭

1563774500962

◇测试——我想要主动关闭

1563774724239

console.log(vm.$el.outerHTML)

测试这个的时候,出现了警告呀!

1563774875142

这来自于 autoClose的默认值为 true,其实这应该是Number类型呀,比如 5这样

◇测试——接受 enableHtml

这个测试思路来自于,如果选择器能选到该元素,那么就是作为一个元素而存在的。

每次测试都得要搞一个错误的结果才行,不然有时候测试代码是错的也通过了。

1563775198371

◇测试——接受 position

思路:就看有没有相应的class在元素身上

如果你使用watched姿势测试的,很容易会出现错误的,如果出现错误最好重启一下!

commit:完成测试用例

之后会讲到测试覆盖率,目前只是把属性和事件测一测就行了。

★总结

★Q&A

①为什么在写 .vue文件时,把 script标签写在 style标签前面呢?

因为CSS是最不重要的!

而且我们很多时候写CSS都喜欢换行,导致CSS代码所占行数过多。

可见,如果把 style标签写在前面的话,那么我们写script的时候,就得每次回顾CSS代码了。

毕竟 更新JavaScript和HTML代码的频率要比CSS高得多!

所以为了减少肉眼负担,提高书写代码效率,只好这样做了。

②为啥用vue的姿势去动态创建toast实例,而不是用原生JavaScript姿势去创建呢?

换言之,是create一个组件好,还是create一个div好?

如果你用了vue,请一定优先create一个组件而不是一个ui。

一个明显的好处是,我们的组件可以使用 @click这种方式绑定事件

而div姿势,则是需要这样 div.addEventListener('click',callback),可见这真TM麻烦啊!

总之,你既然引入了vue,那就用vue吧!毕竟你既然引入了vue,为啥不去充分使用它呢?

③remove可不可以不写?

不写的话,destroy并不能够把元素从页面中移除!

所以这需要自己删掉呀!

这是测试出来的结果!

④再看生命周期图?

img

➹: demo

➹:vue 生命周期函数 - weixin_43208813的博客 - CSDN博客

➹:2019.7.15 - 7.21 中你学到什么? · Issue #1 · KieSun/today-i-learned

⑤关于Vue的插件?

为啥需要插件?——因为我需要为 Vue 添加全局功能

如:

  1. 我需要添加 Vue 实例方法,而这可以通过把它们添加到 Vue.prototype 里边实现。
  2. 我想搞个库,它可以提供自己的 API,同时为Vue提供一个或多个全局功能。如 vue-router

那么如何使用插件呢?

通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

new Vue({
  // ...组件选项
})

Vue.use(MyPlugin)相当于是 MyPlugin.install(Vue)

如何开发一个插件?

  1. 暴露一个 install 方法
    1. 第一个参数是 Vue 构造器
    2. 第二个参数是一个可选的选项对象

如:添加实例方法

MyPlugin.install = function (Vue, options) {
	// 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

测试:

搞一个可以创建Toast组件的插件:

 let plugin = {
  install(Vue,options) {
    Vue.prototype.$toast = (message) => {
      let Constructor = Vue.extend(Toast)
      let toast = new Constructor()
      toast.$slots.default = [message]
      toast.$mount()
      document.body.appendChild(toast.$el)
    }
  }
}

使用:

Vue.use(plugin) //plugin.install(Vue) or plugin.install(Vue,options)

消息传递:

person.cut('jj')

  1. 给 persom 对象发送了一个 cut 消息
  2. person 对象会响应这个消息

这是函数调用姿势,下边这个是另一种语言的语法

Smalltalk :person cut: 'jj'

所以 plugin.install(Vue)

  1. 给 plugin 对象发送个 install 消息
  2. plugin 对象会响应这个消息

plugin install: Vue

假装理解 Vue.use(plugin)

class Vue {
	static use(xxx) {
		return xxx.install(this)
	}
}
Vue.use(plugin)

➹:插件 — Vue.js

toast.$slots.default = [message]?动态为组件的插槽赋值?

➹: demo

➹:细谈 vue - slot 篇 - 掘金

ps:

1563797624630

transform: translate(-50%, -50%);的作用?

1563805910639

一般都是配合定位来搞事情的。

⑧关于合并参数的理解?

1563845121044

总之,写一个数字3相当于是两层意思,一层是我想要自动关闭,另一层是3s后关

⑨凡是用到了JavaScript,就得单元测试吗?

如:toast元素里边的那根线的高度,我们是根据JavaScript计算出来的

  describe('CSS', function () {
    it('line 的高度', (done) => {
      const Constructor = Vue.extend(Toast)
      const vm = new Constructor({
        propsData: {
          position: 'middle',
          closeButton: {
            text: '已充值',
            callback() {
              console.log('他说已经充值智商了!')
            }
          },
          autoClose: false,
        }
      }).$mount()
      let line = vm.$el.querySelector('.line')
      let toast = vm.$el.querySelector('.toast')
      document.body.appendChild(vm.$el) 
      setTimeout(()=>{
        expect(line.style.height).to.eq(`${toast.getBoundingClientRect().height}px`)
        done()
        vm.$el.remove()
        vm.$destroy()
      })
    })
  })

测试是通过了,但是报错了:

1563851535042

表示找不到解决方案。

➹:Vue单元测试探索 - 掘金

Vue.config.errorHandler = done

1563848435021

➹:fix(toast): 添加toast的测试用例 · zyqq/wheel@cf74c2b

是在测试代码里边使用了Vue.nextTick才配置好吧!