Асинхронно с задержкой

Автор: Марина Мости

С Vue 3 мы получили новый и совершенно особенный компонент под названием <Suspense />. Короче говоря, это позволяет нам лучше контролировать, как и когда наш пользовательский интерфейс должен отображаться, когда дерево компонентов включает асинхронные компоненты. Но что именно это означает?

Быстрый отказ от ответственности

На момент написания этой статьи Suspense по-прежнему помечается как экспериментальная функция и, согласно документам, «не гарантируется достижение стабильного статуса, и API может измениться до того, как это произойдет».

Установка

Чтобы лучше понять возможности Suspense, я создал фиктивную CMS, в которой наши пользователи могут видеть данные своей компании (через некоторые фиктивные API). Главный и единственный экран, или приборная панель, выглядит так, как только все загружено.

У нас есть три основных компонента, по одному на каждую из колонок.

📃Сотрудники.vue

<template>
  <div>
    <h2>Employees</h2>
    <p v-if="loading">Loading...</p>
    <ul v-else>
      <li v-for="employee in employees" :key="employee.id">
        {{ employee.firstName }} {{ employee.lastName }} - {{ employee.email }}
      </li>
    </ul>
  </div>
</template>
<script setup>
import { get } from 'axios'
import { ref } from 'vue'
const employees = ref([])
const loading = ref(true)
get('https://hub.dummyapis.com/employee?noofRecords=10&idStarts=1001')
  .then(({ data }) => {
    loading.value = false
    employees.value = data
  })
</script>

📃 Products.vue

<template>
  <div>
    <h2>Products</h2>
    <p v-if="loading">Loading...</p>
    <ul v-else>
      <li v-for="product in products" :key="product.id">
        {{ product.name }} - {{ product.price }}
      </li>
    </ul>
  </div>
</template>
<script setup>
import { get } from 'axios'
import { ref } from 'vue'
const products = ref([])
const loading = ref(true)
get('https://hub.dummyapis.com/products?noofRecords=10&idStarts=1001&currency=usd')
  .then(({ data }) => {
    loading.value = false
    products.value = data
  })
</script>

📃 SlowData.vue

<template>
  <div>
    <h2>Big data</h2>
    <p v-if="loading">Loading...</p>
    <p v-else>🐢</p>
  </div>
</template>
<script setup>
import { get } from 'axios'
import { ref } from 'vue'
const loading = ref(true)
get('https://hub.dummyapis.com/delay?seconds=3')
  .then(({ data }) => {
    loading.value = false
  })
</script>

Три компонента следуют одному и тому же шаблону: он извлекает свои собственные данные из конечной точки демонстрационного API, отключает флаг загрузки после его загрузки, а затем отображает данные. До момента полной загрузки данных вместо этого появляется индикатор Loading.

Компонент BigData.vue намеренно работает медленно, его загрузка занимает целых 3 секунды. Таким образом, всякий раз, когда пользователь посещает страницу, он будет видеть три уникальных текста загрузки. Два из них довольно быстро заменяются данными, а для загрузки одного требуется совсем немного времени.

Проблема

Приведенный выше пример полностью функционален, однако UX не самый лучший. В некоторых сценариях при создании приложений наличие нескольких небольших компонентов, обрабатывающих свои собственные состояния загрузки и отображающих 1, 2, 3+ счетчика загрузки на странице, может быстро выйти из-под контроля и загрязнить наш дизайн. Не говоря уже о том, что если какой-то из этих API выйдет из строя, нам нужно начать обрабатывать возможность возникновения какого-то глобального состояния ошибки.

Вместо этого было бы даже лучше, если бы для этого случая мы могли объединить все состояния загрузки в одно и отображать один флаг Loading....

💡 Совет для продвинутых: если вы когда-либо использовали Promise.all, это будет очень похоже.

Решение

Мы собираемся использовать Suspense для решения нашей проблемы с UX. Это позволит нам «поймать» (на компоненте верхнего уровня) тот факт, что у нас есть подкомпоненты, которые делают сетевые запросы при создании (в пределах setup), и объединить флаг загрузки в одном месте.

Чтобы сделать это, нам сначала нужно изменить наши компоненты, чтобы они действительно были асинхронными. В настоящее время они написаны с использованием метода then JavaScript Promise, который не асинхронен. Блок then выполняется позже и не блокирует выполнение остального кода установки.

Другой распространенный способ написать это — установить асинхронные вызовы в методе onMounted, но опять же — это не асинхронность.

Чтобы компонент действительно считался асинхронным, ему необходимо сделать вызов API в методе setup, который блокирует выполнение скрипта, используя async/await API.

Глядя на код этих компонентов, вы можете кое-что заметить…

📃Сотрудники.vue

<template>
  <div>
    <h2>Employees</h2>
    <ul>
      <li v-for="employee in employees" :key="employee.id">
        {{ employee.firstName }} {{ employee.lastName }} - {{ employee.email }}
      </li>
    </ul>
  </div>
