Розробка фронтенду на TypeScript для 1С-Бітрікс
Розрив між PHP-бекендом Бітрікс та фронтендом на ванільному JavaScript — стандартна історія. Компоненти каталогу, кошик, фільтри, особистий кабінет — кожна з цих сутностей живе на перетині PHP-логіки та клієнтського коду. TypeScript закриває прогалини на цьому стику: контракт між PHP та JS описується у типах, а компілятор ловить невідповідності до деплою.
Розробка фронтенду на TypeScript для 1С-Бітрікс
Архітектура фронтенду Бітрікс-проекту
Два полярних підходи до фронтенду Бітрікс:
-
MPA (Multi-Page Application) — класичний підхід. PHP рендерить сторінки, TypeScript додає інтерактивність: фільтр на AJAX, кошик через REST, галереї, карти. Кожен компонент — окремий ізольований TS-модуль.
-
SPA/Headless — React або Vue на TypeScript споживають Бітрікс як API. Повний фронтенд написаний на TypeScript, PHP забезпечує лише дані та бізнес-логіку.
Більшість проєктів — перший підхід з елементами другого в критичних частинах (каталог, кошик).
Структура фронтенду в шаблоні Бітрікс
/local/templates/my_site/
├── src/
│ ├── components/
│ │ ├── catalog/
│ │ │ ├── CatalogFilter.ts
│ │ │ ├── ProductCard.ts
│ │ │ └── CartButton.ts
│ │ ├── cart/
│ │ │ ├── CartDrawer.ts
│ │ │ └── CartCounter.ts
│ │ └── common/
│ │ ├── Modal.ts
│ │ └── Tooltip.ts
│ ├── api/
│ │ ├── catalog.ts
│ │ ├── cart.ts
│ │ └── user.ts
│ ├── types/
│ │ ├── bitrix.d.ts
│ │ └── api.ts
│ ├── utils/
│ │ ├── http.ts
│ │ └── format.ts
│ └── main.ts
├── dist/
├── package.json
├── tsconfig.json
└── vite.config.ts
Компонент кошика на TypeScript
Повноцінний приклад компонента кошика з типами:
// api/cart.ts
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
img: string | null;
}
interface CartState {
items: CartItem[];
totalPrice: number;
totalCount: number;
currency: string;
}
interface AddToCartPayload {
productId: number;
quantity: number;
properties?: Record<string, string>;
}
export async function addToCart(payload: AddToCartPayload): Promise<CartState> {
const formData = new FormData();
formData.append('sessid', BX.bitrix_sessid());
formData.append('action', 'addItem');
formData.append('product_id', String(payload.productId));
formData.append('quantity', String(payload.quantity));
if (payload.properties) {
Object.entries(payload.properties).forEach(([k, v]) => {
formData.append(`props[${k}]`, v);
});
}
const res = await fetch('/local/ajax/cart.php', {
method: 'POST',
body: formData,
});
if (!res.ok) throw new Error(`Cart error: ${res.status}`);
const json = await res.json();
if (json.status !== 'success') throw new Error(json.error ?? 'Cart error');
return json.cart as CartState;
}
// components/cart/CartButton.ts
import { addToCart } from '@/api/cart';
import { updateCartUI } from './CartCounter';
export function initAddToCartButtons(): void {
document.querySelectorAll<HTMLButtonElement>('[data-action="add-to-cart"]')
.forEach(btn => btn.addEventListener('click', handleAddToCart));
}
async function handleAddToCart(e: MouseEvent): Promise<void> {
const btn = e.currentTarget as HTMLButtonElement;
const productId = Number(btn.dataset['productId']);
if (!productId) return;
btn.disabled = true;
btn.classList.add('loading');
try {
const cart = await addToCart({ productId, quantity: 1 });
updateCartUI(cart.totalCount, cart.totalPrice);
showAddedFeedback(btn);
} catch (err) {
console.error('Add to cart failed:', err);
showError(btn);
} finally {
btn.disabled = false;
btn.classList.remove('loading');
}
}
Інтеграція з PHP-компонентами через data-атрибути
Передача даних з PHP у TypeScript без глобальних змінних:
// template.php компонента каталогу
<div
id="catalog-app"
data-section-id="<?= (int)$arResult['SECTION']['ID'] ?>"
data-iblock-id="<?= (int)$arParams['IBLOCK_ID'] ?>"
data-initial-filter='<?= htmlspecialchars(
json_encode($arResult['FILTER_PARAMS']), ENT_QUOTES
) ?>'
>
<?php // SSR-верстка для першого завантаження ?>
</div>
// TypeScript зчитує дані типізовано
const appEl = document.getElementById('catalog-app');
if (!appEl) throw new Error('#catalog-app not found');
const sectionId = Number(appEl.dataset['sectionId']);
const iblockId = Number(appEl.dataset['iblockId']);
const rawFilter = appEl.dataset['initialFilter'] ?? '{}';
const initFilter = JSON.parse(rawFilter) as FilterState;
Стан без React: патерн EventEmitter
Для MPA-проєктів не завжди потрібен React. Легкий EventEmitter синхронізує компоненти:
// utils/EventBus.ts
type Handler<T> = (payload: T) => void;
class EventBus {
private handlers: Map<string, Handler<unknown>[]> = new Map();
on<T>(event: string, handler: Handler<T>): void {
const list = this.handlers.get(event) ?? [];
list.push(handler as Handler<unknown>);
this.handlers.set(event, list);
}
emit<T>(event: string, payload: T): void {
this.handlers.get(event)?.forEach(h => h(payload as unknown));
}
}
export const bus = new EventBus();
// Використання: кошик сповіщає хедер
bus.emit<CartState>('cart:updated', newCartState);
bus.on<CartState>('cart:updated', (cart) => {
updateCartCounter(cart.totalCount);
});
Збірка та деплой
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
root: 'src',
build: {
outDir: '../dist',
emptyOutDir: true,
rollupOptions: {
input: {
main: 'src/main.ts',
catalog: 'src/pages/catalog.ts',
cart: 'src/pages/cart.ts',
},
},
},
resolve: {
alias: { '@': '/src' },
},
});
Окремі entrypoint для кожного розділу сайту — лише потрібний JS завантажується на сторінці.
Терміни
| Завдання | Терміни |
|---|---|
| Налаштування TypeScript + Vite, архітектура модулів | 1–2 дні |
| Розробка компонентів каталогу (фільтр, список, картка) | 3–5 днів |
| Розробка кошика та міні-кошика в шапці | 2–3 дні |
| Міграція існуючого JS-коду на TypeScript | 2–5 днів (залежить від обсягу) |







