vue-stack-tabs
v2.6.2
Published
Vue 3 multi-tab manager built on Vue Router with iframe-like tab isolation: per-tab component scope, stack-based forward/backward navigation, keep-alive page caching, scroll position memory, i18n and a Nuxt 3/4 module.
Maintainers
Readme
vue-stack-tabs
📖 Документация · 🎮 Демо
Библиотека управления вкладками для Vue 3 на основе Vue Router. Она обеспечивает изоляцию вкладок, подобную iframe, через независимые области видимости Vue: каждая вкладка сохраняет собственный экземпляр страницы, кэш и стек навигации.
Возможности
- Вкладки на уровне маршрутов — каждая вкладка владеет независимыми компонентами и кэшем
- Стековая навигация внутри вкладки — forward / backward внутри вкладки, аналогично стеку истории браузера
- Обновление вкладок — обновление одной или всех вкладок с полной пересборкой экземпляров компонентов
- Вкладки iframe — встраивание iframe-страниц с коммуникацией через postMessage
- Сохранение сессии — восстановление последних активных вкладок после обновления браузера
- Запоминание позиции прокрутки — автоматическое восстановление позиции прокрутки при переключении вкладок
- Контекстное меню — встроенные действия закрытия/обновления
- Слоты — кнопки слева/справа на панели вкладок, свои плейсхолдеры загрузки/ошибки iframe
- Интернационализация — встроенные китайский и английский языки, расширяемо
- Модуль Nuxt 3/4 — работает из коробки
Установка
# npm
npm install vue-stack-tabs
# pnpm
pnpm add vue-stack-tabsБыстрый старт (Vue 3)
1. Регистрация плагина
// main.ts
import { createApp } from 'vue'
import VueStackTabs from 'vue-stack-tabs'
import 'vue-stack-tabs/dist/style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.use(VueStackTabs)
app.mount('#app')
vue-stack-tabsподдерживает только один экземпляр<VueStackTabs>на приложение Vue. Экспорт по умолчанию — плагин Vue дляapp.use(VueStackTabs); если нужен локальный импорт компонента, используйте именованный экспорт ниже.
2. Настройка маршрутов
Вкладки зависят от Vue Router. Необходим родительский маршрут для размещения <VueStackTabs>:
// router.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('./Layout.vue'),
children: [
{ path: '', component: () => import('./pages/Home.vue') },
{ path: 'about', component: () => import('./pages/About.vue') },
{ path: 'settings', component: () => import('./pages/Settings.vue') },
// маршрут-заглушка для iframe (обязательно)
{ path: 'iframe', component: () => import('vue-stack-tabs').then((m) => m.IFrame) }
]
}
]
})
export default router3. Использование компонента
<!-- Layout.vue -->
<template>
<div style="width: 100%; height: 100vh">
<VueStackTabs iframe-path="/iframe" :default-tabs="defaultTabs" :max="20" :contextmenu="true" />
</div>
</template>
<script setup lang="ts">
import type { ITabData } from 'vue-stack-tabs'
const defaultTabs: ITabData[] = [
{
title: 'Главная',
path: '/',
closable: false,
refreshable: true
}
]
</script>Если нужен локальный импорт компонента, используйте именованный экспорт VueStackTabs; именованный экспорт StackTab сохранён как алиас совместимости:
import { VueStackTabs as VueStackTabsComponent } from 'vue-stack-tabs'4. Открытие вкладок
import { useTabActions } from 'vue-stack-tabs'
const { openTab, closeTab, refreshTab } = useTabActions()
// Открыть новую вкладку
openTab({
id: 'about', // необязательно, генерируется автоматически
title: 'О приложении',
path: '/about',
query: { id: '1' } // необязательно
})
// Закрыть вкладку
closeTab('about')
// Обновить вкладку
refreshTab('about')Быстрый старт (Nuxt 3/4)
1. Регистрация модуля
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['vue-stack-tabs/nuxt'],
vueStackTabs: {
locale: 'zh-CN' // встроенные варианты: 'zh-CN' | 'en'
},
css: ['vue-stack-tabs/dist/style.css']
})2. Использование в Layout
<!-- layouts/default.vue -->
<template>
<div style="width: 100%; height: 100vh">
<VueStackTabs iframe-path="/iframe" :default-tabs="defaultTabs" />
</div>
</template>
<script setup lang="ts">
import type { ITabData } from 'vue-stack-tabs'
const defaultTabs: ITabData[] = [
{ title: 'Главная', path: '/', closable: false, refreshable: true }
]
</script>3. Использование на страницах
<!-- pages/index.vue -->
<template>
<div>
<h1>Главная</h1>
<button @click="openTab({ title: 'О приложении', path: '/about' })">
Открыть страницу «О приложении»
</button>
</div>
</template>
<script setup>
import { useTabActions } from 'vue-stack-tabs'
const { openTab } = useTabActions()
</script>ESM-only и суб-вход iframe bridge
vue-stack-tabs начиная с текущей версии поддерживает только ESM import; require('vue-stack-tabs') больше не поддерживается.
Основное приложение продолжает использовать корневой вход:
import VueStackTabs from 'vue-stack-tabs'
import 'vue-stack-tabs/dist/style.css'Для iframe-страниц используйте суб-вход без побочных эффектов стилей:
import { postOpenTab, onRefreshRequest } from 'vue-stack-tabs/iframe-bridge'
postOpenTab(
{ title: 'Детали заказа', path: '/orders/1' },
{ targetOrigin: 'https://parent.example.com' }
)
const off = onRefreshRequest(() => window.location.reload(), {
allowedOrigins: ['https://parent.example.com']
})Если targetOrigin не указан, по умолчанию используется window.location.origin страницы iframe, подходит для родительских страниц с тем же источником; для кросс-доменных родительских страниц необходимо явно передать targetOrigin. В продакшене рекомендуется явно передавать и targetOrigin, и allowedOrigins.
Политика безопасности iframe
VueStackTabs поддерживает настройку атрибутов безопасности iframe:
<VueStackTabs
iframe-path="/__iframe"
iframe-sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-same-origin"
iframe-referrer-policy="strict-origin-when-cross-origin"
iframe-allow="fullscreen"
:iframe-load-timeout="15000"
/>Sandbox по умолчанию сохраняет типичные возможности бизнес-страниц и ориентирован на совместимость, не на строгую изоляцию. Для строгой изоляции удалите allow-same-origin и продолжайте уменьшать токены sandbox по потребностям бизнеса. Если ваши страницы того же источника не могут работать под sandbox, можно передать пустую строку для отключения sandbox, но это снижает уровень изоляции.
Справочник API
useTabActions
Композабл для операций на уровне вкладок.
import { useTabActions } from 'vue-stack-tabs'
const {
openTab, // (tab: ITabData, renew?: boolean) => Promise<string | undefined>
closeTab, // (id: string) => string
closeAllTabs, // () => void
refreshTab, // (id: string) => void
refreshAllTabs, // () => void
activeTab, // (id: string, isRoute?: boolean) => void
moveTab, // (id: string, targetIndex: number) => boolean
reset, // () => void
tabs, // Ref<ITabItem[]>
setIFramePath, // (path: string) => void
getWrapper, // ({ tabId?, pageId? }) => HTMLElement | undefined
openInNewWindow // (id: string) => void
} = useTabActions()| Метод | Описание |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| openTab(tab, renew?) | Открыть новую вкладку. При renew=true очищает стек страниц и открывает заново |
| closeTab(id) | Закрыть указанную вкладку, возвращает ID новой активной вкладки |
| closeAllTabs() | Закрыть все закрываемые вкладки |
| refreshTab(id) | Обновить указанную вкладку (заменяет ID кэша, пересоздаёт экземпляр компонента) |
| refreshAllTabs() | Обновить все вкладки |
| activeTab(id) | Активировать указанную вкладку (переключить вкладку) |
| moveTab(id, targetIndex) | Переместить вкладку на указанный индекс; панель вкладок также поддерживает перетаскивание |
| reset() | Закрыть все вкладки и сбросить состояние |
| setIFramePath(path) | Установить путь маршрута-заглушки iframe (вызывается внутри компонента) |
| getWrapper({ tabId?, pageId? }) | Получить DOM-контейнер содержимого; без параметров вернёт первый контейнер, при нескольких вкладках передавайте tabId или pageId |
| openInNewWindow(id) | Открыть вкладку iframe в новом окне (fallback, если встраивание невозможно) |
Закрытие слева / справа / остальных — операции только контекстного меню, выполняются внутри
TabHeader/ContextMenuчерезuseTabPanel.
useTabRouter
Стековая навигация внутри вкладки. Можно использовать только внутри компонентов страниц во вкладке.
import { useTabRouter } from 'vue-stack-tabs'
const { forward, backward, addScrollTarget } = useTabRouter()| Метод | Описание |
| --------------------------- | ----------------------------------------------------------- |
| forward(to) | Перейти вперёд к новой странице внутри текущей вкладки |
| backward(to, backQuery?) | Вернуться назад внутри текущей вкладки |
| addScrollTarget(selector) | Зарегистрировать CSS-селектор DOM для запоминания прокрутки |
forward
// Перейти на страницу /detail
forward({ path: '/detail', query: { id: '123' } })
// Добавить тот же маршрут в стек (один маршрут может быть добавлен многократно, каждый с независимым кэшем)
forward({ path: '/list', query: { page: '2' } })backward
// Вернуться на 1 шаг назад (по умолчанию)
backward(1)
// Вернуться на N шагов
backward(3)
// Вернуться к определённому пути (автоматический поиск в стеке)
backward('/list')
// Вернуться с параметрами (целевая страница получает через props._back)
backward('/list', { result: 'success', data: { id: 1 } })Получение параметров на целевой странице:
<script setup>
// Объявить props для получения параметров обратной навигации
defineProps<{ _back?: { result: string; data: any } }>()
</script>useTabLoading
Управление состоянием загрузки страницы. Можно использовать только внутри компонентов страниц во вкладке.
import { useTabLoading } from 'vue-stack-tabs'
const { openTabLoading, closeTabLoading } = useTabLoading()
// Показать оверлей загрузки
openTabLoading()
// Закрыть после завершения асинхронной операции
fetchData().finally(() => closeTabLoading())Загрузка автоматически закрывается при размонтировании компонента — ручная очистка не требуется.
Свойства (Props)
Свойства компонента <VueStackTabs>:
| Свойство | Тип | По умолчанию | Описание |
| ---------------------- | --------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| iframePath | string | Обязательно | Путь маршрута-заглушки для iframe |
| iframeSandbox | string | allow-scripts allow-forms allow-popups allow-downloads allow-same-origin | Политика sandbox для iframe; по умолчанию совместимость, не строгая изоляция — удалите allow-same-origin для строгой изоляции; пустая строка отключает sandbox (не рекомендуется) |
| iframeReferrerPolicy | ReferrerPolicy | strict-origin-when-cross-origin | Атрибут referrerpolicy для iframe |
| iframeAllow | string | '' | Атрибут allow для iframe, например fullscreen |
| iframeLoadTimeout | number | 15000 | Тайм-аут загрузки iframe в мс |
| defaultTabs | ITabData[] | [] | Начальный список вкладок |
| homeTab | ITabData | — | Закреплённая домашняя вкладка: всегда первая, не закрывается и не перемещается; другие вкладки не могут встать перед ней |
| max | number | 20 | Максимальное количество вкладок |
| contextmenu | boolean \| object | true | Включить контекстное меню |
| pageTransition | string | 'stack-tab-swap' | Имя анимации перехода при навигации вперёд |
| pageTransitionBack | string | 'stack-tab-swap-back' | Имя анимации перехода при навигации назад |
| tabTransition | string \| TransitionProps | 'stack-tab-zoom' | Эффект перехода при добавлении/удалении вкладок |
| tabScrollMode | TabScrollMode | 'both' | Режим прокрутки панели вкладок: 'wheel' / 'button' / 'both' |
| space | number | 300 | Шаг прокрутки панели вкладок (px) |
| width | string | '100%' | Ширина контейнера |
| height | string | '100%' | Высота контейнера |
| i18n | string | 'zh-CN' | Локаль интернационализации, встроенные 'zh-CN' / 'en' |
| globalScroll | boolean | false | Включить запоминание прокрутки на уровне страницы |
| sessionPrefix | string | '' | Префикс ключа sessionStorage; полезно для нескольких экземпляров на одном источнике |
| iframeAllowedOrigins | string[] | [] (плюс текущий источник) | Разрешённые источники для iframe postMessage |
События (Events)
| Событие | Параметры | Описание |
| -------------- | -------------- | -------------------------------------------- |
| onActive | (id: string) | Срабатывает при активации вкладки |
| onPageLoaded | — | Срабатывает при загрузке компонента страницы |
<VueStackTabs
iframe-path="/iframe"
@on-active="(id) => console.log('active:', id)"
@on-page-loaded="() => console.log('page loaded')"
/>Слоты
| Слот | Параметры слота | Описание |
| --------------- | ---------------- | -------------------------------------------------------------------- |
| leftButton | — | Левая область панели вкладок, обычно с TabHeaderButton |
| rightButton | — | Правая область панели вкладок |
| iframeLoading | { tab } | Свой плейсхолдер загрузки для вкладки iframe |
| iframeError | { tab, retry } | Свой плейсхолдер ошибки для вкладки iframe; retry() — перезагрузка |
<VueStackTabs iframe-path="/iframe">
<template #leftButton>
<TabHeaderButton title="Развернуть" @click="collapse = !collapse" />
</template>
<template #iframeError="{ tab, retry }">
<span>{{ tab.title }} не загрузилась</span>
<button @click="retry">Повторить</button>
</template>
</VueStackTabs>Вкладки iframe
Открытие вкладки iframe
openTab({
title: 'Внешняя страница',
path: 'https://example.com',
iframe: true
})Операции внутри iframe-страниц
Внутри вкладки iframe можно взаимодействовать с хост-контейнером через postMessage для открытия новых вкладок или обработки действий обновления.
1. Использование утилит Bridge (рекомендуется)
Если iframe-страница может ссылаться на код библиотеки (или импортировать инструменты из iframeBridge.ts), рекомендуется этот подход:
import { postOpenTab, onRefreshRequest } from 'vue-stack-tabs/iframe-bridge'
const parentOrigin = 'https://parent.example.com'
// Открыть вкладку
postOpenTab(
{
title: 'Новая страница',
path: '/detail',
query: { id: '1' }
},
{ targetOrigin: parentOrigin }
)
// Слушать запросы на обновление
const off = onRefreshRequest(
() => {
// Пользовательская логика обновления
location.reload()
},
{ allowedOrigins: [parentOrigin] }
)2. Нативная интеграция
Если невозможно импортировать утилиты библиотеки или требуется нулевая зависимость, используйте нативный API:
const parentOrigin = 'https://parent.example.com'
// Открыть вкладку
window.parent.postMessage(
{
type: 'vue-stack-tabs:openTab',
payload: {
title: 'Нативная новая страница',
path: '/detail'
}
},
parentOrigin
)
// Слушать запросы на обновление
window.addEventListener('message', (ev) => {
if (ev.origin !== parentOrigin) return
if (ev.source !== window.parent) return
if (ev.data?.type === 'vue-stack-tabs:refresh') {
// Выполнить обновление
location.reload()
}
})Режимы обновления iframe
При вызове openTab можно указать поведение обновления через iframeRefreshMode:
postMessage(по умолчанию): хост отправляет сообщениеvue-stack-tabs:refreshв iframe; внутренняя страница сама решает, как обновляться (более плавная анимация).reload: хост напрямую сбрасываетsrcилиkeyiframe для принудительной перезагрузки браузером (подходит для кросс-доменных или немодифицируемых страниц).
Интернационализация
Встроенные языки: zh-CN (китайский), en (английский).
// Переключить язык через prop компонента
// <VueStackTabs iframe-path="/iframe" i18n="en" />
// Или при установке плагина
app.use(VueStackTabs, [{ locale: 'en' }])Пользовательский языковой пакет (messages — вложенный объект, глубоко сливается со встроенными языками):
app.use(VueStackTabs, [
{
locale: 'ru-RU',
messages: {
VueStackTab: {
close: 'Закрыть',
reload: 'Обновить',
loading: 'Загрузка'
// ...
}
}
}
])Определения типов
import type { LocationQueryRaw } from 'vue-router'
/** Данные, передаваемые при открытии вкладки */
interface ITabData {
id?: string // ID вкладки (автоматически генерируемый UUID, если не указан)
title: string // Заголовок вкладки
path: string // Путь маршрута или URL iframe
query?: LocationQueryRaw // Параметры маршрута
closable?: boolean // Можно ли закрыть (по умолчанию true)
refreshable?: boolean // Можно ли обновить (по умолчанию true)
iframe?: boolean // Является ли вкладкой iframe (по умолчанию false)
iframeRefreshMode?: 'postMessage' | 'reload' // Способ обновления iframe
}
/** Режим прокрутки панели вкладок */
enum TabScrollMode {
WHEEL = 'wheel',
BUTTON = 'button',
BOTH = 'both'
}Структура проекта
src/lib/
├── StackTabs.vue # Главный компонент
├── index.ts # Точка входа и экспорты
├── hooks/ # Основная логика
│ ├── useTabActions.ts # Публичный API
│ ├── useTabRouter.ts # Навигация внутри вкладки
│ ├── useTabLoading.ts # Состояние загрузки
│ └── useTabPanel.tsx # Ядро движка
├── model/TabModel.ts # Определения типов
├── components/ # UI-компоненты
├── nuxt/ # Модуль Nuxt
└── assets/style/ # СтилиПодробное описание архитектуры см. в ARCHITECTURE.md.