</template>
<script setup>
import { get } from 'axios'
import { ref } from 'vue'
const employees = ref([])
const { data } = await get('https://hub.dummyapis.com/employee?noofRecords=10&idStarts=1001')
employees.value = data
</script>

📃 Products.vue

<template>
  <div>
    <h2>Products</h2>
    <ul>
      <li v-for="product in products" :key="product.id">
        {{ product.name }} - {{ product.price }}
      </li>
    </ul>
  </div>
</template>
<script setup>
import { get } from 'axios'
import { ref } from 'vue'
const products = ref([])
const { data } = await get('https://hub.dummyapis.com/products?noofRecords=10&idStarts=1001&currency=usd')
products.value = data
</script>

📃 SlowData.vue

<template>
  <div>
    <h2>Big data</h2>
    <p>🐢 {{stuff.length}}</p>
  </div>
</template>
<script setup>
import { get } from 'axios'
const { data } = await get('https://hub.dummyapis.com/delay?seconds=3')
const stuff = data
</script>

Все примеры компонентов используют script setup, что позволяет нам напрямую использовать await внутри него, вообще не объявляя async. Если вы не хотите или не можете использовать script setup, а вместо этого хотите использовать функцию setup () {}, вы должны объявить ее асинхронной.

<script>
export default {
  async setup () {
    await something()
  }
}
</script>

Теперь, когда все наши компоненты были правильно настроены (каламбур) как асинхронные, нам нужно обернуть их в компонент Suspense на родительском элементе.

Обратите внимание, что он не должен быть непосредственным родителем, это очень важная функция, о которой следует помнить.

📃 App.vue

<template>
  <h1>Amazing CMS</h1>
  <sub>So amaze ✨</sub>
  <Suspense>
    <main>
      <Employees />
      <Products />
      <SlowData />
    </main>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
<script setup>
import Employees from './components/Employees.vue'
import Products from './components/Products.vue'
import SlowData from './components/SlowData.vue'
</script>

В нашем компоненте App.vue мы теперь обертываем наши три компонента компонентом <Suspense />.

Первое, на что следует обратить внимание, это то, что Suspense не позволяет размещать содержимое слота по умолчанию с несколькими корнями, поэтому я решил объединить их все здесь с тегом <main>. Если вы забудете, Vue выдаст четкое предупреждение с просьбой сделать его одним корневым элементом.

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

В этом случае я решил просто настроить текст Loading..., пока все загружается в фоновом режиме. Конечно, вы можете сделать представление по умолчанию настолько простым или сложным, насколько это необходимо вашему приложению.

Обработка ошибок

Важной частью любого приложения является возможность обрабатывать возникающие ошибки. Сам компонент Suspense не предоставляет API для обработки ошибок, но Vue предоставляет хук onErrorCaptured, который мы можем использовать в нашем компоненте App.vue для перехвата всякий раз, когда возникает ошибка.

📃 App.vue

<script setup>
import { onErrorCaptured } from 'vue'
import Employees from './components/Employees.vue'
import Products from './components/Products.vue'
import SlowData from './components/SlowData.vue'
onErrorCaptured((e, instance, info) => {
  console.log(e, instance, info)
})
</script>

Обратите внимание, что эта стратегия немного слишком запоздала для того, чтобы что-то сделать в отношении сбоя сетевого вызова в одном из ваших подкомпонентов. Подробности API для onErrorCaptured в документации содержат более подробную информацию о каждом из параметров, которые вы получаете, но в двух словах вы получаете:

  • Ошибка
  • Указатель на экземпляр компонента, вызвавшего ошибку
  • Информация о происхождении ошибки (например, функция setup)

Теоретически, на этом этапе вы можете использовать instance для вызова открытого метода для повторной попытки вызова или просто перевести приложение в состояние ошибки и заблокировать весь пользовательский интерфейс — это зависит от конкретных потребностей вашего приложения.

Лучшим и более рекомендуемым подходом было бы установить обработку ошибок внутри самого компонента, если это позволяет ваше конкретное решение.

Например, мы можем принудительно сломать наш компонент Products.vue для демонстрации.

📃 Products.vue

<script setup>
import { get } from 'axios'
import { ref } from 'vue'
const products = ref([])
try {
  const { data } = await get('nopenotanapi.com')
  products.value = data
} catch (e) {
  products.value = []
}
</script>

Блок try/catch предотвратит полную блокировку всего UX, изящно устанавливая ссылку на продукты в пустой массив. Правильное (и лучшее) решение полностью зависит от области вашего приложения и от того, как вы хотите обрабатывать свои ошибки!

Подведение итогов

Компонент Suspense в Vue 3, безусловно, является очень мощным инструментом, обеспечивающим более четкий UX. Если повезет, мы скоро сможем использовать его в его окончательной неэкспериментальной форме, и я надеюсь, что эта статья помогла вам познакомиться с тем, как его реализовать. Чтобы узнать больше о Vue 3, вы можете пройти Глубокое погружение в Vue 3 с Эваном Ю или изучить исходный код Vue 3 Reactivity и Composition API на курсах Vue Mastery.

Первоначально опубликовано на https://www.vuemastery.com 28 сентября 2022 г.