vue-feature-toggles — управление feature flags во Vue 3

20.04.2026
vue-feature-toggles — управление feature flags во Vue 3

Написал плагин для управления feature flags во Vue 3. Расскажу, зачем, как работает и какие задачи решает.


Проблема

Стандартный подход выглядит так:

vue Copy
<template>
  <div v-if="user.features.includes('newDashboard')">
    <NewDashboard />
  </div>
  <div v-else-if="betaFlags.betaSearch">
    <BetaSearchBar />
  </div>
</template>

Флаги размазаны по компонентам, логика включения дублируется, никакого единого места где посмотреть что включено. QA хочет проверить новую фичу — деплоим специальную ветку или комментируем код. Нужно экстренно отключить сломавшуюся фичу — лезем в код, деплоим фикс.

Плагин решает это: единый провайдер флагов, три интерфейса (компонент, директива, composable), URL-оверрайды для QA и runtime-управление.


Три интерфейса

Компонент <Feature>

Основной способ работы — оборачиваешь контент в компонент:

vue Copy
<Feature name="newDashboard">
  <NewDashboard />
</Feature>

Если newDashboard === false — ничего не рендерится. Через слоты — fallback:

vue Copy
<Feature name="betaSearch">
  <template #default>
    <BetaSearchBar />
  </template>
  <template #fallback>
    <LegacySearchBar />
  </template>
</Feature>

Fallback можно задать и строкой через prop — когда не хочется создавать отдельный слот:

vue Copy
<Feature name="betaSearch" fallback="Функция в разработке">
  <BetaSearchBar />
</Feature>

Есть inverted — показать именно когда флаг выключен. Удобно для режима обслуживания:

vue Copy
<Feature name="maintenanceMode" :inverted="true">
  <MainContent />
</Feature>

Prop tag оборачивает содержимое в HTML-тег. Без него — фрагмент, лишнего DOM-узла нет.

Пока флаги грузятся через loader — показывается слот #loading.


Директива v-feature

Работает как v-show — через display: none, не пересоздаёт DOM:

vue Copy
<!-- включить если флаг ON -->
<div v-feature="'newDashboard'">...</div>

<!-- включить если флаг OFF -->
<div v-feature:not="'betaSearch'">...</div>

<!-- включить если ОБА включены (AND) -->
<div v-feature="['newDashboard', 'betaSearch']">...</div>

Директива реактивна — когда флаг меняется (через setFlag или URL-оверрайд), display обновляется автоматически. Внутри — watchEffect с WeakMap для cleanup при unmount.


Composable useFeature

Для логики в <script setup>:

ts Copy
// Один флаг → Ref<boolean>
const isNewDashboard = useFeature('newDashboard')

// Несколько → Record<string, Ref<boolean>>
const { newDashboard, betaSearch } = useFeature(['newDashboard', 'betaSearch'])

// AND по нескольким → Ref<boolean>
const allEnabled = useFeature('newDashboard', 'betaSearch')

Всё реактивно — computed внутри отслеживает флаги и пересчитывается при изменениях.


Как настраивается

ts Copy
app.use(FeatureToggles, {
  // Статические флаги
  flags: {
    newDashboard: true,
    betaSearch:   false,
  },

  // Загрузка с бэкенда — любая async-функция
  loader: async () => {
    const res = await fetch('/api/flags')
    return res.json()
  },

  reloadInterval: 60_000,  // перезагрузка каждую минуту

  urlOverrides: true,      // по умолчанию true в dev, false в prod
  urlPrefix: 'feature',    // ?feature:flagName=true
  defaultValue: false,     // если флаг не найден нигде
})

loader — это просто функция, которая возвращает Record<string, boolean>. Можно подключить что угодно: REST, LaunchDarkly, localStorage, роль пользователя:

ts Copy
// Флаги на основе подписки
loader: async () => {
  const user = await getUser()
  return {
    advancedReports: user.plan === 'pro',
    apiAccess:       user.plan === 'enterprise',
    newDashboard:    true,
  }
}

Порядок приоритетов

Copy
URL overrideruntime setFlag() → loader → static flags → defaultValue

URL-параметр перекрывает всё. setFlag — runtime-оверрайд, который живёт в памяти. Loader-результат подхватывается после загрузки и реактивно обновляет UI. Статический объект flags — база. defaultValue — запасной вариант.


Низкоуровневый доступ

Через useFeatureProvider — полный контроль:

ts Copy
const {
  flags,         // Ref<Record<string, boolean>> — все флаги реактивно
  isLoading,     // Ref<boolean>
  isReady,       // Ref<boolean>
  isEnabled,     // (name: string) => boolean
  getFlagSource, // (name: string) => 'url' | 'runtime' | 'loader' | 'static' | 'default'
  setFlag,       // runtime override
  resetFlag,     // убрать runtime override
  resetAll,      // убрать все runtime overrides
  reload,        // перезапустить loader
} = useFeatureProvider()

