Розробка системи компонентів на 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 тижні |
Система компонентів — це інвестиція в підтримуваність проекту. Перший компонент написати швидко, але в хаотичній архітектурі; десятий — вже болісно дублювати. Правильна система з першого компонента економить час на кожному наступному.







