【element3-小哈喽参与日记】重写Notification组件 上

1,294 阅读4分钟

简介

举例上次重写Link组件已经过了14天了,先来说一下这14天我都做了些什么事吧

  1. 看《红宝书》第四版,因为在参与element3就开始看了,所以这14天中,花了半天时间,看完红宝书
  2. 看《深入理解ES6》,这本书呢,讲的比较通俗易懂,个人觉得,相较于阮一峰老师的《ES6入门教程》第三版阅读性好一些。
  3. 看《绿宝书》,目前看到第三章,这本书因为讲JavaScript的角度不一样,阅读起来会勾起浓厚的兴趣,但阅读也是有一定难度的,需要慢慢磨~
  4. 看《重构 改善既有代码的设计》跟 《10x程序员工作法》,看这两个呢,主要是参加element3的过程中,需要用到,也是element3负责人@春去春又来推荐我们看,重构由于是后面买的,所以才看到第一章,《10x程序员工作法》呢,就比较容易了,可以用听的,这边主要重点听了任务分解的章节
  5. 重写Notification组件

好了,废话了那么多,开始这篇文章的正文

需求

这次相对于Link组件,Notification组件比较难一点,我们还是老规矩,先列Tasking,从Tasking看,就比Link组件的Tasking多了好一些功能 Tasking

TDD

重构前的代码,这边就不贴出来了,大家可以点击连接查看old notification component

通过查看源码,我们会发现源码有对keydown做监听,但文档中并没有告知我们这一点~ 所以我列的Tasking需要添加一条监听keydown。

Tasking第一次列的时候并不一定要列的非常的细,可以通过边编码边完善Tasking,那这样的话,编码就会变得非常简单,只要跟着Tasking一件件的完成就好了,跟打怪升级一样。

Notification组件的基本结构,后面代码将只列出相关部分

<template>
    <transition>
        <div class="el-notification" role="alert">
            <i class="el-notification__icon" v-if="type"></i>
            <div class="el-notification__group">
                <h2 class="el-notification__title"></h2>
                <div class="el-notification__content">
                </div>
                <div class="el-notification__closeBtn el-icon-close">
                </div>
            </div>
        </div>
    </transition>
</template>
<script>
export default {
	name: 'ElNotification',
    props: {
        customClass: { 
            type: String
            default: ''
        },
        dangerouslyUseHTMLString: {
            type: Boolean,
            default: false
        },
        duration: {
            type: Number,
            default: 4500
        },
        iconClass: {
            type: String,
            default: ''
        },
        verticalOffset: {
            type: Number,
            default: 0
        },
        message: [String, Object]
        position: {
            type: String,
            default: 'top-right'
        },
        showClose: {
            type: String,
            default: ''
        },
        title: {
            type: String,
            default: ''
        },
        type: {
            type: String,
            default: '',
            validator(val) {
                return ['', 'success', 'warning', 'info', 'error'].includes(val)
            }
        }
       
    }
}
</script>

通过props.title定义Notification组件的标题

测试

it('title', () => {
    const title = 'this is a title'
    const wrapper = mount(Notification, {
        props: {
            title
        }
    })
    expect(wrapper.get('el-notification__title').text()).toContain(title)
})

代码

## template
<h2 class="el-notification__title" v-text="title></h2>

通过slots的方式,定义Notification组件的内容,并message 类型为VNode

测试

import { h } from 'vue'

it('the message type is VNode', () => {
    const message = h('p','foo')
    const wrapper = mount(Notification,{
        slots: {
            default: message
        }
    })
    expect(wrapper.get('.el-notification__content').get(p).html()).toContain(
        '<p>foo</p>'
    )
})

代码

## template
<div class="el-notification__content">
    <slot>
    </slot>
</div>

通过props.message定义Notification组件的内容

message 类型为string

测试

it('the message type is string'), () => {
    const message = 'this is a message'
    const wrapper = mount(Notification, {
        props: {
            message
        }
    })
    expect(wrapper.get('.el-notification__content').text()).toContain(message)
}) 

代码

## template
<div class="el-notification__content" v-show="message">
    <slot>
        <p>{{ message }}</p>
    </slot>
</div>

