Разработка системы компонентов на Vue.js для 1С-Битрикс
Один Vue-компонент на сайте — это интеграция. Система компонентов — это архитектура. Разница принципиальная: в системе компоненты делят общее состояние, общий дизайн-токен, единый механизм инициализации и взаимодействия с Битрикс-бэкендом. Без системного подхода через полгода на сайте появляются три версии корзины, два разных подхода к AJAX и дублирующийся код в каждом компоненте.
Что входит в систему компонентов
Типичная система Vue-компонентов для Битрикс-магазина:
- Ядро: конфигурация приложения, Pinia stores, API-слой, утилиты
- UI-компоненты: кнопки, инпуты, модалки, тосты — не зависят от бизнес-логики
- Бизнес-компоненты: корзина, избранное, сравнение, отзывы, фильтр — используют stores и API
- Точки монтирования: места в PHP-шаблоне, куда монтируются Vue-компоненты
Структура файлов
/local/js/vue/
├── app.ts # инициализация и монтирование
├── api/
│ ├── client.ts # базовый HTTP-клиент
│ ├── cart.ts
│ ├── catalog.ts
│ └── wishlist.ts
├── stores/
│ ├── cartStore.ts
│ ├── wishlistStore.ts
│ ├── compareStore.ts
│ └── userStore.ts
├── components/
│ ├── ui/ # дизайн-система
│ │ ├── Button.vue
│ │ ├── Modal.vue
│ │ ├── Toast.vue
│ │ ├── Spinner.vue
│ │ └── Badge.vue
│ ├── cart/
│ │ ├── CartButton.vue # кнопка корзины в шапке
│ │ ├── CartDrawer.vue # выдвижная панель корзины
│ │ └── CartItem.vue
│ ├── catalog/
│ │ ├── AddToCartBtn.vue
│ │ ├── WishlistBtn.vue
│ │ └── CompareBtn.vue
│ └── product/
│ ├── Reviews.vue
│ └── SizeAdvisor.vue
└── types/
├── cart.ts
├── product.ts
└── user.ts
Механизм инициализации
Проблема с множеством Vue-компонентов на одной странице: нельзя создавать отдельное приложение (createApp) для каждого — они не будут делить Pinia stores, и состояние корзины в шапке будет отличаться от состояния в поп-апе.
Правильный подход: одно приложение, множество точек монтирования.
// app.ts
import { createApp, defineAsyncComponent } from 'vue'
import { createPinia } from 'pinia'
// Регистрация компонентов для монтирования по data-атрибутам
const componentRegistry: Record<string, any> = {
'cart-button': defineAsyncComponent(() => import('./components/cart/CartButton.vue')),
'add-to-cart': defineAsyncComponent(() => import('./components/catalog/AddToCartBtn.vue')),
'wishlist-btn': defineAsyncComponent(() => import('./components/catalog/WishlistBtn.vue')),
'compare-btn': defineAsyncComponent(() => import('./components/catalog/CompareBtn.vue')),
'reviews': defineAsyncComponent(() => import('./components/product/Reviews.vue')),
'size-advisor': defineAsyncComponent(() => import('./components/product/SizeAdvisor.vue')),
}
// Создаём одно приложение с Pinia
const pinia = createPinia()
// Монтируем компоненты в каждый элемент с data-vue-component
document.querySelectorAll('[data-vue-component]').forEach((el) => {
const name = el.getAttribute('data-vue-component')!
const Component = componentRegistry[name]
if (!Component) return
// Парсим props из data-атрибутов
const props: Record<string, any> = {}
for (const attr of el.attributes) {
if (attr.name.startsWith('data-prop-')) {
const propName = attr.name.replace('data-prop-', '').replace(/-./g, m => m[1].toUpperCase())
props[propName] = JSON.parse(attr.value)
}
}
const app = createApp(Component, props)
app.use(pinia) // Один экземпляр Pinia — общее состояние
app.mount(el)
})
В PHP-шаблоне:
<!-- Шапка: кнопка корзины -->
<div data-vue-component="cart-button"></div>
<!-- Карточка товара: кнопки добавления -->
<div data-vue-component="add-to-cart"
data-prop-product-id="<?= $product['ID'] ?>"
data-prop-price="<?= $product['PRICE'] ?>"></div>
<div data-vue-component="wishlist-btn"
data-prop-product-id="<?= $product['ID'] ?>"></div>
Ключевой момент: один экземпляр pinia передаётся во все приложения. Это означает, что cartStore в шапке и cartStore на карточке товара — одно и то же хранилище, состояние синхронизировано.
API-слой: единый HTTP-клиент
// api/client.ts
const CSRF_TOKEN = (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Bitrix-Csrf-Token': CSRF_TOKEN,
...options.headers,
},
})
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
const data = await res.json()
if (data.errors?.length) throw new Error(data.errors[0].message)
return data
}
export const apiGet = <T>(url: string) => request<T>(url)
export const apiPost = <T>(url: string, body: unknown) =>
request<T>(url, { method: 'POST', body: JSON.stringify(body) })
Все компоненты используют apiGet / apiPost — единая точка для добавления авторизационных заголовков, логирования ошибок, перехватчиков.
Дизайн-система: токены и UI-компоненты
Цвета, шрифты, отступы — через CSS-переменные, которые совпадают с переменными в PHP-шаблоне Битрикс:
/* Определяются в шаблоне Битрикс, используются Vue-компонентами */
:root {
--color-primary: #0052cc;
--color-success: #00875a;
--color-danger: #de350b;
--spacing-sm: 8px;
--spacing-md: 16px;
--border-radius: 4px;
}
Vue-компонент Button.vue использует эти переменные — визуально совместим с остальным сайтом.
Ленивая загрузка и code splitting
defineAsyncComponent + Vite автоматически разбивает код на чанки. CartDrawer.vue загружается только когда пользователь нажимает кнопку корзины — не в начальном бандле. Это критично для производительности: начальная страница не загружает код компонентов, которые возможно никогда не откроются.
// Загрузка только при первом взаимодействии
const CartDrawer = defineAsyncComponent({
loader: () => import('./components/cart/CartDrawer.vue'),
loadingComponent: Spinner,
delay: 200,
})
Тестирование системы
Юнит-тесты Pinia stores через Vitest:
// stores/cartStore.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from './cartStore'
describe('cartStore', () => {
beforeEach(() => setActivePinia(createPinia()))
it('добавляет товар в корзину', async () => {
const store = useCartStore()
await store.add(123, 1)
expect(store.items).toHaveLength(1)
expect(store.count).toBe(1)
})
})
Компонентные тесты через Vue Test Utils + Vitest.
Сборка и интеграция с Битрикс
Vite настраивается для сборки в папку /local/js/vue/dist/. В шаблоне Битрикс подключается через:
// В header.php шаблона
\Bitrix\Main\Page\Asset::getInstance()->addJs('/local/js/vue/dist/app.js', true);
// Или через type=module для нативных ES-модулей
?>
<script type="module" src="/local/js/vue/dist/app.js"></script>
Сроки
| Масштаб | Что входит | Срок |
|---|---|---|
| Базовая система (3–5 компонентов) | Инициализация, Pinia, API-слой, UI-база | 3–5 недель |
| Полноценная система | + корзина, избранное, сравнение, отзывы | 6–10 недель |
| + Дизайн-система, тесты, CI | + токены, Vitest, автосборка | +2–3 недели |
Система компонентов — это инвестиция в поддерживаемость проекта. Первый компонент написать быстро, но в хаотичной архитектуре; десятый — уже болезненно дублировать. Правильная система с первого компонента экономит время на каждом следующем.







