Сінхронізацыя кошыка пакупак між вікнамі браузера: BroadcastChannel протыка localStorage
Чаму ў падзеях сховішча localStorage перадаюцься ідэнтычныя даны, чаму способы з Date.now не ўсегда працуюць надзеямо, і як BroadcastChannel у поўнай з’едначэнні з надзеямым сховішчам рашаюць проблему індыкатораў кошыка між вікнамі браузера.
Крыніцы: Frontend Interview Question Asked in Flipkart: Sync a Shopping Cart Across Browser Tabs
Аблякаты корзіны ў розных вікнах здаюцца задачай, якая можа быць выконана за пяць хвілін, пакуль тыкет QA не падтвердзіць, што першая ідэя насправды не працюе. Прыклад, паказаны ніжэй, апавяртаеся ў звычайнай сцэнарыю інтэрв’ю: вікна з той самай праекцыяй, без спакульнай памяці, без запытоў да сервера — толькі інструменты платформы браузера.
Сцэнарый
Пакупальнік залішае два вікна з товарамі ачымі. Ён дадае тавар у вікно A. Аблякат у заголовку вікна B павінен адразу апоштова падаць новыя даны. Калі не так, у вікна B будзе адображацца стары пісак, і пакупальнік будзе думаць, што дадаванне не выйшла.
Абмежэнняя, накладзеныя інтэрв’юерам: вікна не можа дзеліцца стакамі JavaScript, а для самага сігналу забораненыя мерыджы паветрам. Паведамленне павінна быць пераданае платформай.
Першая спроба: запісы зберагача
Большасць кандыдаў вяртаецца да localStorage і адпаведнага слухача storage. Запіс у аднам вікна паведамляе іншыя вікна той жа праекцыі.
// Tab that adds the item
function addToCart(sku) {
cart.add(sku);
renderBadge();
localStorage.setItem('cart-sync', JSON.stringify({
type: 'CART_ADD', sku, qty: 1
}));
}
// Every other tab
addEventListener('storage', (e) => {
if (e.key !== 'cart-sync') return;
const msg = JSON.parse(e.newValue);
if (msg.type === 'CART_ADD') { cart.add(msg.sku); renderBadge(); }
});
Этот скетч частаў працюе без проблем. Пасля гэтага з’яўляецца тыкет.
Атхэнты QA, якія спачатку працююць
Ёнкераванне: дадаць той самы SKU два разы. У вікні A паказваецца колькасць 2, у вікні B — 1. Пасля перзагрузкі вікна B нарэшце паказваецца 2.
Нічога не выклікае памылак. Журналы спакойныя. Код выглядае правільна — тады чаму іншы працэс не пазначыў апдэйт?
Ключовая інфармацыя — перзагрузка восстанавляе стан: стан «выtrывальны» быў правільным; толькі сігнал у рэальны час не працаваў.
Асновная прычына: аднаковыя значэнні ігнаруюцца
Алгорытм HTML setItem завершае свою працу, калі прыйнятыя даныя збігаюцца з тымі, якія ужо схованы для данага ключа — па спецыфікацыі «калі паказванае значэнне равна новаму значэнню, прыпыніць». Не выканана ніякая запіс у дыск, ніякая трансляцыя, ніякое працэсаванне іншых працэсаў.
Ідэнтычныя дзеянні ў кошыку можу быць серыялізаваныя у тые ж байты:
{"type":"CART_ADD","sku":"SKU-1029","qty":1}
Таблетка A яшчэ і пасля свайго add выконвае дзейства локальна. Таблетка B ніколі не атрымляе запуску змены storage. Пасля перзагрузкі таблетка B зноў чытае даны з аховання і вяршыць нормальна — класычная проблема пад час тэставання.
Чаму дуплікаты здаюцца рэдкімі, але такія ёсць
Прыемы, якія змінююць значэння (перш A, потым B, пасля знову A), продовжаюць працаваць. Проблемы выступаюць у разе ідэнтычных падрадных пакетаў даных:
- Двойныя клікі, якія дадаюць тое ж самае SKU
- Сігналы стану, якія зноў прадаюць незменены статус
"ONLINE" - Паўтаральныя паведамленні пра
SESSION_EXPIRED, калі таблетка ўсё яшчэ запускаецца
Гэтыя саме моменты, калі іншыя працоўныя елементы больш за ўсё патрэбуюць адзвягання.
«Унікальнасць» часовых пазначак ўразламная
У звычных патчах у JSON дадаецца Date.now(), таму строкі становяцься разнымі:
localStorage.setItem('cart-sync', JSON.stringify({
type: 'CART_ADD', sku, qty: 1,
t: Date.now() // force the value to differ
}));
Часы, які фіксуюць час у мілісекундах, сталкаюцца тады, калі два запісу відбываюцца за аднаковы промяжак часу. Тое, чы рэшэнне будзе эфектыва, залежыць ад удачы планавальніка — інодзе так, інодзе няўсёлі. Такая правільнасць, якая залежыць ад раздробленасці часу, не ўважаецца правільнасцю. Калі трэба застацца ў рэжыме зберагання, лепш вжываць crypto.randomUUID() (альбо іншы надзвычайна унікальны токен).
Адчысцэнне стварае подвойную доставку
Залішанне унікальных дадзеных назаўсёды ёсць проблематычным, таму люды негайна removeItem пасля setItem. Гэта выклікае два спавешчэння: адна за запіс, другая за выдаленне.
event 1 → { key:'cart-sync', oldValue: null, newValue: '{"type":"CART_ADD",…}' }
event 2 → { key:'cart-sync', oldValue: payload, newValue: null }
Калі трыгер не ігноруе значэнне newValue === null, партнер два разы застосоўвае зміны кашты. Дызайн, у яком збераганне выступае як шына, цяпер выкалікае патрэбу ў унікальнасці, захопленні ад нуля, парсаванні дадзеных і ўсуненні зайвага — таму што API ёсць хранальнікам ключ/значэнне, які часам выдае сігналы, а не чэргай паведамленняў.
Найболей падобныя засобы: BroadcastChannel
Калі мета — толькі адправка паведамленняў, а не ўтрымванне іх, выкорыстоўваюцца API для адправкі паведамленняў:
const bus = new BroadcastChannel('cart-sync');
// send — the same message, as many times as you like
bus.postMessage({ type: 'CART_ADD', sku, qty: 1 });// receive
bus.onmessage = (e) => {
if (e.data.type === 'CART_ADD') { cart.add(e.data.sku); renderBadge(); }
};addEventListener('pagehide', () => bus.close());
Павтарэнне ідэентычных об’ектаў таксама дапамагае. Не існуе скорачэння вычысленняў за аднаковасцю. Структураваны клон падтрымлівае болей багатыя тыпы, чым JSON (Date, Map, Set, тыпаваныя масівы). Спачатку спрыймайце схованне як базу дадзенаў з неабавязковымі паведамленнямі пра змяны, а BroadcastChannel — як паведамленне без базы дадзенаў.
Дааднейшыя пытанні, якія маюць значэнне ў практычным выкарыстоўванні
Самаадправка. Об’ект канала адправкі не прыме яго сябецкага паведамлення, але іншы экземпляр канала з тым самым іменам у том жа докумэнце — прыме, так сама як і iframes з таго ж вучэльніка. Неабходна адмахнуцца ад дуплікацый, калі на одной сторунцы ёсць калькі падпісчыкаў.
Сінхронна vs асінхронная перадача. Даныя практычна асінхронным спосабом кашэруюцца ў цыкле падзейнаў прыёмніка. Клонаванне пад час postMessage являецца сінхронным і негаўнуча адхоўляе значэнні, якія нельга клонаваць (DataCloneError для функцый).
Вікна, ачыненыя пазней. Каналы не перагледваюць історыю. Вікно, ачыненое пасля дадзення новых даных, яшчэ адображае стары значак, калі толькі не чытае даныя з стойкага сховішча. Тактыка, якая выкарыстоўваецца:
// The shape that actually ships:
// durable store = the truth, channel = the doorbell
async function addToCart(sku) {
await idbPut('cart', sku); // atomic, survives reloads
bus.postMessage({ type: 'CART_CHANGED' }); // just a signal
}
bus.onmessage = async () => renderBadge(await idbCount('cart'));
Стойкія даныя ўважаюцца правдзей; канал — толькі сігнал, які спавяшчае пра змену гэтай правды.
Лічылка ў сховішчы. Чытанне/змена/запіс даных межа вікнамі прыводзіць да адныяўленняў; платформа не прысвайкае механізма блакавання. Не трэба ствараць распадзеленую лічылку на localStorage.
Дзе храненне яшчэ застаецца лепшым выборам. Някі настройкі, такія як тэматыка чыста рэгіон, якія павінны быць прачытаны сінхронна перад адражаванням і рэдкая зменяюцца, лепей выкорыстоўваць з storage адбудовамі; для значэнняй крашчэ вжыць storage, а для адбудов — BroadcastChannel.
Самаперакананне
За дапамогою простаг падходу з роўным калічынай дадзеных, калі два ідэнтычныя запісы стваруюцца з разніцай у адзін секунду, сколькі адбудов будзе стварацца?
localStorage.setItem('cart-sync', '{"sku":"SKU-1029"}');
// … one second later, same product added again …
localStorage.setItem('cart-sync', '{"sku":"SKU-1029"}');
Адказ: адна адбудова. Час, які прыйшоў, не мае значэння; толькі зменена строка спрыяе паведамленню.
Заключанне інтэрв’ю
Для сігналаў межа кансольваў вжывайце BroadcastChannel, а IndexedDB чыста падобныя базы дадзеных залішайце як асновны істочнік дадзеных, і паказвайце прыклад раннейга вярнення з роўным значэннем, калі пропонуецца варыянт з храненням як шыньбусам. Упомяніце ліміты пераадражавання дадзеных у іншыях кансольвах, мультыканальные адбудовы-эха, а таксама прычыны, чаму лямованыя калькуляторы ў localStorage не функцыонуюць.
Выводы
Сінхронізацыя інтэрфейсу между вкладкамі — гэта проблема кароткага перадачы паведамленняяў, якая маскіруецца пад проблему зберагчыка. Спрыяйце тое, каб стан і паведамленні былі тратаваны як адзінольныя слоі, выберыце API, якія падходзяць кожнаму слою, і працавайце з ідэнтычнымі последовымі дзеяннямі — у навучальных прыкладах гэтыя дзеяння прыпускаюцца, а рэальныя корыстувачы сталкнуцца з неабяжным натысканням кнопак.
Дадзенні для рэальнай працы
Мобільныя браузеры можаць агрэсывна адхоўляць фонавыя вкладкі; апдэйт індыкатора на сыгнале visibilitychange, які зноў чытае даныя з надзейныга зберагчыка, памагае у тых случаях, калі паведамленне пропускаецца чытачам, калі той знаходзіцца у стане «заморожання». Спалучыце гэта з Channel для вкладак у першанай пярэдзі.
Автаматызаваныя тэсты должны працаваць у двух кантэкстах (вкладкі Playwright) і пераканацца, што даныя зберагчыка залішаюцца ідэнтычнымі, а таксама што паведамленні доставляюцца праз BroadcastChannel. Дакументавайце выбраную схему ключа для надзейных даных, каб будучы сінхронізацыйныя працэсы моглі ўжываць іх без падтворэння новага крыніцы правды.
Флагі функцыяў інодзе кантролююць працэс адображэння «жываго бейджа». Зберагачыце даныя, якія трэба застаўляць незалежна ад усіх налагоджэнняў; толькі элемент дзвонка можа быць необавязковым. У працоўнай суперангу флаг выключаны будзе назаўседы адмініструвацца не так, як у тых працоўных, дзе флаг увімкнуты.
З точкі зору безпекі ніколі не кладзіце токены аўтэнтыфікацыі ў сообшчэннях localStorage. ID тавароў у кошыку ўсё гаразд; але секрэты сесій — няўжо. Лепшая выкорыстоўваць незрозумелыя ідэнтыфікаторы і дазволіць кожной працоўнай чытаць прывілейованыя дакументы з каналаў httpOnly або з памяці пасля пераканальнай перацэнкі сесіі.
Калі інтэрфейс магазіна распадаецца на калькольвыя паддомены, BroadcastChannel не зможа пераводзіць даныя межы ўсіх іх. Альтэрнатывамі являюцца спяльны працоўнік на аднам загальным домене-родзічы, або запуск з боку сервера падазрэнняў, ключаваных за ідэнтыфікаторам кошыка. Неабходна якомога раней падчас пераглядоў дызайну адзначыць гэтыя обмежэння.
Інтарнацыоналізацыя бейджа (правіла для множкі) павінна выконвацца ў кожной працоўнай пасля чытання колькісных даных — не транслявайце уніфікаваныя строкі, калі не можаце гарантаваць, што ўсі локалі будуць ідэнтычныя.
На завершанне — звяржыцеся з статыстыкай: запісваўце, як часта вырабляюцца дадзеныя пра дублікаты SKU ў аналітыцы. Калі гэтая частота значная, то «пастка равнасці значэнняў» стала бы латэнтным прыбутковым інцыдэнтам, які чакаў на першага аптымалнага корыстувальніка з колькома вікнамі.
Спраўленне інтэрв’ю з документам дызайну
Працуячы над гэтым для каманды, выдзельце тры рашэнні: (1) які надзвычайна стойкі сховішча будзе храніць элементы кошыка, (2) які механізм перадачы дадзенняў будзе актывація іншых вікон, і (3) як прыстрой скасавання бейджа адгукваецца на паведамленні. Адналічэнне гэтых рашэнняў у простае «выкорыстоўваць толькі localStorage» і стварае саму «пастку равнасці».
Короткі документ дызайну можа мець схему абароткі: клік корыстувальніка → змяна дадзенняў у IndexedDB → паведамленне праз BroadcastChannel → іншыя вікна анулююць запіт на бейдж. Звярніце увагу на можлывыя проблемы: недаступнае каналы (рэдкая сітуацыя ў сучасных браузерах, але трэба пераканацца), особлівасці прыватнага рэжыму, а таксама ізоляція сховішча ў браузерах з калькамі прафіляў.
Спісак перагляду прабавок
- Двойна дадзенне ідэнтычных SKU з дзвяночкам толькі для сховання (маецца прычына для непаспэлкі)
- Двойна дадзенне з BroadcastChannel (маецца два апдэйта)
- Ачыць трэці вікніца пасля дадзенняў (маецца правільны лік з надзейнага чытання, а не з перазыграння)
- Швайнае дадзенне за адны мілісэкунду з унікальнасцю часовага пазначэння (маецца перыядычная непаспэлка)
- setItem + removeItem без захавання ад null (маецца двойны лік)
Автаматызацыя гэтага спісака прабавок у CI запобегае рэгрэсіям, калі хтось “спрасцавляе” прыемы звярнення да справаў сховання.
Чаму спрашальнікі любяць гэты запіт
Гэта практыка адзначае ценнасць чытання тэхнічных спефікацый, а не запам’ятоввання назв API. Кандыдаты, якія толькі шукалі інфармацыю на MDN, працягваюць не зрозумець, як вяртаеся значэнне для роўнасці. Кандыдаты, якія стварылі інтэрфейсы з калькулярамі, памятаюць пра BroadcastChannel і стойкіх хранальнях дадзеных без падказак. Даўнейшыя запытання пра вікна, якія зачыняюцца пазней, і лічыльнікі без блокавання паказваюць, чы рэспонс прыйшоў з артыкула на блогу чы ён быў базаваны на рэальным дазнаўце.
Для варыянтав, якія можна забраць домоў, папрасіце прыкладны рэпазітарый з двума маршрутамі і файлам README, які опісвае выбраныя компоненты. Рэвізоры должны ачысці два вікна і нажыць — практычны прыклад лепшы за абарот тэорыі.
Спадзяючыся патэрны
Індыкаторы прысутнасці, саўместныя курсоры (лёгкія) і механізмы выйшчы з сістэмы вікорыстоўваюць той самы модэль „doorbell“. Рэдагаванне дакументаў саўместна зазвычай выклікае патрэбу ў CRDT-ах або серверы; не трэба ператвараць BroadcastChannel у протакол адпаведнасці. Чыста застаўся прыклад корзіны: сінхронізацыя індыкатораў у канечны момент, автарытатывная, стойкая корзіна, а пасля — неабавесць можлівае супрацоўкаванне з серверам.
У рэальных умовах трэба застаўляць усё простым: адна стойкая корзіна, адны модэль „doorbell“, чысткія тэсты на дуплікацыю дзеянняў, і без жадных хітрых способаў на базе часоўнікаваў у мілісекундах. Саме такая простацкая структура дапамагае падтрымваць адпаведнасць індыкатораў у колькіх вікнах, калі пакупцы ачыняюць больш вікна, чым тое, што было практыкуема ў дэманстрацыйных сценарыях.
Гэтая комбінацыя ўсё, што трэба. Готава.