Доступ к общему контексту времени выполнения приложения Nuxt.

useNuxtApp - это встроенный композабл, предоставляющий доступ к общему контексту времени выполнения Nuxt, также известному как Nuxt-контекст, который доступен как на клиенте, так и на сервере. Он помогает вам получить доступ к экземпляру приложения Vue, runtime-хукам, переменным runtime-конфига и внутренним состояниям, таким как ssrContext и payload.

app.vue
<script setup lang="ts">
const nuxtApp = useNuxtApp()
</script>

Если контекст времени выполнения недоступен в вашей области видимости, useNuxtApp выбросит исключение при вызове. Вместо этого вы можете использовать tryUseNuxtApp для композаблов, которые не требуют nuxtApp, или просто для проверки доступности контекста без исключения.

Методы

provide (name, value)

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

Функция provide принимает параметры name и value.

const nuxtApp = useNuxtApp()
nuxtApp.provide('hello', (name) => `Привет, ${name}!`)

// Выведет "Привет, name!"
console.log(nuxtApp.$hello('name'))

Как видно из примера выше, $hello стал новой пользовательской частью контекста nuxtApp и доступен везде, где доступен nuxtApp.

hook(name, cb)

Хуки, доступные в nuxtApp, позволяют вам настраивать аспекты времени выполнения вашего Nuxt-приложения. Вы можете использовать runtime-хуки в композаблах Vue.js и Nuxt-плагинах, чтобы подключиться к жизненному циклу рендеринга.

Функция hook полезна для добавления пользовательской логики, путем подключения к жизненному циклу рендеринга в определенный момент. Функция hook в основном используется при создании плагинов Nuxt.

Смотрите хуки жизненного цикла для получения информации о доступных runtime-хуках, вызываемых Nuxt.

plugins/test.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:start', () => {
    /* ваш код находится здесь */
  })
  nuxtApp.hook('vue:error', (..._args) => {
    console.log('vue:error')
    // if (import.meta.client) {
    //   console.log(..._args)
    // }
  })
})

callHook(name, ...args)

callHook возвращает промис при вызове любого из существующих хуков.

await nuxtApp.callHook('my-plugin:init')

Свойства

Функция useNuxtApp() открывает следующие свойства, которые вы можете использовать для расширения и настройки вашего приложения, а также для обмена состоянием, данными и переменными.

vueApp

vueApp - это глобальный экземпляр приложения Vue.js, к которому вы можете получить доступ через nuxtApp.

Некоторые полезные методы:

  • component() - Регистрирует глобальный компонент, если передается имя в виде строки и определение компонента, или извлекает уже зарегистрированный компонент, если передается только имя.
  • directive() - Регистрирует глобальную пользовательскую директиву, если передается имя в виде строки и определение директивы, или извлекает уже зарегистрированную, если передано только имя (пример).
  • use() - Устанавливает Vue.js-плагин (пример).
Узнать больше https://ru.vuejs.org/api/application.html#application-api.

ssrContext

ssrContext генерируется во время рендеринга на сервере и доступен только на сервере.

Nuxt предоставляет следующие свойства через ssrContext:

  • url (string) - Текущий url запроса.
  • event (request event - unjs/h3) - Доступ к запросу и ответу текущего маршрута.
  • payload (object) - Объект полезной нагрузки NuxtApp.

payload

