Vue.js, TypeScript и TSX в 2019 году

В этой статье мы поговорим о Vue, TypeScript и TSX(JSX под TypeScript)

Основным преимуществом использования.tsx — является проверка типа в функции render.

Вот так будет выглядеть приложение, которое мы с вами создадим:

img

Исходный код этой статьи доступен здесь.

Установка

Устанавливаем Vue CLI v3, затем создаём новое приложение, запустив vue create tsx_adder.

При установке выбираем:

  • Необходимые зависимости — TypeScript и Babel
  • Нам не нужен синтаксис компонента в стиле класса
  • Babel
  • Выбираем “in dedicated files”

Должно получится вот так:

установка babel и ts

Для использования TSX, нам понадобится зависимость — vue-tsx-support. После завершения установки заходим в папку tsx_adder командой cd tsx_adder. Устанавливаем пакет vue add tsx-support.

Структура

В приложении будет два компонента: App.tsx и Adder.tsx. App.tsx мы подключим к хранилищу Vuex и через входные параметры(props) будем передавать данные в Adder, который будет отвечать за разметку и пользовательский интерфейс. Согласно шаблону компоненты-представления, раньше его называли «умные» и «глупые» компоненты, лучше создать AdderContainer.tsx. Если кратко, компоненты-контейнеры отвечают за данные и операции с ними. Их состояние передается в виде свойств в компоненты-представления и отображается. Но для простоты, Adder.tsx будет получать данные из хранилища через входные параметры(props) и передавать данные родительскому компоненту через встроенный метод $emit. Это позволит нам продемонстрировать:

  • типизированные входные параметры(props), включая сложные типы, такие как Enums и Objects
  • проверку типов событий между родительским/дочерним компонентом
  • как получить тип интерфейс и тип безопасность с Vuex State в компонентах

Начнём с преобразования App.vue в App.tsx:

import * as tsx from 'vue-tsx-support'
import { VNode } from 'vue'

const App = tsx.component({
  name: 'App',

  render(): VNode {
    return (
      <Adder />
    )
  }
})

export { App }

Создадим файлAdder.tsx: components/Adder.tsx. Внутри components/Adder.tsx добавляем компонент:

import * as tsx from 'vue-tsx-support'
import { VNode } from 'vue'

const Adder = tsx.component({
  name: 'Adder',

  render(): VNode {
    return (
      <div>Adder</div>
    )
  }
})

export { Adder }

Теперь импортируем его вApp.tsx:
import { Adder } from './components/Adder'. Направляемся в main.tsи изменяем import App from './App.vue на import { App } from './App'. Запускаемyarn serve (илиnpm run serve). localhost:8080должно получить:

img

Типизированные входные параметры(props)

Первое, что мы рассмотрим, это типизированные входные параметры(props), включающие как примитивные типы — Number и Boolean, так и сложные, например Enum. В Adder.tsx, добавляем следующее:

props: {
  left: {
    type: Number,
    required: true as true
  },

  right: {
    type: Number,
    required: true as true
  }
},

true as true необходим TypeScriptу для проверки реквизитов во время компиляции. Здесь это кратко описано. Если ваш редактор поддерживает TypeScript (например VS Code), в App.tsx мы увидим ошибку, под Adderпоявится красная линия. К концу сообщения об ошибке, говорится Type '{}' is missing the following properties from type '{ left: number; right: number; }': left, right. Давайте добавим leftиrightвApp.tsx:

render(): VNode {
  return (
    <Adder 
      left={5}
      right={3}
    />
  )
}

Если вместо этого передать строку — TypeScript предупредит нас, что тип реквизита неверен.

Далее добавим более сложный типenum. Создаём каталог с именем types в src, а внутри него файлsign.ts:

enum Sign {
  'x' = 'x',
  '/' = '/',
  '+' = '+',
  '-' = '-'
}

export { Sign }

Обновляем Adder.tsx

import { Sign } from '@/types/sign'

// ...

props: {
  selectedSign: {
    type: String as () => Sign,
    required: true as true
  }
}

