简介
举例上次重写Link组件已经过了14天了,先来说一下这14天我都做了些什么事吧
- 看《红宝书》第四版,因为在参与element3就开始看了,所以这14天中,花了半天时间,看完红宝书
- 看《深入理解ES6》,这本书呢,讲的比较通俗易懂,个人觉得,相较于阮一峰老师的《ES6入门教程》第三版阅读性好一些。
- 看《绿宝书》,目前看到第三章,这本书因为讲JavaScript的角度不一样,阅读起来会勾起浓厚的兴趣,但阅读也是有一定难度的,需要慢慢磨~
- 看《重构 改善既有代码的设计》跟 《10x程序员工作法》,看这两个呢,主要是参加element3的过程中,需要用到,也是element3负责人@春去春又来推荐我们看,重构由于是后面买的,所以才看到第一章,《10x程序员工作法》呢,就比较容易了,可以用听的,这边主要重点听了任务分解的章节
- 重写Notification组件
好了,废话了那么多,开始这篇文章的正文
需求
这次相对于Link组件,Notification组件比较难一点,我们还是老规矩,先列Tasking,从Tasking看,就比Link组件的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函数。在任务分解这块还需要继续磨炼。好好学习,天天向上。