vue-feature-toggles v0.1.5: мультивариантность, переменные, группы и CLI

21.04.2026
vue-feature-toggles v0.1.5: мультивариантность, переменные, группы и CLI

В первой версии плагина флаг — это boolean. Включён или выключен. Этого хватает для большинства кейсов, но на практике быстро упираешься в стену.

A/B-тест с тремя вариантами? Нужен не boolean, а строка 'v1' | 'v2' | 'control'. Тема оформления для отдельного флага? Набор переменных — цвета, лимиты, тексты — которые можно менять независимо от самого флага. Группа бета-флагов, которую можно включить одной командой? Правило, которое вычисляет значение флага на лету по OS-настройкам или роли пользователя?

Всё это требовало отдельной логики в каждом проекте. В 0.1.5 — это встроено в плагин.


Тип флага: boolean | string

Основное изменение — FlagValue теперь boolean | string. Флаг может быть не просто включён/выключен, но и нести вариант.

ts Copy
app.use(FeatureToggles, {
  flags: {
    newDashboard:  true,
    betaSearch:    false,
    checkoutFlow:  'v1',   // мультивариантный флаг
  },
})

isEnabled('checkoutFlow') вернёт true для любого строкового значения, кроме 'false'. Получить сам вариант — через getValue:

ts Copy
const { getValue } = useFeatureProvider()
const variant = getValue('checkoutFlow') // 'v1' | 'v2' | 'control' | boolean

Мультивариантные флаги

Для мультивариантных флагов — два интерфейса. Composable:

ts Copy
const { variant, isVariant } = useFeatureVariant('checkoutFlow')
// variant.value === 'v1'
// isVariant('v2') === false

И компонент <FeatureVariant> с именованными слотами:

vue Copy
<FeatureVariant name="checkoutFlow">
  <template #v1>
    <CheckoutV1 />
  </template>
  <template #v2>
    <CheckoutV2 />
  </template>
  <template #fallback>
    <CheckoutLegacy />
  </template>
</FeatureVariant>

Рендерится только тот слот, чьё имя совпадает с текущим значением флага. Если флаг выключен или значение не совпало ни с одним слотом — #fallback.


Переменные флага

Флаг может нести не только состояние, но и параметры. Переменные задаются в конфиге:

ts Copy
app.use(FeatureToggles, {
  flags: { newDashboard: true },
  variables: {
    newDashboard: {
      accentColor:  '#4f46e5',
      maxWidgets:   6,
      welcomeText:  'Welcome to the new dashboard!',
    },
  },
})

В компоненте:

ts Copy
const { getVariable, setVariable } = useFeatureProvider()

const accentColor = getVariable('newDashboard', 'accentColor') // '#4f46e5'

// Runtime override — только в памяти
setVariable('newDashboard', 'accentColor', '#e74c3c')

// Или с персистентностью
setVariable('newDashboard', 'maxWidgets', 10, { persist: true })

getVariable читает по тому же принципу приоритетов что и флаги: URL → runtime → static. Переменные удобны для A/B-тестов где вариантам нужны разные параметры без хардкода в компонентах.


Группы флагов

Группы — это именованные наборы флагов, которыми можно управлять вместе:

ts Copy
app.use(FeatureToggles, {
  flags: {
    betaSearch:    false,
    aiSuggestions: true,
    newDashboard:  true,
    darkMode:      true,
  },
  groups: {
    beta:   ['betaSearch', 'aiSuggestions'],
    layout: ['newDashboard', 'darkMode'],
  },
})
ts Copy
const { setGroup, resetGroup, isGroupEnabled } = useFeatureProvider()

setGroup('beta', true)    // включить все флаги группы beta
resetGroup('layout')      // сбросить runtime-оверрайды всей группы layout
isGroupEnabled('beta')    // true если все флаги группы включены

Компонент <Feature> поддерживает prop group:

vue Copy
<!-- показать если вся группа beta включена -->
<Feature group="beta">
  <BetaFeaturesBadge />
</Feature>

В DevTools группы вынесены на отдельную вкладку — можно переключать группу целиком, не трогая флаги по одному.


Зависимости флагов

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

ts Copy
app.use(FeatureToggles, {
  flags: {
    betaSearch:    false,
    aiSuggestions: true,  // зависит от betaSearch
  },
  dependencies: {
    aiSuggestions: ['betaSearch'],
  },
})

Если betaSearch === falseisEnabled('aiSuggestions') вернёт false независимо от значения флага. В getFlagSource будет 'dependency'. Реактивно: включаешь betaSearchaiSuggestions сразу становится доступен.


