[element3-Дневник разработки] Научу переписывать компонент Button касанием рук

внешний интерфейс

Переопределить компонент «Кнопка»

задний план

У меня может быть одноклассник спросит нас, почему мы хотим переписать компонент?

По сути, логика реализации текущих компонентов element3 принудительно переписана с API-интерфейсов опций на API-интерфейсы композиции.

Организация кода беспорядочная, нечитаемая, ремонтопригодная и расширяемая.

Тогда некоторые студенты могут спросить, почему бы не восстановить исходную логику?

Честно говоря, исходная логика действительно запутана, и это даже повлияет на ваше мышление.

Так что давайте смелее и перепишем

В этой статье в основном подробно описаны метод и этапы рефакторинга компонента Button.

В основном, чтобы дать студентам, которые хотят внести свой исходный код, представление о переписывании компонентов.

Содержание этой статьи очень сухое, и это может быть весь код. Пожалуйста, прочитайте внимательно

обработать

Переписывание компонента, вероятно, будет разделено на следующие пункты

  • Подтвердить потребности
  • Tasking
  • Tdd
  • snapshot

Давайте посмотрим по очереди

нужно

Прежде чем переписывать, давайте определим, что мы хотим переписать, чтобы удовлетворить наши потребности.

Прежде всего, внешний интерфейс нельзя модифицировать, например:

  • props
  • emits
  • slots

Это все внешние интерфейсы, которые должны соответствовать исходной логике.

Тогда наша логика состоит в том, чтобы использовать API композиции для реализации

Наконец, что более важно, необходимо убедиться, что уровень охвата юнит-тестами превышает 90%.

Ну вот что нам нужно для переписывания компонента

Tasking

В соответствии с идеей начать с конца, нам нужно сначала определить, какие функции есть у кнопки, Давайте перечислим их одну за другой.

На самом деле, если мы посмотрим документацию по кнопке на официальном сайте element, мы узнаем, в чем заключается функция кнопки.

список функций

  • Размер кнопки может быть установлен на основе свойства размера.
  • Тип кнопки может быть установлен на основе атрибута типа
    • Разные типы, стиль кнопки непоследовательный
  • Является ли кнопка простой кнопкой, можно установить на основе простого свойства.
    • Простые кнопки на самом деле являются изменением стиля
  • Является ли кнопка закругленной кнопкой, можно установить на основе свойства round.
    • это тоже смена стиля
  • Является ли кнопка круглой кнопкой, можно установить на основе свойства круга.
    • или изменение стиля
  • На основе атрибута загрузки вы можете указать, находится ли кнопка в состоянии загрузки.
    • Если установлена ​​загрузка, будет отображаться значок «загрузка», который будет отображаться всегда
  • На основе отключенного атрибута вы можете установить, отключена ли кнопка.
    • Есть изменение в стиле, чтобы показать отключенный значок
    • не кликабельно
  • Значок, отображаемый на кнопке, можно установить на основе свойства значка.
  • Основываясь на атрибуте автофокуса, вы можете установить, будет ли кнопка сфокусирована по умолчанию.
  • Атрибут собственного типа кнопки можно установить на основе атрибута собственного типа.

В дополнение к этим функциональным точкам на поверхности на самом деле есть несколько более подробных функциональных точек, таких как:

  • Если он находится в состоянии загрузки, значок, установленный значок, не может отображаться.
    • То есть компонент может отображать только один значок.
      • Либо загрузка, либо установленная иконка
  • В состоянии загрузки компонент не может быть нажат
  • Может быть три точки для управления размером кнопки.
    • собственный реквизит
    • Когда родительский элемент FormItem, вы можете получить размер элемента
    • Размер можно установить через глобальную конфигурацию
  • Может быть две точки для управления отключением кнопки
    • собственный реквизит
    • Form.disabled также может контролировать, когда родителем является Form.
    • Вышеуказанные два пункта, если один из них верен, кнопка не будет отображаться.
  • Пользователь может определить содержимое компонента через слот