Еще один хак, который показывает некоторые ограничения поддержки Vue TS String as () => Sign. Поскольку перечисление Sign это строки, мы вводим String as () => .... Если бы это было перечисление Object или Array, мы бы ввели Array as () => MyComplexArrayType[]. Здесь об этом можно прочитать подробнее.

Возвращаемся в App.tsx, и видим еще одну ошибку в <Adder />. Исправляем:

// ...
import { Sign } from '@/types/sign'

// ...

  render(): VNode {
    return (
      <Adder 
        left={5}
        right={3}
        selectedSign={Sign['+']}
      />
    )
  }

Типизированные события

Теперь давайте посмотрим, как проверить тип события. Мы хотим, чтобы калькулятор вызывал событие changeSign при клике на любой из четырех знаков. Для этого используемcomponentFactoryOf, документация здесь. Начнем с обновления App.tsx:

// imports...
const App = tsx.component({
  name: 'App',

  methods: {
    changeSign(sign: Sign) {

    }
  },

  render(): VNode {
    return (
      <Adder 
        left={5}
        right={3}
        selectedSign={Sign['+']}
        onChangeSign={this.changeSign}
      />
    )
  }
})

export { App }

<Adder />снова ошибка:Property 'onChangeSign' does not exist on type '({ props: .... Это потому, что мы передаем параметр(props), который Adder не ожидает.

Добавляем в Adder.tsx:

interface IEvents {
  onChangeSign: (sign: Sign) => void
}

const Adder = tsx.componentFactoryOf<IEvents>().create({

  // ...
})

Теперь ошибка исчезла. Попробуйте изменитьchangeSign(sign: Sign)наchangeSign(sign: Number) — TS предупреждает, что параметр имеет неправильный тип. Подробнее о componentFactoryOfздесь.

Для завершения нашего компонента Adder.tsx добавим интерфейс, и функцию data:

// imports ...
interface IAdderData {
  signs: Sign[]
}