通过props.dangerouslyUseHTMLString控制Notification是否将message属性作为HTML片段处理

测试

it('dangerouslyUseHTMLString', () => {
    const wrapper = mount(Notification, {
        props: {
            dangerouslyUseHTMLString: true,
            message: "<div id='foo'>foo</div>"
        }
    })
    expect(wrapper.find('#foo').exists()).toBe(true)
})

代码

## template
<div>
    <slot>
        <p v-if="!dangerouslyUseHTMLString">{{ message }}</p>
        <p v-else v-html="message"></p>
    </slot>
</div>

通过props.type控制Notification组件的主题样式,可选值['success', 'warning', 'info', 'error']

测试

it('show type icon', () => {
    const type = 'info'
    const wrapper = mount(Notification, {
        props: {
            type
        }
    })
    expect(wrapper.find(`.el-icon-${type}`).exists()).toBe(true)
    expect(wrapper.find('.el-notification__icon).exists()).toBe(true)
})

代码

## template
<i class="el-notification__icon" :class="typeClass" v-if="type"></i>

## script
setup(props){
    const typeClass = computed(() =>{
        return props.type ? `el-icon-${props.type}` : ''
    })
    return {
        typeClass
    }
}

通过props.iconClass自定义Notification组件的图标类名,若设置了type,则iconClass会被覆盖

测试

it('iconClass', () => {
    const iconClass = 'el-icon-setting'
    const wrapper = mount(Notification, {
        props: {
            iconClass
        }
    })
    expect(wrapper.find(`.${iconClass}`).exists()).toBe(true)
    expect(wrapper.find('.el-notification__icon').exists()).toBe(true)
})

代码

## template
<i class="el-notification__icon" :class="[typeClass, iconClass]" v-if="type"></i>

测试

it('iconClass should be overridden', () => {
   const type = 'info'
   const iconClass = 'el-icon-setting'
   const wrapper = mount(Notification, {
       props: {
           type,
           iconClass
       }
   })
   expect(wrapper.find(`.el-icon-${type}`).exists()).toBe(true)
   expect(wrapper.find(`.${iconClass}`).exists()).toBe(false)
   expect(wrapper.find('.el-notification__icon).exists().toBe(true)
})

代码

## template
<i class="el-notification__icon" :class="typeClass || iconClass" v-if="type || iconClass"></i>

注: 在重构这块时,发现源码中有<div class="el-notification__group" :class="{'is-with-icon': typeClass || iconClass }"></div>中的is-with-icon未找到定义,故删除。

通过props.customClass自定义Notification组件的类名

测试

it('customize the name of the class', () => {
    const customClass = 'foo'
    const wrapper = mount(Notification, {
        props: {
            customClass
        }
    })
    expect(wrapper.get('.el-notification').classes()).toContain(customClass)
})

代码

## template
<div :class="['el-notification', customClass]" role="alert"></div>

通过props.position自定义Notification组件的弹出位置

测试

it('position', () => {
    const position = 'bottom-right'
    const wrapper = mount(Notification, {
        props: {
            position
        }
    })
    expect(wrapper.find('.el-notification').classes()).toContain('right') 
})
    expect(wrapper.vm.verticalProperty).toBe('bottom')
    expect(wrapper.find('.el-notification').element.style.bottom).toBe('0px')

代码

## template
<div :class="['el-notification', customClass, horizontalClass]" :style="positionStyle" role="alert"></div>

## script
setup(props){
    const verticalOffsetVal = ref(props.verticalOffset)

    const horizontalClass = computed(() => {
        return props.position.endsWith('right') ? 'right' : 'left'
    })

    const verticalProperty = computed(() => {
        return props.position.startsWith('top') ? 'top' : 'bottom'
    })
    
    const positionStyle = computed(() => {
        return {
            [verticalProperty.value] : `${verticalOffsetVal.value}px`
        }
    })
    
    return {
        horizontalClass,
        verticalProperty,
        positionStyle,
        verticalOffsetVal
    }
}

重构这边时,发现源码中有定义verticalOffset,但文档中只提到了offset,并未提到verticalOffset,所以需要补测试,先主要看它影响了什么地方,然后就知道测些什么了

it('verticalOffset', () => {
    const verticalOffset = 50
    const wrapper = mount(Notification, {
      props: {
        verticalOffset
      }
    })
    expect(wrapper.vm.verticalProperty).toBe('top')
    expect(wrapper.find('.el-notification').element.style.top).toBe(
      `${verticalOffset}px`
    )
})
  
it('set both position and verticalOffset', () => {
    const position = 'bottom-right'
    const verticalOffset = 50
    const wrapper = mount(Notification, {
      props: {
        position,
        verticalOffset
      }
    })
    expect(wrapper.vm.verticalProperty).toBe('bottom')
    expect(wrapper.find('.el-notification').element.style.bottom).toBe(
      `${verticalOffset}px`
    )
})

it('update verticalOffset', async () => {
    const wrapper = mount(Notification, {
      props: {
        verticalOffset: 50
      }
    })
    expect(wrapper.find('.el-notification').element.style.top).toBe('50px')
    wrapper.vm.verticalOffsetVal = 100
    await flushPromises()
    expect(wrapper.find('.el-notification').element.style.top).toBe('100px')
})

通过props.showClose控制Notification组件是否显示关闭按钮

测试

it('set the showClose', () =>{
    const showClose = true
    const wrapper = mount(Notification, {
        props: {
            showClose
        }
    })
    expect(wrapper.find('.el-notification__closeBtn').exists()).toBe(true)
    expect(wrapper.find('.el-icon-close').exists()).toBe(true)
})

it('click close btn', async () => {
	const showClose = true
    const wrapper = mount(Notification, {
        props: {
            showClose
        }
    })
    const closeBtn = wrapper.get('.el-notification__closeBtn')
    await closeBtn.trigger('click')
    expect(wrapper.get('.el-notification').isVisible()).toBe(false)
})

代码

## template
<div :class="['el-notification', customClass, horizontalClass]" :style="positionStyle" role="alert" v-show="visible">
    // ... 
    <div class="el-notification__closeBtn el-icon-close" v-if="showClose" @click="handleClose"></div>
</div>

## script
import { ref, getCurrentInstance } from 'vue'
emits: ['close']
setup(props, { emit }){
    const instance = getCurrentInstance()
    const visible = ref(true)
    
    function _close() {
        emit('close', instance)
        visible.vue = false
    }
    
    function handleClose() {
        _close()
    }
    
    function close() {
       _close()
    }
    return {
        close,
        visible,
        handleClose
    }
}

通过props.duration控制Notification组件的显示时间(毫秒),若设置为0,则不会关闭

测试

it('set the duration to 1000, notification is closed when in 1000ms', async () => {
    jest.useFakeTimers()
    const wrapper = mount(Notification, {
        props: {
            duration: 1000
        }
    })
    jest.runTimersToTime(1000)
    await flushPromises()
    expect(wrapper.get('.el-notification').isVisible()).toBe(flase)
})

it('set the duration to 0, the notification does not turn off', async () => {
    jest.useFakeTimers()
    const wrapper = mount(Notification, {
        props: {
            duration: 0
        }
    })
    jest.runTimersToTime(5000)
    await flushPromises()
    expect(wrapper.get('.el-notification').isVisible()).toBe(true)
})

代码

## template
<div :class="['el-notification', customClass, horizontalClass]" :style="positionStyle" role="alert" v-show="visible" @mouseenter="handleMouseenter" @mouseleave="handleMouseleave">

## script
setup(props) {
    function _close() {
       // 关闭组件时,清除计时
       clearTimeout(timer)
       // ...
    }
    
    // ...

    let timer
    function delayClose() {
        if(props.duration >0) {
            timer = setTimeout(() => {
                _close()
            }, props.duration)
        }
    }
    
    function handleMouseenter() {
        clearTimeout(timer)
    }
    
    function handleMouseleave() {
        delayClose()
    }
    
    // 组件初始化,执行方法开始计时
    delayClose()
    
    return {
        // ...
        handleMouseenter,
        handleMouseleave
        
    }
}

覆盖率

覆盖率

总结

重构还未完,对Notification组件的扩展js还未写,目前写累了,下一篇继续~

在这次重构中学到了一个知识点,emit('close')会隐性调用OnClose函数。在任务分解这块还需要继续磨炼。好好学习,天天向上。