Контекстные правила

Правило — это функция, которая вычисляет значение флага:

ts Copy
app.use(FeatureToggles, {
  rules: {
    // darkMode следует OS-настройкам
    darkMode: () => window.matchMedia('(prefers-color-scheme: dark)').matches,

    // бета-доступ по email домену
    betaFeatures: () => {
      const user = useAuthStore()
      return user.email.endsWith('@company.com')
    },
  },
})

Правила вычисляются внутри computed — реактивны. Менять не нужно: если пользователь переключит тему OS, darkMode обновится сам. Правило можно перекрыть через setFlag или URL-оверрайд — приоритеты те же.


Метаданные и срок действия

ts Copy
app.use(FeatureToggles, {
  meta: {
    newDashboard: {
      description: 'Redesigned dashboard UI',
      owner:       'frontend',
      addedAt:     '2025-01-15',
      ticket:      'PROJ-42',
    },
    christmasBanner: {
      description: 'Seasonal Christmas promotional banner',
      owner:       'marketing',
      addedAt:     '2024-11-01',
      ticket:      'MKTG-12',
    },
  },
  expiry: {
    christmasBanner: '2025-01-10',
  },
})

getMeta('newDashboard') возвращает метаданные флага. Expired флаги показываются в DevTools с жёлтым бейджем. CLI-команда stale находит флаги, которые давно не трогали.


Персистентные оверрайды и профили

setFlag по умолчанию хранит оверрайд в памяти — пропадает при перезагрузке. Опция persist: true записывает в localStorage:

ts Copy
setFlag('darkMode', true, { persist: true })
resetFlag('darkMode')  // убирает и из памяти, и из localStorage

Поверх этого — профили: именованные наборы оверрайдов, которые можно сохранить и переключать между ними.

ts Copy
const { saveProfile, loadProfile, listProfiles } = useFeatureProvider()

saveProfile('qa-beta')   // сохранить текущие runtime-оверрайды как профиль
loadProfile('qa-beta')   // загрузить профиль
listProfiles()           // ['qa-beta', 'dark-mode-test']

В DevTools профили доступны через выпадающий список в футере вкладки Flags.


Живые обновления: SSE и WebSocket

loader запрашивает флаги при инициализации. Для живых обновлений без перезагрузки — liveUpdates:

ts Copy
app.use(FeatureToggles, {
  loader: async () => fetch('/api/flags').then(r => r.json()),

  liveUpdates: {
    type: 'sse',
    url:  '/api/flags/stream',
  },
})

При получении события из SSE/WebSocket плагин делает partial merge во loaderFlags — только те флаги, что пришли в апдейте, остальные не трогаются. Реактивность подхватывает — UI обновляется мгновенно.

ts Copy
// WebSocket вместо SSE
liveUpdates: {
  type:      'websocket',
  url:       'wss://api.app.com/flags',
  eventName: 'flags',
}

SSR-гидрация

В SSR флаги нужны синхронно на старте — до того как loader вернёт результат. Иначе гидрация видит одно состояние (без флагов), а браузер — другое (с флагами), и Vue жалуется на несоответствие.

ts Copy
app.use(FeatureToggles, {
  ssrState: {
    newDashboard: true,
    betaSearch:   false,
    checkoutFlow: 'v2',
  },
  loader: async () => fetch('/api/flags').then(r => r.json()),
})

ssrState заполняет loaderFlags синхронно при инициализации. isReady сразу true, isLoadingfalse. loader всё равно выполнится и обновит флаги, но первый рендер пройдёт с корректными значениями.

В Nuxt — передаётся из server-хэндлера:

ts Copy
// server/api/flags.ts
export default defineEventHandler(async (event) => {
  return getFlagsForUser(event)
})

// app.vue
const { data: serverFlags } = await useFetch('/api/flags')
app.use(FeatureToggles, { ssrState: serverFlags.value })

Типизация флагов

По умолчанию useFeature('anyString') принимает любую строку — нет автодополнения, нет ошибок при опечатке. В 0.1.5 — module augmentation:

ts Copy
// feature-flags.d.ts
import 'vue-feature-toggles'

declare module 'vue-feature-toggles' {
  interface FeatureFlagNames {
    newDashboard:    boolean
    betaSearch:      boolean
    checkoutFlow:    'v1' | 'v2' | 'control'
    aiSuggestions:   boolean
    christmasBanner: boolean
  }
}

