web_10.md 13 KB

К содержанию

Vue.js: рефакторинг

#18 Криптономикон: рефакторинг (live)

Качество на RuTube очень паршивое, если не сможете посмотреть оригинал на YouTube, то можете скачать видео с помощью какого-нибудь сервиса.

Содержание урока:

  • Презентация задачи для шаблона письма
  • разделение бизнес логики и реализации (вынос запросов АПИ в отдельный файл, вебсокеты, подписка на обновления)

Расшифровка скринкаста

Спойлер с новой задачей (шаблон письма) не делаем, это просто презентация на будущее

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

Вынесение действий, не относящихся к бизнес-логике, в отдельный js-файл (api.js)

  1. Создаём в каталоге src файл api.js

  2. Выносим в него функцию получения валюты

    Ключевое слово export перед переменной или функцией говорит, что эта переменная (или функция) будет доступна для импорта в других модулях

    Синтаксис с промисами я уже описывал в прошлых лекциях. Вы должны понимать, что этот метод вернёт Promise, т.е. вызывающая функция должна либо использовать асинхронный вызов await, либо метод .then

    const API_KEY =    'ce3fd966e7a1d10d65f907b20bf000552158fd3ed1bd614110baa0ac6cb57a7e'
    
    export const loadTiker = tickerName => fetch(  
        `https://min-api.cryptocompare.com/data/price?fsym=${tickerName}&tsyms=USD&api_key=${API_KEY}`
    ).then(r => r.json())
    

    Не очень похоже на функцию? На самом деле это один из вариантов объявления лямбда функции.

    переменная = (параметры функции) => { тело функции }
    

    Просто тут сделано два упрощения:

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

    Т.е. более привычный способ записи был бы такой:

    export const loadTiker = (tickerName) => {
        return fetch(  
            `https://min-api.cryptocompare.com/data/price?fsym=${tickerName}&tsyms=USD&api_key=${API_KEY}`
        ).then(r => r.json())
    }
    

    а если совсем боитесь (не понимаете) лямбда функций, то такой:

    export function loadTiker (tickerName) {
        return fetch(  
            `https://min-api.cryptocompare.com/data/price?fsym=${tickerName}&tsyms=USD&api_key=${API_KEY}`
        ).then(r => r.json())
    }
    
  3. В основном файле приложения (App.vue) прописываем импорт новой функции и меняем код в том месте, где был запрос валюты

    • импорт

      import { loadTiker } from './api'
      

      Фигурные скобки означают декомпозицию, т.е. имя переменной loadTiker доступно в модуле напрямую.

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

      import someModule from './api'
      ...
      someModule.loadTiker
      
    • замена кода

      setInterval(async () => {
          const data = await loadTiker(tickerName)
          // тут удаляем старый код запроса данных
      

Переписываем кучу одинарных запросов валюты на один пакетный

Весь текст не расшифровываю - смотрите видео

Смысл в том, что в текущей реализации мы при добавлении новой валюты запускаем таймер с получением данных. При этом у нас нет способов отключить таймер. Получается, что если 10 раз добавить/удалить одну и ту же валюту, то у нас получится 10 одинаковых таймеров при пустом списке валют (это конечно маловероятный случай, но наглядно показывающий ущербность текущей реализации)

К счастью АПИ позволяет получить обратные курсы валют. Не <валюта> к доллару, как сейчас, а доллар к списку валют. Т.е. мы одним выстрелом убиваем двух зайцев: минимизируем количество запросов (в этом варианте одним запросом можно запросить до 500 валют) и избавляемся от необходимости удалять таймеры.

  1. Переписываем функцию loadTiker

    export const loadTiker = tickers => fetch(  
        `https://min-api.cryptocompare.com/data/price?fsym=USD&tsyms=${tickers.join(',')}&api_key=${API_KEY}`
    ).then(r => r.json())
    

    Обратите внимание:

    • в параметрах метода вместо одной валюты передается список валют
    • в параметрах запроса fsym поменяли на USD, а tsyms на список валют (функция join преобразует массив в строку, разделенную запятыми)
  2. В основном компоненте (App.vue) реализуем функцию updateTickers

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

    async function updateTickers () {
        // если список валют пуст, то ничего не делаем
        if (!tickers.value.length) return
    
        // получаем данные о валютах
        // в параметрах передаём список валют
        const data = await loadTiker(
            tickers.value.map(
                t => t.name
            )
        )
    
        tickers.value.forEach(t => {
            // в верхний регистр переводить не надо, 
            // мы уже делали самостоятельную работу с валидацией
            // и в списке у нас валюты должны быть в верхнем регистре
            if (!data[t.name]) {
                t.price = '-'
                return
            }
            t.price = 1 / data[t.name]
        })
    }
    
  3. Удаляем из метода add получение данных (таймер)

  4. В методе жизненного цикла onBeforeMount переписываем создание таймера для периодического получения данных (хотя можно и непосредственно в script setup)

    onBeforeMount(() => {
        const tickersData = localStorage.getItem('cryptonomicon-list') ?? '[]'
        if (tickersData) {
            tickers.value = JSON.parse(tickersData)
        }
        setInterval(() => {
            updateTickers()
        }, 5000)
    })
    

    Обратите внимание, в видео описан вариант без обёртки в лямбда-функцию, но у меня так не заработало. Используем "классический" вариант.

  5. В массиве валют стоимость теперь хранится в исходном виде (число с кучей цифр после запятой)

    Реализуем метод форматирования цены:

    function formatPrice (price) {
        if (typeof price == 'number') {
            return price > 1 ? price.toFixed(2) : price.toPrecision(2)
        }
        return price
    }
    

    У меня иногда появлялось исключение типа "метод toFixed не определён", возможно при значении '-', поэтому я добавил проверку на тип данных

    И вставляем его в шаблон, в то место, где выводится стоимость валюты (самостоятельно)

Улучшайзинг полученного кода

В рамках отделения бизнес-логики от реализации перенесём обратное преобразование цены в код метода loadTickers (добавили множественное число)

Добавляем действие в цепочку:

export const loadTikers = tickers => fetch(  
  `https://min-api.cryptocompare.com/data/price?fsym=USD&tsyms=${tickers.join(',')}&api_key=${API_KEY}`
).then(
    r => r.json()
).then(rawData => {
  return Object.fromEntries(
    Object.entries(rawData)
      .map(([key,value]) => [key, 1 / value])
  )
})

Более подробно о преобразованиях:

  • rawData на входе выглядит примерно так: {a: 1, b: 2}
  • Метод Object.entries() превращает их в массив массивов: [['a', 1], ['b', 2]]
  • который мы перебираем методом map и получаем прямые цены [['a', 1], ['b', 0.5]]
  • Object.fromEntries() упаковывает массивы обратно в объект {a: 1, b: 0.5}

Теперь в методе updateTickers мы можем выкинуть лишнюю логику и оставить только присваивание:

const data = await loadTikers(
    tickers.value.map(
        t => t.name
    )
)

tickers.value.forEach(t => {
    t.price = data[t.name] ?? '-'
})

Ещё один улучшайзинг

Кто-то нашёл, что апи поддерживает и прямой запрос список-список

Мы меняем endpoint на pricemulti, в fsyms передаём список валют, а в tsyms USD (тоже список, но из одной валюты)

export const loadTikers = tickers => fetch(  
  `https://min-api.cryptocompare.com/data/pricemulti?fsyms=${tickers.join(',')}&tsyms=USD&api_key=${API_KEY}`
).then(r => r.json()).then(rawData => {
  return Object.fromEntries(
    Object.entries(rawData)
      .map(([key,value]) => [key, value.USD])
  )
})

Т.к. формат ответа отлчается, то немного правим действие в методе map. Value у нас теперь не обратная цена, а объект с названиями валют и их ценами.

Обратите внимание, мы правили только слой апи, а слой бизнес логики не меняли, но всё продолжает работать. В этом и есть цель разделения бизнес-логики и апи.

На мой взгляд, для junior-ов этого уже достаточно.

Исходный курс рассчитан на middl-ов, если кому-то интересно, то самостоятельно реализуйте задачу с подпиской на стоимость (за дополнительную оценку)


  1. Посмотреть видео
  2. Реализовать описанные в скринкасте изменения
  3. В новой реализации потерялось добавление данных в график выбранной валюты. Исправьте это самостоятельно.

Назад | Дальше