Хорошо, наконец-то я перечислил все предыдущие функции Кнопки.На самом деле переписывание компонента является самым критичным моментом.Только после того, как этот шаг будет сначала сглажен, потом будет гладко писать.

Моя собственная привычка — перечислять все задачи

Когда вы выполните задачу позже, выберите

Есть ощущение, что играешь в игры и выполняешь задания.Каждый раз, когда проверяешь опыт, получаешь +1

Конечно, я называю это «прогрессом, который вы видите».

Таким образом, вы будете знать, как далеко вы от завершения функции

TDD

Некоторые студенты могут спросить, что такое TDD? Я не буду освещать здесь популярную науку, заинтересованные студенты могут учиться на Baidu.

Вот краткое введение в TDD в качестве метода программирования

  • Сначала напишите неудачный тест
  • Затем просто напишите логику, чтобы этот неудачный тест прошел.
  • рефакторинг

Вопрос в том, что мы пишем модульные тесты для тестирования? На самом деле точки, которые мы хотим протестировать, уже перечислены на шаге Tasking.

Эта глава на самом деле включает в себя множество небольших шагов рефакторинга, и описывать их все — пустая трата времени, поэтому я использую форму вставки кода для повышения эффективности.

Пользователь может определить содержимое компонента через слот

Сначала найдите самую простую функцию для реализации, это самая простая

Сначала найдите мягкую хурму

тестовое задание

import Button from '../src/Button.vue'
import { mount } from '@vue/test-utils'
describe('Button.vue', () => {
  it('should show content', () => {
    const content = 'foo'

    const wrapper = mount(Button, {
      slots: {
        default: content
      }
    })

    expect(wrapper.text()).toContain(content)
  })
})

Код

<template>
 <button>
   <slot></slot>
 </button>
</template>

<script>
export default {
  setup() {
    return {}
  }
}
</script>

Размер кнопки может быть установлен на основе свойства размера.

тестовое задание

  describe('set button size', () => {
    it.only('by props.size', () => {
      const size = 'small'

      const wrapper = mount(Button, {
        props: {
          size
        }
      })
			
      expect(wrapper.classes()).toContain(`el-button--${size}`)
    })
  })

Использование метода утверждения toContain может помочь нам распечатать классы, которые в настоящее время принадлежат оболочке, когда тест не пройден, что является методом написания теста, который более удобен для отладки.

Код

<template>
    <button
    class="el-button"
    :class="[
      buttonSize ? `el-button--${size}` : ''
    ]"
  >
    <slot></slot>
  </button>
</template>

<script>
import { toRefs } from 'vue'
export default {
  props: {
	size: {
      type: String,
      validator(val) {
        if(val === "") return true
        return ['medium', 'small', 'mini'].indexOf(val) !== -1
      }
    },
  }
}
</script>

Здесь реализована проверка размера реквизита

Установите размер кнопки на основе elFormItem.elFormItemSize

тестовое задание

    it('by elFormItem.elFormItemSize', () => {
      const size = 'small'
      const wrapper = mount(Button, {
        global: {
          provide: {
            elFormItem: reactive({
              elFormItemSize: size
            })
          }
        }
      })

      expect(wrapper.classes(`el-button--${size}`)).toBeTruthy()
    })

Код

<template>
  <button
    class="el-button"
    :class="[
      buttonSize ? `el-button--${buttonSize}` : '',
    ]"
  >
    <slot></slot>
  </button>
</template>
<script>
import { toRefs, inject, computed } from 'vue'
export default {
  props: [
   	size: {
      type: String,
      validator(val) {
  		if (val === '') return true
        return ['medium', 'small', 'mini'].indexOf(val) !== -1
      }
    },
  ],
  setup(props) {
    const { size } = toRefs(props)

    const buttonSize = useButtonSize(size)

    return {
      buttonSize
    }
  }
}

const useButtonSize = (size) => {
  return computed(() => {
    const elFormItem = inject('elFormItem', {})
    return size?.value || elFormItem.elFormItemSize
  })
}
</script>

Из-за теста, чтобы убедиться, что рефакторинг также очень уверен

