Асинхронные плагины для Vue.js

Представим ситуацию. Вы делаете большое приложение на Vue.js: тут вам и отдельная команда бэкендеров, и вдумчивая разработка, и вообще всё, что полагается.

У приложения есть API, он версионный и часто меняющийся. Бэкендеры решают использовать для автоматизации спеки и визуализации методов какую-нибудь утилиту, допустим – Swagger. На стороне фронта вы используете модуль Swagger’a, чтобы при помощи спецификации обращаться к методам не по URL, так как в процессе разработки он часто, но незначительно меняется, а по имени метода, которое назначено вашим бэкендовым коллегой.

Вы пишите простой сервис-обёртку над сваггером, который делает с данными всё, что вам нужно, и оформляете его как Vue-плагин, добавляющий в прототип вашего экземпляра Vue свойство $api, и теперь вы можете с лёгкой душой обращаться к методам апи из любого места в вашем приложении.

Круто, да?

Однако, есть одна вещь, о которой мы забыли. Наш сервис-обёртка в самую первую очередь должен загрузить спеку, а загрузка файла - асинхронная операция.

Пока мы не загрузим спеку, мы не сможем обрати ться ни к одному методу, ведь мы делаем это не по URL, а по имени.

Есть несколько решений:

  1. Мы кешируем спеку из предыдущего сеанса работы с приложением, в localStorage, посредством ServiceWorker или как-то ещё.
    НО: что если человек впервые попадает на наш сайт?
  2. Мы храним в глобальном стейте приложения переменную-идентификатор, позволяющую нам узнать, загружена спека или нет, и в зависимости от этого разрешаем или запрещаем какие-либо операции.
  3. Мы напишем асинхронный плагин

Разумеется, под разные ситуации нужны разные решения, и можно использовать любое из вышеперечисленных. В моём случае не имело смысла запускать приложение, если не удалось загрузить спеку: это означало глобальные проблемы на сервере и отсутствие каких-либо данных вообще.

Итак, перейдем же к реализации!

Реализация

Есть у JavaScript (как, впрочем, и у других языков) замечательная особенность: возвращать данные из функции. В частности – объекты. Кроме того, в JavaScript функция может быть асинхронной: этим мы и воспользуемся.

В данном примере я буду использовать синтаксис async-await, но того же эффекта можно добиться и промисами.

Итак, представим, что у нас есть проект на Vue, в папке src нам нужны три файла:

# ./src

main.js                # Главный файл приложения
AsyncService.js        # Файл с классом того самого плагина, 
                       # который выполняет асинхронное действие при инициализации
AsyncService.plugin.js # Файл, который будет импортироватсья в main.js

В контексте нашей статьи содержимое файла AsyncService.js значения не имеет. Просто представим, что при его инициализации нужно вызвать один из его асинхронны методов.

Давайте займемся написанием асинхронного плагина. Для этого откроем AsyncService.plugin.js и напишем туда следующее:

// ./AsyncService.plugin.js
import AsyncService from 'AsyncService'

const AsyncServicePlugin = {
    install: function (Vue) {
      Vue.prototype.$service = new AsyncService()
    }
}

export default AsyncServicePlugin

Примерно так выглядит обычный плагин Vue в общем смысле: в прототип добавляется что-либо, что пригодится в будущем.

Теперь представим, что у нашего сервиса есть некий метод asyncInit, который выполняет, как не трудно догадаться, асинхронную инициализацию. Если мы вызовем его в методе install, то поток программы не будет дожидаться его выполнения, даже если мы объявим install как асинхронный. Решение – обернуть все это в асинхронную функцию, и возвращать уже готовый экземпляр AsyncService:

// ./AsyncService.plugin.js
import AsyncService from 'AsyncService'

const initAsyncServicePlugin = async () => {
    const asyncService = new AsyncService()
    await asyncService.asyncInit()
    return {
        install: function (Vue) {
          Vue.prototype.$service = asyncService
        }
    }
}

export default initAsyncServicePlugin

Мы экспортировали уже не сам объект плагина, а функцию, которая его возвращает.

Как же теперь использовать его с Vue.use? На помощь приходят так называемые IIFE, или немедленно вызываемые функциональные выражения. И да – они могут быть асинхронными!

Все, что нужно сделать – обернуть создание экземпляра Vue в такое выражение, предварительно добавив подключение нашего плагина:

// ./main.js
import AsyncServicePlugin from './AsyncService.plugin'
...

(async () => {
    Vue.use(await AsyncServicePlugin())
    new Vue({
     ...
    })
})()

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

Конечно, будет очень хорошо, если вы добавите какую-нибудь визуализацию, типа Load Spinner, который пользователь будет видеть в то время, пока выполняются все асинхронные операции.

Так решил эту проблему я, однако я буду рад узнать другие решения :)