【element3-小哈喽参与日记】被摸手后,自己重写Link组件

1,855 阅读3分钟

背景

在参加完开课吧的web全栈课程途中,得知了开课吧团队(现花果山团队:hug-sun)fork了一份element-ui,帮助开发者学习vue3、组件化后,我就偷摸混进了他们的内部群里。

流程

具体流程可以参考@春去春又来手摸手教程 PS:就是他摸的我。

需求

我这个小菜鸟,当然是找简单的组件,开始捏软柿子啦。这不就盯上了Link组件! 功能列表

TDD

通过slot的方式,定义Link组件内容

测试

it('should show content', () => {
    const content = 'Link'
    const wrapper = mount(Link,{
        slots: {
            default: content
        }
    }
    expect(wrapper.text()).toContain('Link')
})

代码实现

<template>
    <a>
        <span>
            <slot></slot>
        </span>
    </a>

</template>
<script>
export default {
    name: 'ElLink'
}
</script>
<style>
</style>

通过props.type控制Link组件的主题样式,可选值[primary/success/warning/danger/info]

测试

it('set the type, link displays the corresponding style', () => {
    const type = 'primary'
    const wrapper = mount(Link,{
        props: {
            type
        }
    }
    expect(wrapper.classes()).toContain(`el-link--${type}`)
})

代码实现

<template>
    <a :class="classes">
        <span>
            <slot></slot>
        </span>
    </a>

</template>
<script>
export default {
    name: 'ElLink',
    props: {
        type: {
            type: String,
            default: 'default'
        }
    },
    setup(props){
        const classes = useClasses(props)
        return {
            classes
        }
    }
}
const useClasses = (props) => {
    return [
        props.type ? `el-link--${props.type}` : ''
    ]
}
    
</script>
<style>
</style>

通过props.disabled控制Link组件是否禁用

测试

it('set the disabled, link displays the corresponding style', () => {
    const disabled = true
    const wrapper = mount(Link, {
        props: {
            disabled
        }
    })
    expect(wrapper.classes()).toContain('is-disabled')
    
})

代码实现

<template>
    <a :class="classes">
        <span>
            <slot></slot>
        </span>
    </a>

</template>
<script>
export default {
    name: 'ElLink',
    props: {
        type: {
            type: String,
            default: 'default'
        },
        disabled: {
            type: Boolean,
            default: false
        }
    },
    setup(props){
        const classes = useClasses(props)
        return {
            classes
        }
    }
}
const useClasses = (props) => {
    return [
        props.type ? `el-link--${props.type}` : '',
        props.disabled && 'is-disabled'
    ]
}
    
</script>
<style>
</style>

通过props.underline控制Link组件是否显示下划线

测试

it('set the underline, link displays the corresponding style', () => {
    const underline = true
    const wrapper = mount(Link, {
        props: {
            underline
        }
    })
    expect(wrapper.classes()).toContain('is-underline')
})

代码实现

<template>
    <a :class="classes">
        <span>
            <slot></slot>
        </span>
    </a>

</template>
<script>
export default {
    name: 'ElLink',
    props: {
        type: {
            type: String,
            default: 'default'
        },
        disabled: {
            type: Boolean,
            default: false
        },
        underline: {
            type: Boolean,
            default: true
        }
    },
    setup(props){
        const classes = useClasses(props)
        return {
            classes
        }
    }
}
const useClasses = (props) => {
    return [
        props.type ? `el-link--${props.type}` : '',
        props.disabled && 'is-disabled',
        props.underline && !props.disabled && 'is-underline'
    ]
} 
</script>
<style>
</style>

通过props.href制定Link组件的图片

测试

it('set the href', () =>{
    const href = 'https://element3-ui.com/'
    const wrapper = mount(Link, {
        props: {
            href
        }
    }
    expect(wrapper.attributes('href')).toBe(href)
})

代码实现

<template>
    <a :class="classes" :href=" disabled ? null : href" v-bind="$attrs">
        <span>
            <slot></slot>
        </span>
    </a>

</template>
<script>
export default {
    name: 'ElLink',
    props: {
        type: {
            type: String,
            default: 'default'
        },
        disabled: {
            type: Boolean,
            default: false
        },
        underline: {
            type: Boolean,
            default: true
        },
        href: String
    },
    setup(props){
        const classes = useClasses(props)
        return {
            classes
        }
    }
}
const useClasses = (props) => {
    return [
        props.type ? `el-link--${props.type}` : '',
        props.disabled && 'is-disabled',
        props.underline && !props.disabled && 'is-underline'
    ]
} 
</script>
<style>
</style>

通过props.icon指定Link组件的图片

测试

it('set the icon, link displays the corresponding style', () => {
    const icon = 'el-icon-search'
    const wrapper = mount(Link, {
        props: {
            icon
        }
    }
    const i = wrapper.find('i')
    expect(i.exists()).toBe(true)
    expect(i.classes()).toContain(icon)
})

代码实现

<template>
    <a :class="classes" :href=" disabled ? null : href" v-bind="$attrs">
        <i :class="icon"></i>
        <span>
            <slot></slot>
        </span>
    </a>

</template>
<script>
export default {
    name: 'ElLink',
    props: {
        type: {
            type: String,
            default: 'default'
        },
        disabled: {
            type: Boolean,
            default: false
        },
        underline: {
            type: Boolean,
            default: true
        },
        href: String,
        icon: String
    },
    setup(props){
        const classes = useClasses(props)
        return {
            classes
        }
    }
}
const useClasses = (props) => {
    return [
        props.type ? `el-link--${props.type}` : '',
        props.disabled && 'is-disabled',
        props.underline && !props.disabled && 'is-underline'
    ]
} 
</script>
<style>
</style>

用户可以自定义click事件,但组件设置禁用或href属性后,用户的自定义事件静默失效

测试

it('should captured click events emitted via click', () => {
    const wrapper = mount(Link)
    wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeTruthy()
})

it("when disabled prop is equal to true",() => {
    const wrapper = mount(Link, {
        props: {
            disabled: true
        }
    })
    wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeFalsy()
})

it('when href prop is to be truthy', () => {
    const wrapper = mount(Link, {
        props: {
            href: 'https://element3-ui.com/'
        }
    })
    wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeFalsy()
})

代码实现

<template>
    <a :class="classes" :href=" disabled ? null : href" v-bind="$attrs" @click="handleClick">
        <i :class="icon"></i>
        <span>
            <slot></slot>
        </span>
    </a>

</template>
<script>
export default {
    name: 'ElLink',
    props: {
        type: {
            type: String,
            default: 'default'
        },
        disabled: {
            type: Boolean,
            default: false
        },
        underline: {
            type: Boolean,
            default: true
        },
        href: String,
        icon: String
    },
    emits:['click'],
    setup(props, { emit }){
        const classes = useClasses(props)
        
        const handleClick = (event) => {
            if(props.disabled) return
            if(props.href) return
            
            emit('click', event)
        }
        return {
            classes,
            handleClick
        }
    }
}
const useClasses = (props) => {
    return [
        props.type ? `el-link--${props.type}` : '',
        props.disabled && 'is-disabled',
        props.underline && !props.disabled && 'is-underline'
    ]
} 
</script>
<style>
</style>

TDD环节挺干的,你以为到这里就结束了吗,还有一些细节需要整理,比如用户没有指定icon,i标签需要加载吗?下面列出完整的代码跟测试

完整代码

<template>
  <a
    :class="classes"
    :href="disabled ? null : href"
    v-bind="$attrs"
    @click="handleClick"
  >
    <i v-if="icon" :class="icon"></i>
    <span v-if="$slots.default" class="el-link--inner">
      <slot></slot>
    </span>
  </a>
</template>

<script>
export default {
  name: 'ElLink',
  props: {
    type: {
      type: String,
      default: 'default'
    },
    disabled: {
      type: Boolean,
      default: false
    },
    underline: {
      type: Boolean,
      default: true
    },
    href: String,
    icon: String
  },
  emits: ['click'],
  setup(props, { emit }) {
    const classes = useClasses(props)

    const handleClick = (event) => {
      if (props.disabled) return
      if (props.href) return

      emit('click', event)
    }
    return {
      classes,
      handleClick
    }
  }
}
const useClasses = (props) => {
  return [
    props.type ? `el-link--${props.type}` : '',
    props.disabled && 'is-disabled',
    props.underline && !props.disabled && 'is-underline'
  ]
}
</script>

<style></style>

完整测试代码

import { mount } from '@vue/test-utils'
import Link from '../Link.vue'
describe('Link', () => {
  describe('props', () => {
    it('initialize the Link component', () => {
      const wrapper = mount(Link)
      expect(wrapper.find('i').exists()).toBe(false)
      expect(wrapper.find('span').exists()).toBe(false)
    })
    it('should show content', () => {
      const content = 'Link'
      const wrapper = mount(Link, {
        slots: {
          default: content
        }
      })
      expect(wrapper.text()).toContain('Link')
    })

    it('set the type, link displays the corresponding style', () => {
      const type = 'primary'
      const wrapper = mount(Link, {
        props: {
          type
        }
      })
      expect(wrapper.classes()).toContain(`el-link--${type}`)
    })

    it('set the disabled, link displays the corresponding style', () => {
      const disabled = true
      const wrapper = mount(Link, {
        props: {
          disabled
        }
      })
      expect(wrapper.classes()).toContain('is-disabled')
    })

    it('set the underline, link displays the corresponding style', () => {
      const underline = true
      const wrapper = mount(Link, {
        props: {
          underline
        }
      })
      console.log(wrapper.vm)
      expect(wrapper.classes()).toContain('is-underline')
    })

    it('set the href', () => {
      const href = 'https://element3-ui.com/'
      const wrapper = mount(Link, {
        props: {
          href
        }
      })
      expect(wrapper.attributes('href')).toBe(href)
    })

    it('set the icon, link displays the corresponding style', () => {
      const icon = 'el-icon-search'
      const wrapper = mount(Link, {
        props: {
          icon
        }
      })
      const i = wrapper.find('i')
      expect(i.exists()).toBe(true)
      expect(i.classes()).toContain(icon)
    })

    it('should get target attr value', () => {
      const wrapper = mount(Link, {
        props: {
          target: '_blank'
        }
      })
      expect(wrapper.attributes('target')).toBe('_blank')
    })
  })

  describe('click', () => {
    it('should captured click events emitted via click', () => {
      const wrapper = mount(Link)
      wrapper.trigger('click')
      expect(wrapper.emitted('click')).toBeTruthy()
    })

    describe("can't captured click event emitted", () => {
      it('when disabled prop is equal to true', () => {
        const wrapper = mount(Link, {
          props: {
            disabled: true
          }
        })
        wrapper.trigger('click')
        expect(wrapper.emitted('click')).toBeFalsy()
      })

      it('when href prop is to be truthy', () => {
        const wrapper = mount(Link, {
          props: {
            href: 'https://element3-ui.com/'
          }
        })
        wrapper.trigger('click')
        expect(wrapper.emitted('click')).toBeFalsy()
      })
    })
  })
})

覆盖率

Link组件测试覆盖率

总结

在这次重构组件中,体验到了测试的魅力,在有测试的保障下,我写代码的时候,逻辑更清晰了,如有神助,代码出错也能迅速定位,并解决。今天下单了《重构 改善既有代码的设计(第2版)》,让我们学起来,奥利给