Установите размер кнопки на основе размера глобальной конфигурации

тестовое задание

    it('by global config ', () => {
      const size = 'small'
      const wrapper = mount(Button, {
        global: {
          config: {
            globalProperties: {
              $ELEMENT: {
                size
              }
            }
          }
        }
      })

      expect(wrapper.classes()).toContain(`el-button--${size}`)
    })

Код

const useButtonSize = (size) => {
  return computed(() => {
    const elFormItem = inject('elFormItem', {})
    return (
      size?.value ||
      elFormItem.elFormItemSize ||
      getCurrentInstance().ctx.$ELEMENT?.size
    )
  })
}

Мы справились с задачей размера

Тип кнопки может быть установлен на основе атрибута типа

тестовое задание

  it('set button type by prop type ', () => {
    const type = 'success'

    const wrapper = mount(Button, {
      props: {
        type
      }
    })

    expect(wrapper.classes()).toContain(`el-button--${size}`)
  })

Код

<template>
  <button
    class="el-button"
    :class="[
      buttonSize ? `el-button--${buttonSize}` : '',
      type ? `el-button--${type}` : ''
    ]"
  >
    <slot></slot>
  </button>
</template>
<script>
export default {
  props: {
    size: {
      type: String,
      validator(val) {
        if (val === '') return true
        return ['medium', 'small', 'mini'].indexOf(val) !== -1
      }
    },
    type: {
      type: String,
      validator(val) {
        return (
          ['primary', 'success', 'warning', 'danger', 'info', 'text'].indexOf(
            val
          ) !== -1
        )
      }
    }
  }
</script>

Используйте класс для управления стилем типа отображения

Является ли кнопка простой кнопкой, можно установить на основе простого свойства.

тестовое задание

  it('set button plain by prop type', () => {
    const wrapper = mount(Button, {
      props: {
        plain: true
      }
    })

    expect(wrapper.classes()).toContain(`is-plain`)
  })

Код

<template>
  <button
    class="el-button"
    :class="[
      buttonSize ? `el-button--${buttonSize}` : '',
      type ? `el-button--${type}` : '',
      {
        'is-plain': plain
      }
    ]"
  >
    <slot></slot>
  </button>
</template>
<script>
	...
  props:{
    plain: Boolean
  }
  ...
</script>

Является ли кнопка закругленной кнопкой, можно установить на основе свойства round.

тестовое задание

  it('set button round by prop type', () => {
    const wrapper = mount(Button, {
      props: {
        round: true
      }
    })

    expect(wrapper.classes()).toContain(`is-round`)
  })

Код

<template>
  <button
    class="el-button"
    :class="[
      buttonSize ? `el-button--${buttonSize}` : '',
      type ? `el-button--${type}` : '',
      {
        'is-plain': plain,
        'is-round': round
      }
    ]"
  >
    <slot></slot>
  </button>
</template>
<script>
  ……
	props:{
  	  round:Boolean
	}
  ……
</script>

Просто добавьте класс

Является ли кнопка круглой кнопкой, можно установить на основе свойства круга.

тестовое задание

  it('set button circle by prop type', () => {
    const wrapper = mount(Button, {
      props: {
        circle: true
      }
    })

    expect(wrapper.classes()).toContain(`is-circle`)
  })

Код

<template>
...
      {
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle
      }
...
  >
</template>

<script>
  ……
  	props:{
       circle: Boolean
    }
  ……
</script>

Установив загрузку, пусть кнопка показывает состояние загрузки

Если он находится в состоянии загрузки, кнопка не должна быть нажата и отображать значок загрузки.

тестовое задание

  it('set button loading by prop loading', async () => {
    const wrapper = mount(Button, {
      props: {
        loading: true
      }
    })
    
    expect(wrapper.classes()).toContain(`is-loading`)
    expect(wrapper.attributes()).toHaveProperty('disabled')
  })

Здесь вам нужно только проверить, есть ли отключенный атрибут на кнопке

Код

<template>
...
	:disabled="loading"
	:class="[
      {
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle,
   		'is-loading': loading
      }
		]
    <i class="el-icon-loading" v-if="loading"></i>
	<slot></slot>