getFlagSource возвращает откуда конкретно пришло значение флага — полезно для дебага и для DevTools.


URL-оверрайды

Когда urlOverrides: true, параметры вида ?feature:flagName=true перекрывают значение флага. Оверрайды читаются реактивно через popstate/hashchange — перезагрузка страницы не нужна.

Copy
https://staging.app.com/checkout?feature:newCheckout=true&feature:legacyCart=false

QA открывает ссылку — видит новую фичу. Остальные пользователи её не видят. Флаг в бэкенде не тронут.

Оверрайды живут только в памяти — не попадают в localStorage, не влияют на других.


DevTools-панель

<FeatureDevTools> — фиксированный оверлей в правом нижнем углу для работы с флагами во время разработки.

vue Copy
<FeatureDevTools v-if="isDev" />

Что показывает: имя флага, текущее значение ON/OFF, источник значения (цветной бейдж: url, runtime, loader, static, default), кнопки toggle и reset для каждого флага, кнопки «Reset all» и «↺ reload». Панель сворачивается.

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


Nuxt-модуль

Для Nuxt — отдельный модуль:

ts Copy
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['vue-feature-toggles/nuxt'],

  featureToggles: {
    flags: {
      newDashboard: true,
      betaSearch:   false,
    },
    urlOverrides: true,
  },
})

<Feature> и v-feature регистрируются глобально. Провайдер доступен вне компонентов:

ts Copy
// plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
  const { $featureToggles } = nuxtApp
  await $featureToggles.reload()
})

Если нужен loader с Nuxt — создаёшь плагин напрямую, без модуля:

ts Copy
// plugins/feature-toggles.ts
import { FeatureToggles } from 'vue-feature-toggles'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(FeatureToggles, {
    loader: async () => $fetch('/api/flags'),
    urlOverrides: true,
  })
})

Сценарии применения

Тарифы подписки

Фичи доступны в зависимости от плана пользователя. Флаги вычисляются на бэкенде после авторизации:

ts Copy
loader: async () => {
  const user = await getUser()
  return {
    advancedReports: user.plan === 'pro',
    exportPDF:       user.plan !== 'free',
    apiAccess:       user.plan === 'enterprise',
  }
}
vue Copy
<Feature name="advancedReports">
  <AdvancedReports />
  <template #fallback>
    <UpgradeBanner feature="advancedReports" />
  </template>
</Feature>

Изменить доступность фичи — задача бэкенда, не деплоя фронтенда.


QA тестирует через URL без деплоя

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

Copy
https://staging.app.com/checkout?feature:newCheckout=true

Открыл ссылку — видит новую версию. Закрыл вкладку — всё вернулось. Ссылку можно передать коллеге — он увидит то же самое.

Работает без изменений в коде: urlOverrides: true включён по умолчанию в dev-режиме.


Экстренное отключение фичи

В продакшне сломалась фича — приходит алерт от мониторинга. Нужно убрать её из UI немедленно, не ждать деплоя:

ts Copy
const { setFlag } = useFeatureProvider()
setFlag('newPaymentFlow', false)

Изменение реактивно — все <Feature name="newPaymentFlow"> и v-feature="'newPaymentFlow'" в DOM скрываются мгновенно. Если флаги хранятся на бэкенде и включён reloadInterval — достаточно поменять значение там, фронтенд подхватит при следующей загрузке.


Защита роутов

Фича доступна только пользователям с нужным планом. Вместо проверок в каждом компоненте — один guard:

ts Copy
// router/guards.ts
import { useFeatureProvider } from 'vue-feature-toggles'

router.beforeEach((to) => {
  const { isEnabled } = useFeatureProvider()

  if (to.meta.feature && !isEnabled(to.meta.feature as string)) {
    return { name: 'NotFound' }
  }
})
ts Copy
// routes
[
  { path: '/analytics', component: Analytics, meta: { feature: 'advancedReports' } },
  { path: '/api-console', component: ApiConsole, meta: { feature: 'apiAccess' } },
]

Попытка открыть недоступный раздел — 404. Флаги загружаются один раз при старте через loader, guard работает синхронно.


NPM: https://www.npmjs.com/package/vue-feature-toggles
GitHub: https://github.com/macrulezru/vue-feature-toggles

Читать далее

20.04.2026

Vue Network Dashboard: трансформация ответов, брейкпоинты и OpenAPI-импорт

Новая порция фич для встраиваемого отладчика сети: теперь можно модифицировать реальные ответы на лету, замораживать запросы как в Charles Proxy и генерировать моки из OpenAPI-спеки одним кликом.

Метки
vuetypescriptdevtoolsnetworkdebuggingopensource