Продвинутое использование
Отладка: история ошибок
/devtools — не интеграция с браузерным расширением Vue Devtools, а небольшая, не имеющая зависимостей история в памяти, которую можно добавить на страницу во время разработки:
import { createErrorHistory, ErrorHistoryPanel } from 'vue-error-boundary-kit/devtools'
const history = createErrorHistory({ limit: 50 })<ErrorBoundary :reporter="[sentryReporter, history.record]">…</ErrorBoundary>
<ErrorHistoryPanel v-if="isDev" :history="history" />history.record сам по себе ErrorReporter — он использует существующий механизм репортеров, поэтому дополнительная разводка не нужна. history.entries — реактивный массив, сначала самые новые (ограничен limit, по умолчанию 50); <ErrorHistoryPanel> — опциональный, не имеющий зависимостей компонент, отрисовывающий его (со встроенными стилями, отдельный импорт CSS не нужен) с кнопкой «Очистить».
Почему не полноценная интеграция с расширением Vue Devtools? Рассмотрено и отклонено.
@vue/devtools-apiсам не свободен от зависимостей — он тянет за собой@vue/devtools-kitи его собственное дерево зависимостей, а минимальный забандленный вызовsetupDevtoolsPlugin()весит около 20 кБ gzip сам по себе — более чем в 9 раз больше всего базового бюджета этого пакета (~2.1 кБ gzip). В отличие от адаптеров Sentry/Bugsnag/LogRocket, нет способа зависеть от него «структурно», не бандля его, — у devtools-инспектора нет уже инициализированного внешнего клиента, на который можно опереться.<ErrorHistoryPanel>закрывает ту же потребность в отладке без этой цены и без требования устанавливать расширение вообще.
А как насчёт кастомной вкладки Nuxt DevTools (
@nuxt/devtools-kit)? Тоже рассмотрено и отклонено — это не тот же вопрос, что про браузерное расширение выше, но вывод тот же.addCustomTab()реален и ничего не стоил бы в продакшене (работает только внутриnuxi dev), но реактивное состояниеcreateErrorHistory()живёт в браузерном инстансе приложения, а вкладка devtools регистрируется изsetup()модуля, который выполняется в Node.js. Единственный тип представления, поддерживающий живые данные (iframe, указывающий на обслуживаемый роут dev-сервера), требует собственного RPC-моста клиент↔devtools-сервер (extendServerRpc/iframe-client) — отдельное небольшое SPA и протокол, а не быстрое добавление, — и экосистема сейчас в процессе перехода на мажорную версию (latestу@nuxt/devtools-kitна npm сейчас —4.0.0-alpha; стабильная линия —3.4.1).<ErrorHistoryPanel>уже закрывает ту же потребность везде, независимо от того, открыт DevTools или нет.
Тестирование приложения
vue-error-boundary-kit/testing — тестовые дублёры и фикстуры для проверки кода, использующего <ErrorBoundary>/useErrorBoundary()/адаптер, framework-агностичные помимо самого Vue (без встроенной зависимости от vi.fn()/jest.fn(), поэтому работает одинаково под Vitest или Jest):
import {
ThrowInRender, // выбрасывает ошибку в render() при `shouldThrow` (по умолчанию true); проп `message`
ThrowInSetup, // выбрасывает синхронно в setup() — source: 'render'
ThrowInAsyncSetup, // выбрасывает после await в async setup() — source: 'async'; рендерите внутри <Suspense>/<AsyncBoundary>
ThrowAbortError, // выбрасывает DOMException AbortError, соответствующий отменённому fetch()
makeCapturedError, // собрать фикстуру CapturedError для юнит-теста репортера/адаптера
createRecordingReporter, // тестовый дублёр ErrorReporter: { calls, report(), reset() }
} from 'vue-error-boundary-kit/testing'import { mount } from '@vue/test-utils'
import { h, nextTick } from 'vue'
import { ErrorBoundary } from 'vue-error-boundary-kit'
import { ThrowInRender, createRecordingReporter } from 'vue-error-boundary-kit/testing'
const reporter = createRecordingReporter()
const wrapper = mount(ErrorBoundary, {
props: { reporter },
slots: {
default: () => h(ThrowInRender, { message: 'boom' }),
fallback: ({ error }) => h('div', { class: 'fallback' }, error.message),
},
})
await nextTick() // onErrorCaptured синхронно устанавливает реактивное состояние; замена DOM — нет
expect(wrapper.find('.fallback').text()).toBe('boom')
expect(reporter.calls[0]?.error.source).toBe('render')Заметки о SSR
<ErrorBoundary> SSR-безопасен в самом важном смысле: упавшее поддерево никогда не обрушивает renderToString и не превращается в полноценную страницу 500, а события ошибок и репортеры срабатывают на сервере корректно, точно так же, как на клиенте.
Есть одно честное ограничение, о котором стоит знать, коренящееся в том, как работает SSR-рендерер Vue, а не в самом пакете: на клиенте установка реактивного состояния в onErrorCaptured запускает настоящий второй проход рендера, так что разметка слота fallback заменяет упавший контент. У серверного рендерера Vue нет эквивалентного шага «перерисовки» — render() компонента уже вернулся к моменту, когда перехватывается сбой потомка, поэтому серверный HTML для этой конкретной позиции выходит как пустой placeholder, а не как разметка самого слота fallback. Достижение пиксель-идеального fallback HTML при SSR потребовало бы либо внутренних API рендерера, либо повторного выполнения setup() упавшего поддерева во второй раз — от обоих подходов этот пакет намеренно отказывается (см. готовность к Vapor-режиму ниже).
Это было подтверждено эмпирически, а не просто предположено: оборачивание слота default в <Suspense> не меняет результат ни для синхронного throw, ни для отклонённого асинхронного setup() — SSR-буферизация <Suspense> откладывает только неразрешённые асинхронные зависимости, чтобы зафиксировать свою ветку #default, как только они устаканятся; у него нет механизма, публичного или приватного, зафиксировать свою ветку #fallback, когда зависимость вместо этого отклоняется. Голый <Suspense> вообще без границы обработки ошибок вокруг, чья ветка #default отклоняет setup(), всё равно сериализуется в пустой placeholder — подтверждая, что это не специфика того, как этот пакет использует onErrorCaptured.
Что гарантировано и покрыто тестами:
renderToStringникогда не выбрасывает исключение из-за сбоя внутри границы.- Соседний контент рендерится нормально вокруг упавшего поддерева.
- Событие
errorи любой настроенный репортер срабатывают ровно один раз на сервере, точно так же, как на клиенте. - Гидратация всегда сходится к корректному, интерактивному клиентскому контенту — даже если сервер и клиент в итоге расходятся во мнении о том, упало ли данное поддерево (например, fetch, упавший только на сервере, к моменту гидратации на клиенте уже успешно завершился). Собственное восстановление после расхождения при гидратации во Vue может залогировать свой стандартный dev-only warning в этом случае расхождения (вырезается из продакшен-сборок); конечное состояние всегда корректно.