payload передает данные и переменные состояния с сервера на клиент. Следующие ключи будут доступны клиенту после того, как они будут переданы с сервера:

  • ServerRendered (boolean) - Указывает, что ответ уже отрендерен на сервере.
  • data (object) - Когда вы получаете данные из конечной точки API, используя либо useFetch, либо useAsyncData, результирующая полезная нагрузка может быть доступна из payload.data. Эти данные кэшируются и помогают предотвратить получение одних и тех же данных в случае, если идентичный запрос выполняется несколько раз.
    <script setup lang="ts">
    const { data } = await useAsyncData('count', () => $fetch('/api/count'))
    </script>
    

    После получения значения count с помощью useAsyncData в примере выше, если вы обратитесь к payload.data, вы увидите { count: 1 }, записанные в нем.
    При обращении к тем же данным payload.data из ssrcontext, вы можете получить доступ к тому же значению и на стороне сервера.
  • state (object) - Когда вы используете композабл useState в Nuxt для установки общего состояния, доступ к этим данным состояния осуществляется через payload.state.[название-вашего-состояния].
  • plugins/my-plugin.ts
    export const useColor = () => useState<string>('color', () => 'pink')
    
    export default defineNuxtPlugin((nuxtApp) => {
      if (import.meta.server) {
        const color = useColor()
      }
    })
    
    Также можно использовать более сложные типы, такие как ref, reactive, shallowRef, shallowReactive и NuxtError.
    Начиная с Nuxt v3.4, можно определить свой собственный редьюсер/ревайвер для типов, которые не поддерживаются Nuxt.
    Посмотрите видео от Александра Лихтера о сериализации полезной нагрузки, особенно в отношении классов.

    В примере ниже мы определяем редьюсер (или сериализатор) и ревайвер (или десериализатор) для класса Luxon DateTime, используя плагин полезной нагрузки.
    plugins/date-time-payload.ts
    /**
     * Такого рода плагины запускаются в самом начале жизненного цикла Nuxt, до того, как мы получим полезную нагрузку.
     * У вас не будет доступа к маршрутизатору или другим внедряемым свойствам Nuxt.
     *
     * Обратите внимание, что строка "DateTime" является идентификатором типа и должна
     * быть одинаковой как на редьюсере, так и на ревайвере.
     */
    export default definePayloadPlugin((nuxtApp) => {
      definePayloadReducer('DateTime', (value) => {
        return value instanceof DateTime && value.toJSON()
      })
      definePayloadReviver('DateTime', (value) => {
        return DateTime.fromISO(value)
      })
    })
    

isHydrating

Используйте nuxtApp.isHydrating (boolean), чтобы проверить, гидрируется ли приложение Nuxt на клиенте.

components/nuxt-error-boundary.ts
export default defineComponent({
  setup (_props, { slots, emit }) {
    const nuxtApp = useNuxtApp()
    onErrorCaptured((err) => {
      if (import.meta.client && !nuxtApp.isHydrating) {
        // ...
      }
    })
  }
})

runWithContext

Скорее всего, вы попали сюда, потому что получили сообщение "Nuxt instance unavailable". Пожалуйста, используйте этот метод осторожно и сообщайте о примерах, вызывающих проблемы, чтобы в конечном итоге их можно было решить на уровне фреймворка.

Метод runWithContext предназначен для вызова функции и передачи ей явного контекста Nuxt. Обычно контекст Nuxt передается неявно, и вам не нужно беспокоиться об этом. Однако при работе со сложными сценариями async/await в middleware/плагинах вы можете столкнуться с ситуацией, когда текущий экземпляр был сброшен после вызова async.

middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to, from) => {
  const nuxtApp = useNuxtApp()
  let user
  try {
    user = await fetchUser()
    // компилятор Vue/Nuxt теряет контекст из-за блока try/catch.
  } catch (e) {
    user = null
  }
  if (!user) {
    // а теперь применяем к нашему вызову `navigateTo` правильный Nuxt-контекст.
    return nuxtApp.runWithContext(() => navigateTo('/auth'))
  }
})

Использование

const result = nuxtApp.runWithContext(() => functionWithContext())
  • functionWithContext: Любая функция, которая требует контекст текущего приложения Nuxt. Этот контекст будет правильно применен автоматически.

runWithContext вернет то, что возвращает functionWithContext.

Более глубокое объяснение контекста

Composition API Vue.js (и композаблы Nuxt аналогично) работают в зависимости от неявного контекста. Во время жизненного цикла Vue устанавливает временный экземпляр текущего компонента (а Nuxt - временный экземпляр nuxtApp) в глобальную переменную и сбрасывает (unset) его в том же тике. При рендеринге на сервере происходит множество запросов от разных пользователей, а nuxtApp работает в одном глобальном контексте. Поэтому Nuxt и Vue немедленно отменяют установку этого глобального инстанса, чтобы избежать утечки общей ссылки между двумя пользователями или компонентами.