...
  >
</template>
<script>
  export default {
     	props:{
     		 loading: Boolean
    	} 
  }
}
</script>

На основе отключенного атрибута вы можете установить, отключена ли кнопка.

тестовое задание

  describe('set button disabled', () => {
    it('by props.disabled', () => {
      const wrapper = mount(Button, {
        props: {
          disabled: true
        }
      })

      expect(wrapper.classes()).toContain(`is-disabled`)
      expect(wrapper.attributes()).toHaveProperty('disabled')
    })
  })

Поскольку disabled включает в себя 2 функциональные точки, одна управляется реквизитами, а другая управляется формой родительского компонента, поэтому мы используем описание для организации тестов.

Тест здесь немного отличается от предыдущего, не только для проверки имени класса is-disabled, нам также нужно установить disabled компонента, чтобы компонент был недействительным.

Код

<template>

  <button
    class="el-button"
    :disabled="disabled || loading"
    :class="[
      buttonSize ? `el-button--${buttonSize}` : '',
      type ? `el-button--${type}` : '',
      {
        'is-disabled': disabled
      }
    ]"
  ></template>
<script>
props:{
  disabled: Boolean
}
</script>

Если родительский компонент не "От", а для параметра "отключен" установлено значение "истина", текущий компонент также будет затронут.

тестовое задание

    it('by elForm.disable', () => {
      const wrapper = mount(Button, {
        global: {
          provide: {
            elForm: reactive({
              disabled: true
            })
          }
        }
      })

      expect(wrapper.classes()).toContain(`is-disabled`)
      expect(wrapper.attributes()).toHaveProperty('disabled')
    })

Код

<template>
  <button
    class="el-button"
    :disabled="buttonDisabled || loading" 
    :class="[
      buttonSize ? `el-button--${buttonSize}` : '',
      type ? `el-button--${type}` : '',
      {
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle,
        'is-loading': loading,
        'is-disabled': buttonDisabled
      }
    ]"
  >
    <slot></slot>
  </button>
</template>
<script>

  setup(props){
    const { size, disabled } = toRefs(props)
    const buttonDisabled = useButtonDisabled(disabled)

    return {
   		...
	    buttonDisabled
    }
  }
  
  const useButtonDisabled = (disabled) => {
  return computed(() => {
    const elForm = inject('elForm', {})

    return disabled?.value || elForm.disabled
  })
}

</script>

Значок, отображаемый на кнопке, можно установить на основе свойства значка.

тестовое задание

  it('set button icon by props.icon', () => {
    const wrapper = mount(Button, {
      props: {
        icon: 'el-icon-edit'
      }
    })

    expect(wrapper.find('.el-icon-edit').exists()).toBe(true)
  })

Обнаружение элемента присутствия с необходимостью найти + существует с использованием

Код

<template>
		……
    + <i :class="icon" v-if="icon"></i>
  </button>
</template>
<script>
props:{
  icon:String
}
</script>

Продолжаем, у нас еще логика, если отображается загрузка, то иконка может не отображаться

Если он находится в состоянии загрузки, значок, установленный значком, не может отображаться.

тестовое задание

    it("don't show icon when loading eq true", () => {
      const wrapper = mount(Button, {
        props: {
          icon: 'el-icon-edit',
          loading: true
        }
      })

      expect(wrapper.find('.el-icon-edit').exists()).toBe(false)
      expect(wrapper.find('.el-icon-loading').exists()).toBe(true)
    })

Код

<template>
	……
   <i class="el-icon-loading" v-if="loading"></i>
   <i :class="icon" v-else-if="icon"></i>
	……
</template>

Это также очень просто реализовать, потому что загрузка и значок могут хранить только одно, поэтому мы можем использовать v-else-if для достижения этого.

Основываясь на атрибуте автофокуса, вы можете установить, будет ли кнопка сфокусирована по умолчанию.

Это на самом деле не требуется, и это будет добавлено во внутреннюю кнопку при настройке Автофокуса снаружи.

