Search for a command to run...
В этом руководстве вы узнаете, как витрина подбирает регион продаж для нового посетителя, как включить определение по IP через ip-api и как добавить собственный провайдер определения.
У посетителя, который ещё не открывал магазин, региона нет. Вместо того чтобы брать регион, который бэкенд вернул первым, витрина определяет страну в middleware и сохраняет её в cookie , откуда её затем читают все страницы и корзина. Само определение реализовано как подключаемый провайдер, так же как автозаполнение адреса.
считывает активный провайдер из переменной окружения и переключается по нему:
src/lib/geolocation/detect.ts1export const resolveGeolocationProvider = (): GeolocationProvider => {2 switch (true) {3 case isIpApi(geolocationProvider):4 return ipApiProvider5 default:6 return platformProvider7 }8}
читает , а сверяет значение со строкой , оба определены в том же файле. Любое другое значение, включая незаданную переменную, проваливается в , который читает geo-заголовки, уже выставленные вашим хостингом.
Middleware обращается к фиче в одном месте. возвращает функцию, которая дописывает cookie к тому ответу, который в итоге уйдёт на запрос, поэтому приоритет cookie, цепочка резервных вариантов и отладочные заголовки остаются внутри :
src/middleware.tsconst applyCountry = await resolveCountry(request, regionMap)
Порядок разрешения одинаков для любого провайдера: сначала уже существующая cookie , затем ответ провайдера, затем , затем первый регион из ответа бэкенда. Cookie идёт первой, потому что в неё пишет переключатель регионов в шапке, и определение не должно перебивать выбор, который посетитель сделал сам.
В задано , но не является кодом страны, поэтому ни один регион из seed-данных под него не подходит. Остаётся последнее звено цепочки, первый регион из ответа бэкенда, а это произвольная страна. Укажите реальный код ISO 3166-1 alpha-2 в нижнем регистре, например или , чтобы при неудачном определении посетитель попадал туда, куда вы решили.
Провайдер по умолчанию не требует настройки и не делает сетевых запросов. Он читает страну, которую ваша платформа уже определила, в таком порядке:
| Источник | Платформа |
|---|---|
| Cloudflare Workers | |
| Vercel | |
| Cloudflare | |
| Google App Engine | |
| Fastly | |
| , | nginx GeoIP или собственный прокси |
Значения, означающие «страна неизвестна», а это , и , отбрасываются, а не принимаются за страну: поиск региона по всё равно не удастся и уведёт посетителя в цепочку резервных вариантов.
На Vercel и Cloudflare этого достаточно. Заголовок бесплатен, приходит мгновенно и точнее любого сторонного сервиса, поэтому там оставьте незаданной.
Развёртываниям без geo-заголовков, а также локальной разработке, нужен настоящий поиск по IP. Задайте провайдер в :
apps/storefront/.env.local1GEOLOCATION_PROVIDER=ip-api2GEOLOCATION_PROVIDER_API_KEY=
Ни одна из переменных не начинается с : определение работает только на сервере, и ключ не должен попадать в браузерную сборку.
Провайдер сначала проверяет заголовки хостинга и обращается к ip-api только если ни один из них не принёс страну, поэтому на Vercel его включение ничего не стоит. Удачный поиск по IP посетителя кэшируется на час, а одновременные запросы по одному IP склеиваются в один вызов. Провайдер также соблюдает лимиты ip-api: он читает заголовки ответа и и перестаёт обращаться к сервису на секунд, когда лимит исчерпан или пришёл ответ . Именно это не даёт получить бан на час при высокой нагрузке.
ip-api работает и без ключа, но его бесплатный эндпоинт доступен только по HTTP, разрешает 45 запросов в минуту, а условия использования запрещают коммерческое применение, приводя в качестве примера недопустимого «местную валюту или ближайший магазин для посетителей интернет-магазина». Магазин делает с результатом ровно это, поэтому боевому магазину нужен платный ключ. С заданным ключом провайдер обращается к по HTTPS, и больше ничего менять не нужно.
читает сначала , затем , , , и , и приводит найденное к каноническому виду: убирает порт, убирает скобки IPv6 и разворачивает IPv4-mapped адрес IPv6, так что распознаётся как петлевой адрес . От того, что получилось, зависит одно из трёх поведений:
| Адрес клиента | Поведение |
|---|---|
| Публичный адрес | Поиск по собственному IP посетителя, кэш на час, таймаут 1,5 с |
| Петлевой или из приватного диапазона | Посетитель находится в сети сервера, поэтому ip-api вызывается без IP и определяет исходящий адрес самого сервера. Без кэша, таймаут 5 с |
| Ни в одном заголовке адреса нет | Страна не определяется, работает цепочка резервных вариантов |
Вторая строка и делает так, что и адрес в локальной сети разрешаются в страну, из которой машина реально выходит в интернет, одинаково в и в сборке через и . Поскольку там ничего не кэшируется, смена VPN и удаление меняют регион на следующем же запросе. Это стоит одного обращения к сервису на каждую загрузку страницы без cookie, поэтому такое поведение ограничено посетителями из сети сервера.
Последняя строка намеренно не делает предположений. Определение по исходящему адресу сервера для посетителя с неизвестным IP привязало бы всех таких посетителей к стране датацентра. Если развёртывание не определяет страну совсем, значит стоящий перед ним прокси не передаёт .
Задайте , и middleware начнёт добавлять к каждому ответу то, что он решил. Флаг работает в любом режиме, включая production-сборку:
apps/storefront/.env.localGEOLOCATION_DEBUG=true
| Заголовок | Значение |
|---|---|
| , или одна из причин | |
| Что вернул провайдер, если ничего не определил | |
| , или , когда ответила cookie | |
| Адрес посетителя, или | |
| Страна, которая применилась |
Откройте панель сети, перезагрузите страницу и прочитайте заголовки на запросе документа. означает, что cookie не была удалена, означает, что переменная не дошла до сервера, а источник называет, какое звено цепочки дало ответ.
Страну, которую middleware определить не смог, он сохраняет на пять минут, а не на год, поэтому одна неудачная попытка не привяжет посетителя к резервному региону навсегда. Страну, выбранную посетителем в переключателе регионов, и страну, которая действительно определилась, он сохраняет на год.
Каждый провайдер реализует один и тот же контракт из двух полей:
src/lib/geolocation/types.ts1export type GeolocationContext = {2 headers: Headers3 cf?: { country?: string | null }4}56export type GeolocationProvider = {7 name: string8 lookup: (context: GeolocationContext) => Promise<string | null>9}
возвращает код ISO 3166-1 alpha-2 в нижнем регистре или , если ничего не определил. Поле нужно не для вида: оно попадает в ключ кэша, поэтому два провайдера никогда не читают результаты друг друга.
Чтобы добавить провайдер для другого сервиса или для локальной базы вроде MaxMind GeoLite2:
Ориентироваться лучше на : это более короткий пример, 50 строк вообще без сетевых запросов. А показывает, что добавляет настоящий поиск: таймаут, соблюдение лимитов и ветвление по адресу клиента.