cdek-pvz-widget
v0.1.0
Published
Lightweight CDEK pickup-point (PVZ) picker with server-side city filtering. Loads offices for a single city (~1MB) instead of the whole world (~19MB). Drop-in compatible with @cdek-it/widget (window.CDEKWidget).
Maintainers
Readme
cdek-pvz-widget
Лёгкий виджет выбора пункта выдачи (ПВЗ/постамат) СДЭК. В отличие от официального
@cdek-it/widget, грузит пункты по городу пользователя (серверная фильтрация
service.php по fias_guid/city_code) — ~1 МБ на город вместо ~19 МБ на весь мир,
поэтому открывается за ~1–2 с даже на ПК.
- Drop-in по основным опциям: глобал
window.CDEKWidget,new CDEKWidget({...}),onChoose(mode, tariff, address). - Совместим с API СДЭК: ходит в тот же
service.php(action=offices). - Без зависимостей и без привязки к конкретному магазину: ключи, геокодер и тема передаются хостом.
- ESM + UMD, типы
.d.ts, i18n (ru/eng), тема, a11y.
Неофициальный проект, не аффилирован со СДЭК.
Зачем (мотивация)
Официальный @cdek-it/widget на старте грузит весь мировой список ПВЗ (~19 МБ, 10k+ точек) и
оборачивает его в реактивность — на десктопе окно выбора пункта может открываться 15–20 секунд.
Проблема подробно разобрана в статье «Виджет СДЭК» — rant.dimius.ru.
Это решение грузит пункты по городу пользователя через тот же service.php
(fias_guid/city_code) — ~1 МБ вместо ~19 МБ, поэтому открывается за ~1–2 с. Независимая
реализация с упором на производительность и совместимость с API СДЭК.
Установка
npm i cdek-pvz-widgetили через CDN (UMD): <script src="https://unpkg.com/cdek-pvz-widget"></script> → window.CDEKWidget.
Быстрый старт
import CDEKWidget from 'cdek-pvz-widget';
const widget = new CDEKWidget({
root: 'cdek-map', // <div id="cdek-map" style="height:600px"></div>
apiKey: 'YANDEX_MAPS_KEY', // ключ Yandex Maps JS API
servicePath: 'https://host/cdek/service.php',
defaultLocation: 'Москва',
// геокодер инжектится хостом (строгая сепарация):
geocoder: CDEKWidget.createDadataGeocoder({token: 'DADATA_TOKEN'}),
onChoose: (mode, tariff, address) => {
console.log(address.city, address.address, address.code);
},
});
// widget.destroy();Контейнеру root нужно задать высоту, иначе карта схлопнется.
Геокодер (обязателен для серверной фильтрации)
Чтобы грузить офисы по городу, виджету нужен city_code или fias_guid города.
Их даёт геокодер — выберите готовый адаптер или передайте свой:
| Адаптер | Даёт код города | Замечание |
|----------------------------------------------------------|-----------------------|---------------------------------------------------------------|
| CDEKWidget.createCdekLocationGeocoder({ servicePath }) | city_code | Дефолт. Требует action=location в service.php (см. ниже). |
| CDEKWidget.createDadataGeocoder({ token }) | fias_guid | Токен виден в браузере — используйте доменно-ограниченный. |
| CDEKWidget.createYandexGeocoder({ apiKey }) | — (только координаты) | Для центрирования; серверно фильтровать по городу не может. |
Свой геокодер — любой объект, реализующий интерфейс:
interface Geocoder {
suggest(query: string, signal?: AbortSignal): Promise<CityRef[]>;
resolve?(query: string, signal?: AbortSignal): Promise<CityRef | null>;
}
interface CityRef {
name: string;
region?: string;
cityCode?: number;
fiasGuid?: string;
lat?: number;
lon?: number
}Опционально reverse(lat, lon) — обратное геокодирование центра карты для режима followMap (есть у DaData-адаптера).
Требования к service.php
service.php — серверный прокси СДЭК (его раздаёт СДЭК; дорабатывается на вашем бэкенде).
GET ?action=offices— обязателен (есть в стоковомservice.php). Виджет шлётfias_guid/city_codeи фильтры.GET ?action=location&query=...— нужен только для дефолтногоcreateCdekLocationGeocoder. Проксируйте к СДЭКGET /v2/location/suggest/cities(или/v2/location/cities) и верните массив[{ code, city, region, fias_guid, longitude, latitude }]. Если экшена нет — используйте DaData/Yandex адаптер или передайтеinitialCity.
CSP хоста должен разрешать домены: api-maps.yandex.ru (карта) и домен геокодера (например suggestions.dadata.ru).
Хосты внешних ресурсов передавайте в рантайме, не зашивая в код виджета и не запекая билд-переменными:
mapsApiUrl (опция виджета), url у адаптеров геокодера (createDadataGeocoder({ url }),
createYandexGeocoder({ url })),
servicePath. У всех есть дефолты для standalone-использования, но хост (например из своего рантайм-конфига) может их
переопределить.
Опции
| Опция | Тип | По умолчанию | Описание |
|-----------------------|--------------------------------------------------------|-----------------------------------|------------------------------------------------------------------------------|
| root | string | — | id контейнера. Обязательно. |
| apiKey | string | — | Ключ Yandex Maps. Обязательно. |
| servicePath | string | — | URL service.php. Обязательно. |
| mapsApiUrl | string | https://api-maps.yandex.ru/2.1/ | База Yandex Maps API. Передавайте хост в рантайме. |
| geocoder | Geocoder | CDEK location | Источник городов. |
| defaultLocation | string \| [lng,lat] | — | Стартовый город. |
| initialCity | CityRef | — | Готовый город (минует геокодер). |
| fallbackCity | CityRef | — | Резерв, если резолв не удался. |
| selectedPoint | { code?, lat?, lon? } | — | Восстановить выбранный пункт при открытии (центр + подсветка). |
| followMap | boolean | false | Определять город по центру карты при перемещении (нужен geocoder.reverse). |
| sender | boolean | false | Режим отправителя (handout-флаги). |
| canChoose | boolean | true | Показывать кнопку выбора. |
| hideFilters | { type, have_cashless, have_cash, is_dressing_room } | — | Скрыть фильтры. |
| forceFilters | OfficeFilters | — | Принудительные значения фильтров (уходят в запрос). |
| hideDeliveryOptions | { office, door } | — | Скрыть типы доставки. |
| lang | 'rus' \| 'eng' | 'rus' | Язык. |
| labels | Partial<Labels> | — | Переопределение текстов. |
| theme | Theme | палитра СДЭК | Переопределение цветов (CSS-переменные). |
| debug | boolean | false | Дебаг-логи + метрики производительности в консоль (console.debug). |
| from | string \| FromLocation | — | Адрес отправления для расчёта тарифов. |
| goods | Parcel[] | — | Габариты/вес посылок (см/г). Без них тарифы не считаются. |
| tariffs | { office, door, pickup } | дефолты СДЭК | Белые списки tariff_code по типам доставки. |
| currency | CurrencyCode | 'RUB' | Валюта расчёта (в API уходит числом). |
| tariff | Tariff \| null | — | Фиксированный тариф (тогда calculate не вызывается). |
| selected | { door, office } | — | Предвыбранные точка/адрес. |
| popup | boolean | false | Модальный режим: методы open()/close(). |
| requirePostcode | boolean | false | Курьер: требовать индекс, иначе onChoose не вызывается. |
| onReady | () => void | — | После первой загрузки. |
| onChoose | (mode, tariff, address) => void | — | Выбор: mode office/door, tariff Tariff\|null, address офис или адрес.|
| onCalculate | (prices, location) => void | — | После расчёта: prices = { office, door, pickup }. |
| onError | (error) => void | — | Ошибки config/maps/offices/geocoder/calculate. |
Совместимость с официальным @cdek-it/widget
| Возможность | Статус |
|---|---|
| root, apiKey, servicePath, defaultLocation, from, sender, canChoose, hideFilters, hideDeliveryOptions, goods, tariffs, currency, tariff, selected, popup, lang, onReady, onChoose, onCalculate | ✅ совпадают |
| Выбор ПВЗ + расчёт тарифов (onChoose('office', tariff, office), onCalculate) | ✅ |
| Курьерская доставка door (адрес → расчёт → onChoose('door', tariff, address)) | ✅ |
| Popup-режим: open() / close() | ✅ |
| Методы addParcel/getParcels/resetParcels/clearSelection/updateLocation/updateTariff/selectOffice | ✅ |
| geocoder, mapsApiUrl, labels, theme, initialCity, fallbackCity, followMap, selectedPoint, onError | ➕ расширения |
Адрес курьера геокодируется через Yandex Geocoder (как у официального виджета). Расчёт тарифов требует непустой
goods и заданный from — иначе tariff в onChoose будет null.
Методы
widget.open() / widget.close() // popup-режим
widget.addParcel(parcel | parcel[]) // добавить посылку(и), пересчитать
widget.getParcels() // текущие посылки (копия)
widget.resetParcels() // очистить посылки
widget.updateTariff(tariff | null) // зафиксировать/снять тариф
widget.clearSelection() // сбросить выбор
widget.updateLocation('Город' | CityRef) // сменить город
widget.selectOffice(code) // выбрать пункт по коду
widget.destroy() // снять виджетТема
new CDEKWidget({ /* ... */ theme: {accent: '#1AB248', accentDark: '#158E3A', text: '#44403C'}});Любое поле theme → CSS-переменная --cdek-* на корне; можно стилизовать и обычным CSS по .cdek-widget.
Разработка
npm install
npm run dev # демо-страница (index.html), ключи из .env (см. .env.example)
npm run build # dist/: ESM + UMD + .d.ts
npm run typecheck
npm run lint
npm testОтладка и метрики
С debug: true виджет пишет в console.debug метрики производительности (с префиксом [cdek-widget]):
- тайминги:
ymaps load,geocode initial city,map init,offices fetch [Город],render list+map,total mount→ready,reverse geocode (followMap); - по офисам: число пунктов и реальный сетевой размер ответа (
transfer/decodedиз PerformanceResourceTiming); - снимки JS-heap (
used / total / limit) на ключевых этапах (где доступно — Chromium); - события
followMap(определённый город),destroy.
Этого достаточно, чтобы оценить, сколько виджет «ест» по сети/CPU/памяти и где узкое место. В проде debug держите
выключенным.
Безопасность
- Ключ Yandex Maps виден в браузере (так работает их JS API) — используйте доменно-ограниченный ключ из кабинета Yandex.
- Токен DaData тоже виден на клиенте — берите доменно-ограниченный токен или проксируйте через свой backend-геокодер.
- Не включайте
debug: trueв проде (вonError/консоль может попасть содержимое сетевых ошибок). - Данные пунктов из
service.phpэкранируются перед вставкой в DOM/балун; виджет не используетinnerHTMLс непроверенными данными.
Лицензия
MIT.