После этого useFeature('unknownFlag') — ошибка TypeScript. getValue('checkoutFlow') возвращает 'v1' | 'v2' | 'control'. setFlag('checkoutFlow', 'v3') — ошибка. Автодополнение в IDE работает во всех трёх интерфейсах: компонент, директива, composable.


CLI

bash Copy
npx vue-feature-toggles list
npx vue-feature-toggles check ./src
npx vue-feature-toggles stale --months 3

Три команды, которые читают feature-toggles.config.js из корня проекта.

list — обзор всех флагов

Copy
Flag              Value    Source    Owner        Added       Expiry      Groups
────────────────────────────────────────────────────────────────────────────────
newDashboard      true     static    frontend     2025-01-15              layout
betaSearch        false    static    search-team  2025-06-01              beta
checkoutFlow      v1       static    checkout     2025-09-01
aiSuggestions     true     static    ai-team      2025-10-01              beta
christmasBanner   true     static    marketing    2024-11-01  [EXPIRED]

Expired флаги подсвечиваются жёлтым. Флаги без метаданных — без owner/ticket.

check — аудит кода

Сканирует исходники, находит все обращения к флагам через useFeature, v-feature, isEnabled. Сравнивает с известными флагами из конфига:

Copy
✅ newDashboard
✅ betaSearch
✅ checkoutFlow
❌ newCheckout   — unknown flag. Did you mean: checkoutFlow?
❌ betaSearchV2  — unknown flag. Did you mean: betaSearch?

Неизвестные флаги — предложение с ближайшим совпадением по редакционному расстоянию. Выход с кодом 1 — можно встроить в CI.

stale — устаревшие флаги

bash Copy
npx vue-feature-toggles stale --months 3

Показывает флаги, у которых meta.addedAt старше N месяцев и значение true. Это кандидаты на удаление: фича давно в проде, флаг больше не нужен, но так и остался в коде.


DevTools: три вкладки

Виджет вырос. Раньше — список флагов и несколько кнопок. Теперь — три вкладки с разным контентом и своими элементами управления в футере.

Flags

Список всех флагов с источником, бейджем expired, инлайн-переключением. Новое:

  • Строковые флаги (мультивариантные) — иконка карандаша рядом с значением, по клику появляется <input> прямо в строке. Enter подтверждает, Escape отменяет
  • Кнопка разворачивает секцию переменных для флага. Видны все переменные с текущими значениями, поля для редактирования, кнопка Set
  • Бейдж dep на флаге чья зависимость не выполнена

Footer: выбор профиля из дропдауна, поле для сохранения нового профиля, Reset all, Copy URL (формирует URL с оверрайдами), Export JSON, Import JSON.

Groups

Список групп из конфига. Для каждой — флаги-чипы, статус (все включены / частично / выключены), кнопки Enable all / Reset group.

Footer: Reset all groups.

History

Лог событий: все изменения флагов и переменных с источником и временем. Реактивно — новые события появляются сверху.

Footer: кнопка Clear + счётчик записей.

Drag в пределах вьюпорта

Панель перетаскивается. Позиция зажата в границах окна браузера с учётом реальных размеров панели:

ts Copy
const onMove = (ev: MouseEvent) => {
  const pw = panelRef.value?.offsetWidth  ?? 380
  const ph = panelRef.value?.offsetHeight ?? 420
  pos.value = {
    x: Math.max(0, Math.min(ev.clientX - startX, window.innerWidth  - pw)),
    y: Math.max(0, Math.min(ev.clientY - startY, window.innerHeight - ph)),
  }
}

Панель нельзя утащить за пределы экрана — остаток всегда виден.


Testing и Storybook

ts Copy
// в тестах
import { createFeatureTogglesMock } from 'vue-feature-toggles/testing'

const wrapper = mount(MyComponent, {
  global: {
    plugins: [
      createFeatureTogglesMock({
        newDashboard: true,
        betaSearch:   false,
      }),
    ],
  },
})

createFeatureTogglesMock возвращает упрощённый провайдер без загрузчика и реактивности — для тестов где нужно просто проверить рендер при конкретных значениях флагов.

Storybook:

ts Copy
// .storybook/preview.ts
import { featureTogglesDecorator } from 'vue-feature-toggles/storybook'

export const decorators = [
  featureTogglesDecorator({
    newDashboard: true,
    betaSearch:   false,
  }),
]
ts Copy
// MyComponent.stories.ts
export const WithBeta = {
  parameters: {
    featureToggles: { betaSearch: true },
  },
}

parameters.featureToggles перекрывают дефолтные значения из декоратора. Каждая история запускается с нужным набором флагов — без моков, без обёрток вручную.


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