[К содержанию](../readme.md#введение-в-web-разработку) # Vue.js: рефакторинг ## #18 Криптономикон: рефакторинг (live) * [YouTube](https://www.youtube.com/live/AzsO67rloQw?feature=share&t=248) * [RuTube](https://rutube.ru/video/979b2cce76478860252ca7f6ee63b4b1/?r=wd&t=250) >Качество на RuTube очень паршивое, если не сможете посмотреть оригинал на YouTube, то можете скачать видео с помощью какого-нибудь сервиса. **Содержание урока:** - Презентация задачи для шаблона письма - разделение бизнес логики и реализации (вынос запросов АПИ в отдельный файл, вебсокеты, подписка на обновления) ## Расшифровка скринкаста >Спойлер с новой задачей (шаблон письма) не делаем, это просто презентация на будущее >В этом видео много раз переделывается одно и то же. Не спешите копипастить, постарайтесь понять что происходит. ### Вынесение действий, не относящихся к бизнес-логике, в отдельный js-файл (api.js) 1. Создаём в каталоге `src` файл `api.js` 1. Выносим в него функцию получения валюты Ключевое слово **export** перед переменной или функцией говорит, что эта переменная (или функция) будет доступна для импорта в других модулях >Синтаксис с промисами я уже описывал в прошлых лекциях. Вы должны понимать, что этот метод вернёт **Promise**, т.е. вызывающая функция должна либо использовать асинхронный вызов **await**, либо метод _.then_ ```js 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()) ``` Не очень похоже на функцию? На самом деле это один из вариантов объявления лямбда функции. ``` переменная = (параметры функции) => { тело функции } ``` Просто тут сделано два упрощения: * если у функции только один параметр, то можно не писать круглые скобки * если тело функции состоит из одного выражения, то можно не писать и фигурные скобки: результат этого выражения и будет результатом функции Т.е. более привычный способ записи был бы такой: ```js 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()) } ``` а если совсем боитесь (не понимаете) лямбда функций, то такой: ```js 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()) } ``` 1. В основном файле приложения (`App.vue`) прописываем импорт новой функции и меняем код в том месте, где был запрос валюты * импорт ```js import { loadTiker } from './api' ``` Фигурные скобки означают декомпозицию, т.е. имя переменной _loadTiker_ доступно в модуле напрямую. >Все экспорты модуля снаружи выглядят как объект и, если не использовать декомпозицию, то для обращения к функции пришлось бы использовать свойство объекта. Например > >```js >import someModule from './api' >... >someModule.loadTiker >``` * замена кода ```js setInterval(async () => { const data = await loadTiker(tickerName) // тут удаляем старый код запроса данных ``` ### Переписываем кучу одинарных запросов валюты на один пакетный >Весь текст не расшифровываю - смотрите видео Смысл в том, что в текущей реализации мы при добавлении новой валюты запускаем таймер с получением данных. При этом у нас нет способов отключить таймер. Получается, что если 10 раз добавить/удалить одну и ту же валюту, то у нас получится 10 одинаковых таймеров при пустом списке валют (это конечно маловероятный случай, но наглядно показывающий ущербность текущей реализации) К счастью АПИ позволяет получить обратные курсы валют. Не `<валюта>` к доллару, как сейчас, а доллар к списку валют. Т.е. мы одним выстрелом убиваем двух зайцев: минимизируем количество запросов (в этом варианте одним запросом можно запросить до 500 валют) и избавляемся от необходимости удалять таймеры. 1. Переписываем функцию _loadTiker_ ```js 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** преобразует массив в строку, разделенную запятыми) 1. В основном компоненте (`App.vue`) реализуем функцию _updateTickers_ Эта функция вызывает функцию получения данных о курсах валют и для каждой валюты в списке валют обновляет её стоимость, учитывая возможность отсутствия валюты в результате ```js 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] }) } ``` 1. Удаляем из метода _add_ получение данных (таймер) 1. В методе жизненного цикла **onBeforeMount** переписываем создание таймера для периодического получения данных (хотя можно и непосредственно в `script setup`) ```js onBeforeMount(() => { const tickersData = localStorage.getItem('cryptonomicon-list') ?? '[]' if (tickersData) { tickers.value = JSON.parse(tickersData) } setInterval(() => { updateTickers() }, 5000) }) ``` Обратите внимание, в видео описан вариант без обёртки в лямбда-функцию, но у меня так не заработало. Используем "классический" вариант. 1. В массиве валют стоимость теперь хранится в исходном виде (число с кучей цифр после запятой) Реализуем метод форматирования цены: ```js function formatPrice (price) { if (typeof price == 'number') { return price > 1 ? price.toFixed(2) : price.toPrecision(2) } return price } ``` >У меня иногда появлялось исключение типа "метод toFixed не определён", возможно при значении `'-'`, поэтому я добавил проверку на тип данных И вставляем его в шаблон, в то место, где выводится стоимость валюты (самостоятельно) ### Улучшайзинг полученного кода В рамках отделения бизнес-логики от реализации перенесём обратное преобразование цены в код метода _loadTickers_ (добавили множественное число) Добавляем действие в цепочку: ```js 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_ мы можем выкинуть лишнюю логику и оставить только присваивание: ```js const data = await loadTikers( tickers.value.map( t => t.name ) ) tickers.value.forEach(t => { t.price = data[t.name] ?? '-' }) ``` ### Ещё один улучшайзинг Кто-то нашёл, что апи поддерживает и прямой запрос список-список Мы меняем **endpoint** на _pricemulti_, в _fsyms_ передаём список валют, а в tsyms USD (тоже список, но из одной валюты) ```js 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. Посмотреть видео 1. Реализовать описанные в скринкасте изменения 1. В новой реализации потерялось добавление данных в график выбранной валюты. Исправьте это самостоятельно. [Назад](./web_09.md) | [Дальше](./web_11.md)