<Button autofocus></Button>

Атрибут собственного типа кнопки можно установить на основе атрибута собственного типа.

тестовое задание

  it('set native-type by props.native-type', () => {
    const nativeType = 'reset'

    const wrapper = mount(Button, {
      props: {
        nativeType
      }
    })

    expect(wrapper.attributes('type')).toBe(nativeType)
  })

Код

<template>
	<button
  	:type="nativeType"        
  >
    
  </button>
</template>
<script>
	props:{
  	  nativeType:String
	}
</script>

рефакторинг

Перед рефакторингом

<template>
  <button
    class="el-button"
    :type="nativeType"
    :disabled="buttonDisabled || loading"
    :class="[
      buttonSize ? `el-button--${buttonSize}` : '',
      type ? `el-button--${type}` : '',
      {
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle,
        'is-loading': loading,
        'is-disabled': buttonDisabled
      }
    ]"
  >
    <i class="el-icon-loading" v-if="loading"></i>
    <i :class="icon" v-else-if="icon"></i>
    <slot></slot>
  </button>
</template>

<script>
import { toRefs, inject, computed, getCurrentInstance } from 'vue'
export default {
  props: {
    size: {
      type: String,
      validator(val) {
        if (val === '') return true
        return ['medium', 'samll', 'mini'].indexOf(val) !== -1
      }
    },
    type: {
      type: String,
      validator(val) {
        return (
          ['primary', 'success', 'warning', 'danger', 'info', 'text'].indexOf(
            val
          ) !== -1
        )
      }
    },
    plain: Boolean,
    round: Boolean,
    circle: Boolean,
    loading: Boolean,
    disabled: Boolean,
    icon: String,
    nativeType: String
  },
  setup(props) {
    const { size, disabled } = toRefs(props)

    const buttonSize = useButtonSize(size)
    const buttonDisabled = useButtonDisabled(disabled)

    return {
      buttonSize,
      buttonDisabled
    }
  }
}

const useButtonDisabled = (disabled) => {
  return computed(() => {
    const elForm = inject('elForm', {})

    return disabled?.value || elForm.disabled
  })
}

const useButtonSize = (size) => {
  return computed(() => {
    const elFormItem = inject('elFormItem', {})
    return (
      size?.value ||
      elFormItem.elFormItemSize ||
      getCurrentInstance().ctx.$ELEMENT?.size
    )
  })
}
</script>

Мне не нравится, что все классы обрабатываются в шаблоне, поэтому я должен реорганизовать этот логический момент.

Благодаря модульному тестированию я могу с уверенностью проводить рефакторинг

после рефакторинга

<template>
  <button
    class="el-button"
    :class="classes"
    :type="nativeType"
    :disabled="buttonDisabled || loading"
  >
    <i class="el-icon-loading" v-if="loading"></i>
    <i :class="icon" v-else-if="icon"></i>
    <slot></slot>
  </button>
</template>

<script>
import { toRefs, inject, computed, getCurrentInstance } from 'vue'
export default {
  name: 'ElButton',
  props: {
    size: {
      type: String,
      validator(val) {
        if (val === '') return true
        return ['large', 'medium', 'small', 'mini'].indexOf(val) !== -1
      }
    },
    type: {
      type: String,
      validator(val) {
        return (
          ['primary', 'success', 'warning', 'danger', 'info', 'text'].indexOf(
            val
          ) !== -1
        )
      }
    },
    nativeType: {
      type: String,
      default: 'button'
    },
    plain: Boolean,
    round: Boolean,
    circle: Boolean,
    loading: Boolean,
    disabled: Boolean,
    icon: String
  },
  setup(props) {
    const { size, disabled } = toRefs(props)

    const buttonSize = useButtonSize(size)
    const buttonDisabled = useButtonDisabled(disabled)
    const classes = useClasses({
      props,
      size: buttonSize,
      disabled: buttonDisabled
    })

    return {
      buttonDisabled,
      classes
    }
  }
}

