Наблюдатель и сохранение фильтра
Оглавление серии «Vue 3: кабинет сотрудников «Команда»»
После выделения useTeam фильтр снова становится пустым при пересоздании страницы. Сохраним поисковую строку в localStorage, оставляя саму команду учебным массивом. Результат урока — watcher, который выполняет конкретный побочный эффект, и безопасное восстановление значения. Мы сохраняем настройку представления, а не сотрудника и не подтверждение серверного действия.
Это различие влияет на обработку отказа. Если браузер запрещает хранилище, кабинет всё ещё должен показывать участников и выполнять локальный поиск. Недоступность удобства не должна обрушивать основную работу. Поэтому чтение и запись заключены в try, а неправильный формат заменяется пустой строкой. Такое правило не скрывает ошибку приложения вообще: оно описывает допустимое поведение именно необязательной настройки.
Наблюдение за одной зависимостью
Полностью заменим src/composables/useTeam.js. Ключ team-query:v1 обозначает название настройки и её формат. Начальное чтение выполняется один раз для данного экземпляра, watcher реагирует на дальнейшее изменение query.
import { computed, ref, watch } from 'vue'
import { seedMembers } from '../data/members.js'
const storageKey = 'team-query:v1'
function readQuery() {
try {
const value = JSON.parse(localStorage.getItem(storageKey) ?? '""')
return typeof value === 'string' && value.length <= 200 ? value : ''
} catch { return '' }
}
export function useTeam() {
const members = ref(seedMembers.map(member => ({ ...member })))
const query = ref(readQuery())
const onlyAvailable = ref(false)
const descending = ref(false)
watch(query, value => {
try { localStorage.setItem(storageKey, JSON.stringify(value)) } catch { /* optional setting */ }
})
const visibleMembers = computed(() => members.value.filter(member =>
`${member.name} ${member.role}`.toLocaleLowerCase('ru')
.includes(query.value.trim().toLocaleLowerCase('ru'))
&& (!onlyAvailable.value || member.available)
).sort((a, b) => (descending.value ? -1 : 1) * a.name.localeCompare(b.name, 'ru')))
function toggle(id) {
const member = members.value.find(item => item.id === id)
if (member) member.available = !member.available
}
function create(draft) {
members.value.push({ id: crypto.randomUUID(), ...draft, available: true, version: 1 })
}
return { members, query, onlyAvailable, descending, visibleMembers, toggle, create }
}
Документация наблюдателей Vue различает чтение зависимостей и выполнение эффектов при их изменении. Наш watcher получает сам ref query, а не query.value. Во втором случае он получил бы готовую строку, которая не описывает источник наблюдения. Если наблюдать поле объекта, часто понадобится функция, возвращающая это поле; здесь самостоятельный ref уже даёт подходящий источник.
Ожидаемая последовательность такова: пользователь вводит «Анна», watcher сохраняет JSON-строку, при следующем создании useTeam readQuery получает её и фильтр сразу показывает Анну. Рабочая коллекция при этом снова начинается с исходных трёх записей. Добавленный ранее Максим не восстанавливается. Такое поведение следует из выбора сохраняемого поля и не является частичным восстановлением базы сотрудников.
Формат сохранённого значения
localStorage хранит строки. Мы записываем JSON, чтобы чтение имело явный формат и позднее могло отличить старую настройку от нового объекта. Однако JSON.parse может выбросить исключение при повреждённых данных. Поэтому код проверяет не только наличие значения, но и корректность разбора, тип строки и длину. Сохранённая настройка — внешние данные, которые нельзя считать правильными только потому, что когда-то их создавало это приложение.
Ключ с v1 не обновляет автоматически структуру и не выполняет миграцию. Он лишь помогает человеку определить версию формата. Если позже будем сохранять несколько полей одним объектом, можно использовать другой ключ и явно преобразовать прежнее значение. Не называйте смену имени ключа полноценной миграцией пользовательских настроек: старое значение просто останется в хранилище, пока приложение не примет отдельное решение о его очистке.
Ввод ограничен при восстановлении двумястами единицами, но поле текущего App пока не содержит maxlength для query. Это не опасность сохранения сотрудников, однако даёт полезное упражнение согласования правил: если продукт ограничивает длину поиска, добавьте одинаковое правило интерфейсу и чтению. В учебном варианте длинная строка может работать в текущем сеансе, а после пересоздания будет сброшена. Ограничение следует прочитать как правило восстановления, не как серверную валидацию.
Жизненный цикл эффекта
useTeam вызывается синхронно из script setup App. Созданный там watcher связывается с компонентом и прекращает наблюдение при его уничтожении. Если вынести создание наблюдателя в поздний таймер или случайный callback, эта связь уже не гарантируется тем же способом. Поэтому мы не откладываем сам watch, даже если в будущем понадобится задержать дорогую запись. Нужно отличать момент регистрации эффекта от момента выполнения его действия.
localStorage доступен в браузере, но не является общей возможностью любой среды JavaScript. Этот composable предназначен для текущего клиентского приложения. При серверном рендеринге чтение нужно организовать с учётом отсутствия браузерного объекта и различия первого HTML и клиентского состояния. Простая обёртка try защитит от исключения, но не решит весь договор гидратации. В этой серии SSR не используется, поэтому мы явно ограничиваем среду.
Отказ записи не показывается как ошибка создания участника. Он относится к удобству фильтра и не должен выглядеть как неудачное сохранение человека. Если продукт считает настройку обязательной, понадобится отдельный статус сохранения настройки. Текущая фраза optional setting в комментарии описывает выбранное правило, а не позволяет бездумно игнорировать любые исключения. Сетевой запрос, изменение прав и сохранение формы в дальнейшем получат другие способы сообщения об отказе.
Полезный неправильный случай
Представьте, что в ключе хранится строка без правильных кавычек JSON либо число 17. В первом случае разбор попадает в catch, во втором проверка типа возвращает пустую строку. Команда остаётся видимой, поиск можно снова ввести. Если убрать защиту, повреждённая необязательная настройка может не дать создать компонент вообще. Причина здесь находится на внешней границе, а не в вычисляемом списке.
Изменение localStorage в другой вкладке не обновляет query нашего компонента само по себе. Для этого понадобился бы обработчик storage и продуманное правило согласования пользовательского ввода. Мы не добавляем такой механизм, потому что сохраняем фильтр между пересозданиями, а не синхронизируем работу нескольких вкладок. Проверьте, что название обещания соответствует реализации: «восстановление настройки» точнее, чем «общее состояние браузера».
Перед маршрутизацией запишите, какие значения сейчас переживают пересоздание компонента. Ответ будет неоднородным: query восстанавливается из storage, onlyAvailable и descending создаются заново, members копируется из исходного набора. Следующий урок введёт два экрана и покажет, почему навигация усиливает значение этих границ. Один адрес должен обозначать одного участника, но сам адрес ещё не создаёт общее хранилище данных.
Значение пустой сохранённой строки
Пустой запрос является допустимой настройкой, а не ошибкой. Он означает просмотр всей команды. Поэтому null от отсутствующего ключа преобразуется в JSON пустой строки и получает то же поведение. Если использовать небрежное выражение с ложностью вместо проверки типа, можно спутать допустимые значения с отсутствием. Для одного строкового поля различие невелико, но выбранный приём показывает, как явно отделять формат от предметного значения. После очистки поиска watcher также сохраняет пустую строку, чтобы прежний запрос не возвращался неожиданно.
Для самостоятельного воспроизведения результата «Сохранять только настройки и корректно читать storage» выберите lesson-09 в архиве исходников. Статус выполнения этого состояния: not_run.