|
@@ -1,49 +1,208 @@
|
|
|
[К содержанию](../readme.md#введение-в-web-разработку)
|
|
[К содержанию](../readme.md#введение-в-web-разработку)
|
|
|
|
|
|
|
|
-# Введение во Vue
|
|
|
|
|
|
|
+# Vue.js практика: computed, watch
|
|
|
|
|
|
|
|
-1. [Vue 3: Введение](https://v3.ru.vuejs.org/ru/guide/introduction.html)
|
|
|
|
|
-1. Создание проекта
|
|
|
|
|
|
|
+## #17 Криптономикон: рефакторинг
|
|
|
|
|
|
|
|
- >Для новых проектов теперь рекомендуется использовать `create-view` для создания проектов на основе Vite.
|
|
|
|
|
|
|
+<!-- 1 час 24 мин -->
|
|
|
|
|
|
|
|
- >Для работы с проектами на **Vue** необходим **NPM** (англ. Node Package Manager) — менеджер пакетов, входящий в состав Node.js.
|
|
|
|
|
|
|
+* [YouTube](https://www.youtube.com/watch?v=_esgbWGiP3c)
|
|
|
|
|
+* [RuTube](https://rutube.ru/video/84b87c1a5285bb57d7771e67e9436fc7/?r=wd&t=613)
|
|
|
|
|
|
|
|
- Создание **Vue** проекта:
|
|
|
|
|
|
|
|
|
|
- ```
|
|
|
|
|
- npm init vite@latest
|
|
|
|
|
|
|
+**Содержание урока:**
|
|
|
|
|
+
|
|
|
|
|
+* [формирование списка проблем для рефакторинга](#список-проблем-существующего-кода)
|
|
|
|
|
+* [наличие в состоянии зависимых данных (computed)](https://www.youtube.com/live/_esgbWGiP3c?t=1980)
|
|
|
|
|
+
|
|
|
|
|
+**Материалы к изучению:**
|
|
|
|
|
+
|
|
|
|
|
+- [вычисляемые свойства](https://ru.vuejs.org/guide/essentials/computed.html)
|
|
|
|
|
+
|
|
|
|
|
+## Расшифровка скринкаста
|
|
|
|
|
+
|
|
|
|
|
+### Список проблем существующего кода
|
|
|
|
|
+
|
|
|
|
|
+* [x] одинаковый код в наблюдателях
|
|
|
|
|
+* [ ] при удалении остается подписка (таймер)
|
|
|
|
|
+* [ ] количество запросов (в АПИ есть возможность получить данные по списку тикеров)
|
|
|
|
|
+* [ ] запросы к АПИ внутри компонента
|
|
|
|
|
+* [ ] обработка ошибок АПИ
|
|
|
|
|
+* [x] наличие состояния зависимых данных
|
|
|
|
|
+* [ ] график переполняется
|
|
|
|
|
+* [x] при удалении тикера не меняется LocalStorage
|
|
|
|
|
+* [ ] LocalStorage и анонимные вкладки (LocalStorage может быть недоступен)
|
|
|
|
|
+* [ ] магические строки и числа (url, 5000 мсек, ключ localstorage, размер страницы)
|
|
|
|
|
+
|
|
|
|
|
+**Параллельно**
|
|
|
|
|
+
|
|
|
|
|
+* [x] график сломан, если везде одинаковые значения
|
|
|
|
|
+* [x] при удалении тикера остается выбор
|
|
|
|
|
+
|
|
|
|
|
+### Наличие состояния зависимых данных
|
|
|
|
|
+
|
|
|
|
|
+Метод **filteredTickers** зависит от страниц и массива тикеров, в таком случае нужно использовать [вычисляемые свойства](https://ru.vuejs.org/guide/essentials/computed.html)
|
|
|
|
|
+
|
|
|
|
|
+В _Composition API_ у вычисляемых свойств другой синтаксис:
|
|
|
|
|
+
|
|
|
|
|
+```js
|
|
|
|
|
+const calculatedProperty = computed(() => {
|
|
|
|
|
+ // тут какие-то вычисления на основе состояний
|
|
|
|
|
+ return result
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Вычисляемые свойства кешируются, т.е. будут вычисляться только при изменнии используемых реактивных переменных (состояний). В нашем случае это "номер страницы", "строка фильтра" и "список тикеров"
|
|
|
|
|
+
|
|
|
|
|
+При использовании в шаблоне надо помнить, что **computed** это свойство, а не функция. Он не может принимать аргументы (не нужно указывать круглые скобки)
|
|
|
|
|
+
|
|
|
|
|
+Если установлен линтер, то он может показать ошибку, что присваиванию _hasNextPage_ не место в **computed**
|
|
|
|
|
+
|
|
|
|
|
+Т.к. это тоже вычисляемое свойство, то его тоже можно завернуть в **computed**
|
|
|
|
|
+
|
|
|
|
|
+>Попутно решили, что start и end тоже можно вынести в **computed** для единообразия
|
|
|
|
|
+
|
|
|
|
|
+В итоге получились такие вычисляемые свойства:
|
|
|
|
|
+
|
|
|
|
|
+```js
|
|
|
|
|
+const startIndex = computed(() => {
|
|
|
|
|
+ return (page.value - 1) * 6
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const endIndex = computed(() => {
|
|
|
|
|
+ return page.value * 6
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// это объясняется чуть позже
|
|
|
|
|
+const filteredTickers = computed(() => {
|
|
|
|
|
+ return tickers.value.filter(
|
|
|
|
|
+ t => t.name.includes(filter.value.toUpperCase()))
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// не забудьте поменять в шаблоне filteredTickers на paginatedTickers
|
|
|
|
|
+const paginatedTickers = computed(() => {
|
|
|
|
|
+ return filteredTickers.value.slice(startIndex.value, endIndex.value)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// не забудьте удалить переменную hasNextPage
|
|
|
|
|
+const hasNextPage = computed(() => {
|
|
|
|
|
+ // я уже упоминал, что computed возвращает реактивный объект, т.е. к его значению надо обращаться через свойство .value
|
|
|
|
|
+ return filteredTickers.value.length > endIndex.value
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Как понять что нужно использовать **computed**? Очень просто. Если нет прямого изменения состояния (присваивания), а только вычисление по другим реактивным переменным (в лекции сказали ещё "если результат используется в шаблоне", но это не так, в этом же примере _startIndex_ и _endIndex_ сделаны вычисляемыми, но они используются только в коде).
|
|
|
|
|
+
|
|
|
|
|
+Смотрим, что ещё можно поменять на **computed**?
|
|
|
|
|
+
|
|
|
|
|
+* **normalizeGraph** - переделайте в **computed** самостоятельно, заодно переименуйте в _normalizedGraph_ (прилагательное вместо глагола, т.е. у нас не действие, а свойство)
|
|
|
|
|
+
|
|
|
|
|
+* разбить **filteredTickers** на два вычисляемых свойства (выше уже есть код)
|
|
|
|
|
+
|
|
|
|
|
+### Починка графика
|
|
|
|
|
+
|
|
|
|
|
+Если минимальное значение равно максимальному, то возвращать 50%, чтобы было ровное плато посередине.
|
|
|
|
|
+
|
|
|
|
|
+```js
|
|
|
|
|
+const normalizedGraph = computed(() => {
|
|
|
|
|
+ const maxValue = Math.max(...graph.value)
|
|
|
|
|
+ const minValue = Math.min(...graph.value)
|
|
|
|
|
+
|
|
|
|
|
+ if (maxValue == minValue) {
|
|
|
|
|
+ return graph.value.map(() => 50)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return graph.value.map(
|
|
|
|
|
+ price => 5 + ((price - minValue) * 95) / (maxValue - minValue)
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Применение "наблюдателей"
|
|
|
|
|
+
|
|
|
|
|
+В коде часто встречаются условия "если изменилось что-то, то сделай то-то". Это значит, что можно использовать **watch**
|
|
|
|
|
+
|
|
|
|
|
+Рассматривается пример с удалением тикеров на второй странице. В результате мы остаемся на пустой странице.
|
|
|
|
|
+
|
|
|
|
|
+Вариантов решения два:
|
|
|
|
|
+
|
|
|
|
|
+* в методе удаления тикеров проверить длину массива _paginatedTickers_ и если он пустой, то уменьшить номер страницы
|
|
|
|
|
+* использовать наблюдатели
|
|
|
|
|
+
|
|
|
|
|
+ >Обратите внимание, я использую синтаксис с параметрами. Первым параметром в функции приходит новое значение переменной.
|
|
|
|
|
+
|
|
|
|
|
+ ```js
|
|
|
|
|
+ watch(paginatedTickers, (value) => {
|
|
|
|
|
+ if (value.length === 0 && page.value > 1) page.value--
|
|
|
|
|
+ })
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
- Процесс инициализации проекта интерактивный. Установщик предлагает ввести название проекта и выбрать нужные пакеты (практически все можно оставить по-умолчанию):
|
|
|
|
|
|
|
+### при удалении тикера остается выбор
|
|
|
|
|
|
|
|
- * `Project name:` - название проекта (по-умолчанию предлагает `vue-project` - можете оставить его)
|
|
|
|
|
- * `Add TypeScript?` - оставляем `No`
|
|
|
|
|
- * `Add JSX Support?`
|
|
|
|
|
- * `Add Vue Router...`
|
|
|
|
|
- * `Add Pinia...`
|
|
|
|
|
- * `Add Vitest...`
|
|
|
|
|
- * `Add Ent-to-End Testing Solution`
|
|
|
|
|
- * `Add ESLint` - меняем на `Yes`
|
|
|
|
|
- * `Add Prettier` - меняем на `Yes`
|
|
|
|
|
|
|
+В метод **handleDelete** добавляем логику проверки и очистки выделенного тикера
|
|
|
|
|
|
|
|
- После инициализации проекта в текущем каталоге появится папка с именем проекта, который вы ввели
|
|
|
|
|
|
|
+```js
|
|
|
|
|
+if (sel.value == tickerToRemove) {
|
|
|
|
|
+ sel.value = null
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
- >Пример выше был расписан под **Linux**, но, как оказалось, под **Windows** поведение скрипта отличается.
|
|
|
|
|
|
|
+Заодно поменяйте _sel_ на человеко-понятное наименование (названия переменных должны быть самоочевидны)
|
|
|
|
|
|
|
|
- **Важно!!!** При инициализации проекта не загружаются зависимости. После установки необходимо выполнить команду `npm install`
|
|
|
|
|
|
|
+### Вынос сброса графика в методе **select** в watch
|
|
|
|
|
|
|
|
-1. Продолжаем изучать [JavaScript](https://learn.javascript.ru/property-accessors)
|
|
|
|
|
|
|
+Напоминаю текущий код:
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+```js
|
|
|
|
|
+function select (ticker) {
|
|
|
|
|
+ sel.value = ticker
|
|
|
|
|
+ graph.value = []
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-**Задание**
|
|
|
|
|
|
|
+Он соответствует правилу "если изменилось что-то, то сделай то-то", соответственно можно его перенести очистку графика в наблюдатель (сделайте самостоятельно)
|
|
|
|
|
+
|
|
|
|
|
+### Вынос логики сохранения тикеров в LocalStorage в наблюдатель (заодно исправится бага, когда список не сохранялся при удалении тикера)
|
|
|
|
|
+
|
|
|
|
|
+Наблюдатель реализуйте сами. И не забудьте изменить добавление тикеров в массив (иначе на сработает **watch** на массив)
|
|
|
|
|
+
|
|
|
|
|
+```js
|
|
|
|
|
+tickers.value = [...tickers.value, newTicker]
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-1. Перечитать и осмыслить [Vue 3: Введение](https://v3.ru.vuejs.org/ru/guide/introduction.html)
|
|
|
|
|
|
|
+Что делает этот код?
|
|
|
|
|
|
|
|
-1. Создать проект на **Vue**, изучить структуру, запустить и "поиграться" с вёрсткой и кодом
|
|
|
|
|
|
|
+Выражение `...array` делает декомпозицию массива, т.е. извлекает все его элементы и вставляет в новый массив. В итоге в массив тикеров записывается новый массив, содержащий все старые элементы и новый.
|
|
|
|
|
|
|
|
-1. Перечитать и осмыслить первую часть [Учебника](https://learn.javascript.ru/property-accessors) до раздела "Промисы: async/await"
|
|
|
|
|
|
|
+### Исправление дублирования логики в наблюдателях **page** и **filter**
|
|
|
|
|
+
|
|
|
|
|
+1. Создаем вычисляемое свойство _pageStateOptions_, которое будет хранить и фильтр и страницу:
|
|
|
|
|
+
|
|
|
|
|
+ ```js
|
|
|
|
|
+ const pageStateOptions = computed(() => {
|
|
|
|
|
+ return {
|
|
|
|
|
+ filter: filter.value,
|
|
|
|
|
+ page: page.value
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+1. И вешаем на него наблюдателя
|
|
|
|
|
+
|
|
|
|
|
+ ```js
|
|
|
|
|
+ watch(pageStateOptions, (value) => {
|
|
|
|
|
+ window.history.pushState(
|
|
|
|
|
+ null,
|
|
|
|
|
+ document.title,
|
|
|
|
|
+ `${window.location.pathname}?filter=${value.filter}&page=${value.page}`
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**Задание**
|
|
|
|
|
|
|
|
|
|
+*
|
|
|
|
|
|
|
|
[Назад](./web_08.md) | [Дальше](./web_10.md)
|
|
[Назад](./web_08.md) | [Дальше](./web_10.md)
|