const useClasses = ({ props, size, disabled }) => {
  return computed(() => {
    return [
      size.value ? `el-button--${size.value}` : '',
      props.type ? `el-button--${props.type}` : '',
      {
        'is-plain': props.plain,
        'is-round': props.round,
        'is-circle': props.circle,
        'is-loading': props.loading,
        'is-disabled': disabled.value
      }
    ]
  })
}

const useButtonDisabled = (disabled) => {
  return computed(() => {
    const elForm = inject('elForm', {})

    return disabled?.value || elForm.disabled
  })
}

const useButtonSize = (size) => {
  return computed(() => {
    const elFormItem = inject('elFormItem', {})
    return (
      size?.value ||
      elFormItem.elFormItemSize ||
      getCurrentInstance().ctx.$ELEMENT?.size
    )
  })
}
</script>

Пока что все наши задачи выполнены, не знаю, чувствуете ли вы это, на самом деле мы каждый раз фокусируемся только на небольшой функции, и реализовать ее очень просто.

Логика компонента завершена, теперь осталось посмотреть стиль компонента

добавить снимок

На самом деле перед добавлением снапшота нам нужно вручную проверить стиль компонента, ведь мы даже не видели UI в процессе TDD.

Снимок теста

  it('snapshot', () => {
    const wrapper = mount(Button)
    expect(wrapper.element).toMatchSnapshot()
  })

Тест снапшота очень прост, после написания нескольких строчек кода jest поможет нам сгенерировать снапшот текущего компонента.

// button/tests/_snapshots__/Button.spec.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Button.vue snapshot 1`] = `
<button
  class="el-button"
  type="button"
>
  <!--v-if-->
  
  
</button>
`;

## тестовое покрытие

Наконец, исходя из наших потребностей, мы должны достичь 90% покрытия тестами.

Посмотрим, какое у нас покрытие сейчас

Выполните следующую команду

yarn test packages/button/tests/Button.spec.js --coverage

Вы можете увидеть следующие результаты

PASS  packages/button/tests/Button.spec.js
  Button.vue
     snapshot (20 ms)
     should show content (10 ms)
     set button type by prop type  (2 ms)
     set button plain by prop type (2 ms)
     set button round by prop type (2 ms)
     set button circle by prop type (2 ms)
     set button loading by prop loading (2 ms)
     set button loading by prop loading (2 ms)
     set native-type by props.native-type (2 ms)
    set button size
       by props.size (3 ms)
       by elFormItem.elFormItemSize (1 ms)
       by global config  (2 ms)
    set button disabled
       by props.disabled (2 ms)
       by elForm.disable (1 ms)
    set button icon
        by props.icon (6 ms)
       don't show icon when loading eq true (2 ms)

-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |      100 |     100 |     100 |
 src             |     100 |      100 |     100 |     100 |
  Button.vue     |     100 |      100 |     100 |     100 |
 tests           |     100 |      100 |     100 |     100 |
  Button.spec.js |     100 |      100 |     100 |     100 |
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       16 passed, 16 total
Snapshots:   1 passed, 1 total
Time:        3.359 s

100% тестовое покрытие

Поскольку мы разрабатываем с помощью TDD, достижение 100% покрытия тестами является обычным делом.

Суммировать

Все вышесказанное посвящено переписыванию компонента Button, небольшое резюме.

Нам нужно сначала определить функцию компонента

Затем реализуйте его по крупицам на основе TDD.

В итоге мы получим компонент со 100% тестовым покрытием.

Даже если функция находится в сложном компоненте, она реализуется маленькой функцией, в процессе TDD мы фактически уменьшаем умственную нагрузку, давайте заботимся только о реализации маленькой функции, а из-за гарантии тестирования , мы всегда можем провести рефакторинг

Все компоненты element3 в дальнейшем также будут переписаны вышеописанным образом.

Для максимального качества кода, конечно же, это и для расширения последующих новых фич

Последующие статьи упростят шаги TDD, потому что это слишком много хлопот! ! !


  • это мыКоманда Хуагошаньпроект с открытым исходным кодомelement3
  • Библиотека компонентов внешнего интерфейса, поддерживающая vue3.