const Adder = tsx.componentFactoryOf<IEvents>().create({
  // ...
  
  data(): IAdderData {
    return {
      signs: [
        Sign["+"],
        Sign["-"],
        Sign["x"],
        Sign["/"]
      ]
    }
  }

Наконец, добавим функцию render для Adder.tsx.

render(): VNode {
  const { signs, left, right, selectedSign } = this

  return (
    <div class='wrapper'>
      <div class='inner'>
        <div class='number'>
          {left}
        </div>

        <div class='signs'>
          {signs.map(sign =>
            <span 
              class={sign === selectedSign ? 'selected sign' : 'sign'}
              onClick={() => this.$emit('changeSign', sign)}
            >
              {sign}
            </span>)
          }
        </div>

        <div class='number'>
          {right}
        </div>
      </div>

      <div class='result'>
        <span>
          Result: {this.$slots.result}
        </span>
      </div>
    </div>
  )
}

Маленькая оговорка: мы определяем интерфейс события как onChangeSign, но выделяем changeSign.

Чтобы приложение выглядело получше, добавим немного стилей. Создаем components/adder.cssи вставляем следующее:

.wrapper, .signs {
  display: flex;
  flex-direction: column;
  width: 200px;
}

.signs {
  align-items: center;
}

.sign {
  cursor: pointer;
  border: 2px solid rgba(100, 100, 20, 0.4);
  padding: 5px;
  width: 30px;
  height: 30px;
  margin: 5px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.selected {
  background-color: rgba(0, 0, 255, 0.4);
}

.inner {
  display: flex;
  justify-content: space-between;
}

.number {
  font-size: 2.5em;
}

.result {
  font-size: 3em;
}

Затемimport './adder.css'сверхуAdder.tsx. Теперь страница выглядит вот так:

img

Добавляем Vuex и кнопки начинают работать.

Добавление типизированного Vuex хранилища

Создаем папкуstoreвнутриsrc, затем вstoreсоздаемindex.tsиcalculation.ts. Внутриstore/index.tsдобавляем слудующее:

iimport Vue from 'vue'
import Vuex from 'vuex'

import { calculation, ICalculationState } from './calculation'

Vue.use(Vuex)

interface IState {
  calculation: ICalculationState
}

const store = new Vuex.Store<IState>({
  modules: {
    calculation
  }
})

export { store, IState } 

Определяем новое Veux хранилище и передаем модульcalculation. В calculation.ts добавляем следующее:

import { Module } from 'vuex'

interface ICalculationState {
  left: number
  right: number
}

const calculation: Module<ICalculationState, {}> = {
  state: {
    left: 3,
    right: 1
  }
}

export {
  calculation,
  ICalculationState
}

Определяем модуль calculation со значениями left и right. Импортируем его в main.ts:

import Vue from 'vue'
import { App } from './App'
import { store } from '@/store'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

ОбновляемApp.tsx:

import * as tsx from 'vue-tsx-support'
import { VNode } from 'vue'
import { Adder } from './components/Adder'

import { Sign } from '@/types/sign'
import { IState } from '@/store'

const App = tsx.component({
  name: 'App',

  computed: {
    left(): number {
      return (this.$store.state as IState).calculation.left
    },

    right(): number {
      return (this.$store.state as IState).calculation.right
    }
  },

  methods: {
    changeSign(sign: Sign) {

    }
  },

  render(): VNode {
    return (
      <Adder 
        left={this.left}
        right={this.right}
        selectedSign={Sign['+']}
        onChangeSign={this.changeSign}
      />
    )
  }
})

export { App }

Добавляем (this.$store.state as IState) для проверки типа модулей. Есть и другие альтернативы, которые позволят вам проверить тип без приведения this.$state в State, но мы будем использовать этот шаблон.

Добавление мутации

Нам необходимо сохранить selectedSign в хранилище и обновить его мутацией. Обновляем calculation.ts:

import { Module } from 'vuex'

import { Sign } from '@/types/sign'

interface ICalculationState {
  left: number
  right: number
  sign: Sign
}

const SET_SIGN = 'SET_SIGN'

const calculation: Module<ICalculationState, {}> = {
  namespaced: true,

  state: {
    left: 3,
    right: 1,
    sign: Sign['+']
  },

  mutations: {
    [SET_SIGN](state, payload: Sign) {
      state.sign = payload
    }
  }
}

export {
  calculation,
  ICalculationState
}

Мы добавили мутацию SET_SIGN и будем использовать ее вApp.tsx:

// ...
  methods: {
    changeSign(sign: Sign) {
      this.$store.commit('calculation/SET_SIGN', sign)
    }
  },
// ...

Давайте доделаем наше приложение. В App.tsx добавим вычисляемое свойство(computed property) result для вычисления результата:

import * as tsx from 'vue-tsx-support'
import { VNode } from 'vue'
import { Adder } from './components/Adder'

import { Sign } from '@/types/sign'
import { IState } from '@/store'

const App = tsx.component({
  name: 'App',

  computed: {
    left(): number {
      return (this.$store.state as IState).calculation.left
    },

    right(): number {
      return (this.$store.state as IState).calculation.right
    },

    sign(): Sign {
      return (this.$store.state as IState).calculation.sign
    },

    result(): number {
      switch (this.sign) {
        case Sign['+']:
          return this.left + this.right
        case Sign['-']:
          return this.left - this.right
        case Sign['x']:
          return this.left * this.right
        case Sign['/']:
          return this.left / this.right
      }
    }
  },

  methods: {
    changeSign(sign: Sign) {
      this.$store.commit('calculation/SET_SIGN', sign)
    }
  },

  render(): VNode {
    return (
      <Adder 
        left={this.left}
        right={this.right}
        selectedSign={this.sign}
        onChangeSign={this.changeSign}
      >
        <div slot='result'>
          {this.result}
        </div>
      </Adder>
    )
  }
})

export { App }

Теперь наше приложение выглядит так:

img

Заключение

Эта статья демонстрирует:

  • создание компонентов Vue с помощью tsx
  • типизация входных параметров(props) и событий
  • использование интерфейсов в this.$store

Оригинал статьи

Добавить комментарий