ソースを参照

vue рефакторинг

Евгений Колесников 1 年間 前
コミット
a546217caa
2 ファイル変更238 行追加8 行削除
  1. 238 1
      articles/web_10.md
  2. 0 7
      articles/web_19.md

+ 238 - 1
articles/web_10.md

@@ -2,19 +2,256 @@
 
 # Vue.js:
 
-## #18 Криптономикон: рефакторинг
+## #18 Криптономикон: рефакторинг (live)
 
 <!-- 2ч -->
 
 * [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)

+ 0 - 7
articles/web_19.md

@@ -1,10 +1,3 @@
 [К содержанию](../readme.md#введение-в-web-разработку)
 
-# Криптономикон: рефакторинг - Vue.js: практика (часть 2)
-
-
-
-
-
-
 [Назад](./web_18.md) | [Дальше](./web_20.md)