Что это значит? Composition API и Nuxt-композаблы доступны только во время жизненного цикла и в том же тике перед любой асинхронной операцией:

// --- Внутренняя часть Vue ---
const _vueInstance = null
const getCurrentInstance = () => _vueInstance
// ---

// Vue / Nuxt устанавливает глобальную переменную, ссылающуюся на текущий компонент, в _vueInstance при вызове setup()
async function setup() {
  getCurrentInstance() // Работает
  await someAsyncOperation() // Vue отменяет контекст в том же тике перед async-операцией!
  getCurrentInstance() // null
}

Классическим решением этого является кэширование текущего экземпляра при первом вызове в локальную переменную типа const instance = getCurrentInstance() и использование ее в следующем вызове композабла, но проблема в том, что тогда любой вложенный вызов композабла должен явно принимать экземпляр в качестве аргумента, а не зависеть от неявного контекста Composition API. Это ограничение дизайна композаблов, а не проблема как таковая.

Чтобы преодолеть это ограничение, Vue выполняет некоторую закулисную работу при компиляции кода нашего приложения и восстанавливает контекст после каждого вызова <script setup>:

const __instance = getCurrentInstance() // Генерируется компилятором Vue
getCurrentInstance() // Работает!
await someAsyncOperation() // Vue "снимает" контекст
__restoreInstance(__instance) // Генерируется компилятором Vue
getCurrentInstance() // Все ещё работает!

Для лучшего описания того, что на самом деле делает Vue, смотрите unjs/unctx#2 (комментарий).

Решение

Именно здесь можно использовать runWithContext для восстановления контекста, аналогично тому, как работает <script setup>.

Nuxt внутренне использует unjs/unctx для поддержки композаблов, подобных Vue, для плагинов и middleware. Это позволяет композаблам, таким как navigateTo(), работать без непосредственной передачи им nuxtApp - привнося DX и преимущества производительности Composition API во весь фреймворк Nuxt.

Nuxt-композаблы имеют тот же дизайн, что и Vue Composition API, и поэтому нуждаются в аналогичном решении, чтобы волшебным образом выполнять это преобразование. Посмотрите unjs/unctx#2 (предложение), unjs/unctx#4 (реализация трансформации) и nuxt/framework#3884 (интеграция в Nuxt).

В настоящее время Vue поддерживает только асинхронное восстановление контекста для <script setup> при использовании async/await. В Nuxt 3 была добавлена поддержка преобразования для defineNuxtPlugin() и defineNuxtRouteMiddleware(), что означает, что при их использовании Nuxt автоматически преобразует их с восстановлением контекста.

Остающиеся проблемы

Преобразование unjs/unctx для автоматического восстановления контекста, похоже, имеет ошибку с операторами try/catch, содержащими await, которая в конечном итоге должна быть решена, чтобы устранить необходимость в предложенном выше обходном пути.

Нативный async-контекст

Используя новую экспериментальную возможность, можно включить нативную поддержку асинхронного контекста, используя AsyncLocalStorage Node.js и новую поддержку unctx, чтобы сделать асинхронный контекст доступным нативно для любого вложенного асинхронного композабла без необходимости преобразования или ручной передачи/вызова контекста.

Нативная поддержка async-контекста в настоящее время работает в Bun и Node.
Узнать больше Docs > Guide > Going Further > Experimental Features#asynccontext.

tryUseNuxtApp

Эта функция работает точно так же, как и useNuxtApp, но вместо исключения возвращает null, если контекст недоступен.

Вы можете использовать ее для композаблов, которые не требуют nuxtApp, или для простой проверки наличия или отсутствия контекста без исключения.

Пример использования:

composable.ts
export function useStandType() {
  // Всегда работает на клиенте
  if (tryUseNuxtApp()) {
    return useRuntimeConfig().public.STAND_TYPE
  } else {
    return process.env.STAND_TYPE
  }
}