Үшінші тарап скриптінің мәнін ол window-ға түскен сәтте-ақ ұстап алу
Сіз бақыламайтын скрипт өз нәтижесін window глобалына сіз болжай алмайтын сәтте жазады. Көп жағдайда код онымен timeout арқылы жарысады немесе main thread-ті полингке жұмсайды. Мұнда — жазуды оқиғаға айналдыратын accessor-property трюгі: ұмытуға болмайтын getter және мәнді толықтай ауыстыру тәсілі.
Ерте ме, кеш пе, сіз өзіңізге тиесілі емес скриптті интеграциялайсыз — attribution SDK, consent-құрал, A/B-фреймворк, tag manager арқылы қосылған deep-link resolver — және ол сіздің бетіңізге window-дағы глобалға мән беру арқылы хабарласады. Мысалы, window.SMART_LINK_RESULT = { url: … } секілді. Сіздің міндетіңіз — сол мәнді оқу және ол пайда болған бойда әрекет ету.
Мәселе мынада: сізде осы меншіктеудің қашан болатынын ешқандай бақылау жоқ. Ол сіздің кодыңыз орындалғанға дейін, одан кейін бірнеше жүз миллисекундтан соң, немесе — егер желі баяу болса не скрипт мүлдем жүктелмесе — ешқашан болмауы мүмкін. Бұл мақалада алдымен барлығы қолданатын екі тәсіл, олардың неге екеуі де компромисс екені, және оданда жақсы үшінші тәсіл — меншіктеудің өзін accessor property арқылы ұстап алу — қарастырылады.
Бастапқы жағдай: қашан келгісі келсе, сонда келетін мән
Нақтырақ айтсақ, жеңіл redirect бетін елестетіңіз. Tag manager дұрыс, толық атрибуцияланған мақсатты URL-ды есептейтін скриптті енгізеді және ол аяқталғанда оны window.SMART_LINK_RESULT-қа жазады. Сіз сол URL қолжетімді болған сәтте оған redirect жасағыңыз келеді, ал ол ешқашан пайда болмаса, қарапайым дефолт URL-ға оралғыңыз келеді.
Мән — шағын объект, әрі оны шығаратын скрипт бізге тиесілі емес — біз тек күтетін пішінімізді ғана білеміз:
// A third-party script assigns this global once it has computed a result.
// We don't own the script, only the type we expect on the window.
interface SmartLinkResult {
url?: string;
[key: string]: unknown;
}
declare global {
interface Window {
SMART_LINK_RESULT?: SmartLinkResult;
}
}
export {};1-әрекет: тұрақты timeout
Алғашқы инстинкт — скрипт аяқталғанша «қауіпсіз» уақыт мөлшерін күту, содан кейін глобалды бір рет оқып, жүру:
const FALLBACK_URL = "https://example.com/download";
// Wait a "safe" amount of time, then read the global once and redirect.
setTimeout(() => {
const url = window.SMART_LINK_RESULT?.url;
window.location.href = url?.trim() ? url : FALLBACK_URL;
}, 3000);Бұл продакшнға шығуға жеткілікті жиі жұмыс істейді, содан кейін тынышырақ сізге қымбатқа түседі. Тұрақты кідірістен кейінгі жалғыз оқу — сіз екі бағытта да ұтылатын жарыс:
- Тым ерте: егер скрипт мәнді 3 секунд белгісіне дейін жазып үлгермесе, сіз
undefinedоқисыз да, fallback-қа redirect жасайсыз — сәл кейінірек келетін дұрыс URL-ды тастап кетесіз. - Тым кеш: егер скрипт 200 мс ішінде аяқталса да, пайдаланушыны бір нәрсе болғанға дейін толық 3 секунд бос redirect бетіне қарап тұруға мәжбүрлейсіз.
- «Дұрыс» кідірісті білу мүмкін емес, өйткені ол желіге және үшінші тарапқа байланысты — сондықтан сіз ақыр соңында жылдам клиенттер үшін тым ұзақ, баяулары үшін тым қысқа санды таңдап аласыз.
2-әрекет: полинг
«Тым кеш» жартысының көрнекі шешімі — тұрақты сәтті күтуді доғарып, оның орнына мән пайда болғанша қайта-қайта тексеру, бас тартатын дедлайнмен қоса:
const FALLBACK_URL = "https://example.com/download";
const DEADLINE = 3000;
let elapsed = 0;
const timer = setInterval(() => {
const url = window.SMART_LINK_RESULT?.url;
if (url?.trim()) {
clearInterval(timer);
window.location.href = url; // finally showed up
} else if ((elapsed += 50) >= DEADLINE) {
clearInterval(timer);
window.location.href = FALLBACK_URL; // gave up waiting
}
}, 50);Бұл жақсырақ — толық кідірісті күтудің орнына, мән келгеннен кейін бір интервал ішінде әрекет етеді. Бірақ бұл әлі де компромисс: сіз тіктерінің көбінде пайдалы ешнәрсе жасамайтын таймерді жүргізесіз, латентілікті баптауға болатын интервалға айырбастадыңыз (тым ірі болса баяу сезіледі, тым жіңішке болса main thread-ті өртейді), әрі дәл бір рет болатын оқиға үшін ыстық циклде қасиетті үздіксіз оқисыз.
Трюк: жазудың өзін ұстап алу
Екі тәсіл де меншіктеуді болған соң анықтайтын нәрсе ретінде қарастырады — қашан болғанын болжау арқылы, немесе әлі болды ма деп қайта-қайта сұрау арқылы. Бірақ қасиетке меншіктеу дегеніміз — егер сіз скрипт орындалғанға дейін қасиетті accessor ретінде анықтасаңыз, JavaScript сізге қуана-қуана өзі айтып беретін нәрсе. window.SMART_LINK_RESULT-тағы қарапайым деректер слотын getter/setter жұбымен ауыстырыңыз:
// Store the current value in a closure so we control what the property holds.
let value: SmartLinkResult | undefined = window.SMART_LINK_RESULT;
Object.defineProperty(window, "SMART_LINK_RESULT", {
configurable: true,
get() {
return value;
},
set(next: SmartLinkResult | undefined) {
value = next; // keep it readable for whoever assigned it
if (next?.url?.trim()) {
window.location.href = next.url; // fires the instant the script writes
}
},
});Енді үшінші тарап скрипті window.SMART_LINK_RESULT = … орындаған сәтте, ол қарапайым слотқа жазып тұрған жоқ — ол сіздің set функцияңызды синхронды түрде, мәнді аргумент ретінде беріп шақырып тұр. Кідіріс жоқ, полинг жоқ, болжам жоқ. Сіз мән бар болған дәл сол сәтте әрекет етесіз, бір тик те ерте немесе кеш емес. Timeout сіздің негізгі механизміңіз болудан қалып, әуел бастан қандай болуы керек еді, сол нәрсеге айналады: мән ешқашан келмеген жағдайға арналған fallback.
Тұрақты timeout мән қашан келетінін болжайды. Полинг ол келді ме деп қайта-қайта сұрайды. Accessor property-ге жай ғана хабарланады — меншіктеу нөлдік латентілігі және нөлдік босқа жұмсалған еңбегі бар синхронды callback-қа айналады.
Getter неге міндетті түрде қажет
Тек set-ті ғана анықтау азғырады — өйткені жазуға әрекет ету дәл сол мақсат емес пе. Олай істемеңіз. Setter-і бар, бірақ getter-і жоқ accessor property әрбір оқуда undefined қайтарады. Үшінші тарап скрипті (және беттегі кез келген басқа нәрсе) жиі өзінің глобалын кері оқиды — оны тексеру үшін, ондағы өрісті жаңарту үшін, оны басқа модульге беру үшін. Тек setter-і бар accessor мұның бәрін үнсіз бұзады:
// A set-only accessor silently swallows the value on read.
Object.defineProperty(window, "SMART_LINK_RESULT", {
configurable: true,
set(next: SmartLinkResult | undefined) {
if (next?.url) window.location.href = next.url;
},
});
// The third-party script does its normal thing:
window.SMART_LINK_RESULT = { url: "/go" };
// ...but now anyone reading it back — including the script itself — sees nothing:
console.log(window.SMART_LINK_RESULT); // undefinedЕреже қарапайым: жазуларды ұстап алсаңыз, оқуларды да қамтамасыз етуіңіз керек. Соңғы мәнді closure айнымалысында сақтаңыз, оны get-тен қайтарыңыз, ал set-те жаңартыңыз. Басқалар үшін қасиет ол алмастырған қарапайым деректер қасиеті сияқты дәл солай әрекет етеді — сіз тек меншіктеуге бүйірлік әсер қосқансыз.
Жарысты екінші жақтан жабу: ерте тексеру
Ұстап алушы сіз оны орнатқаннан кейін болатын жазуларды ғана ұстайды. Егер үшінші тарап скрипті жылдам болса — немесе сіздің кодыңыздан бұрын inline түрде қосылса — мән сіз орындалғанда window-да бұрыннан жатқан болуы мүмкін. Сондықтан алдымен оны тексеріңіз, әрі accessor-ды тек ол әлі жоқ болса ғана орнатыңыз:
// The value may already be on the window before our code runs.
// Check first, and only install the interceptor if it isn't there yet.
const existing = window.SMART_LINK_RESULT?.url;
if (existing?.trim()) {
window.location.href = existing;
} else {
installInterceptor(); // the Object.defineProperty from above
}Ерте тексеру мен setter бірге болғанда, сіз бүкіл уақыт шкаласын қамтыдыңыз: мән бұрыннан бар еді (қазір оқыңыз), не кейін келеді (setter іске қосылады), не ешқашан келмейді (fallback таймер). Нақты мән сізден жасырын өтіп кете алатын ешқандай саңылау жоқ.
Мәнді жай бақылап қоймай, оны ауыстыру
Қасиеттің сақталу орны енді сіздің closure-ыңызда өмір сүретіндіктен, сіз мәнді тек бақылаумен шектелмейсіз — оның нақты нені ұстайтынын өзіңіз шешесіз. Тыңдаушы мен ұстап алушының айырмашылығы дәл осында. Сіз бұзылған payload-ты нормалдай аласыз, өрісті алып тастай аласыз, немесе үшінші тараптың дефолтын одан кейін оны ешкім оқымас бұрын өзіңіздікімен ауыстыра аласыз:
let value: SmartLinkResult | undefined = window.SMART_LINK_RESULT;
Object.defineProperty(window, "SMART_LINK_RESULT", {
configurable: true,
get() {
return value;
},
set(next: SmartLinkResult | undefined) {
// Don't store what they sent verbatim — normalize it, or replace it entirely.
value = normalize(next) ?? { url: FALLBACK_URL };
},
});Скрипт не меншіктесе де, ол сіздің set-іңіз арқылы өтеді, ал сіздің get-іңіз не қайтарса, беттің қалғаны соны көреді. Скрипт орнатқысы келген дефолт ешқашан күшіне енбеуі мүмкін — тек сіз сақтауды таңдаған мән ғана күшінде болады.
Тазалау: қасиетті орнына қайтару
Глобалға орнатылған accessor оны қосқан компоненттен ұзағырақ өмір сүреді, сондықтан жұмысты бітіргенде оны бұзыңыз — әсіресе single-page қосымшаларда, React Strict Mode-да немесе бір код екі рет орындалуы мүмкін кез келген жерде. Accessor-ды жойыңыз және, егер мән болса, оны қарапайым деректер қасиеті ретінде қалпына келтіріңіз:
function uninstall() {
const current = value; // whatever the accessor last held
delete window.SMART_LINK_RESULT; // remove the getter/setter pair
if (current !== undefined) {
// Put it back as an ordinary data property so nothing downstream breaks.
window.SMART_LINK_RESULT = current;
}
}Қарапайым қасиетті қалпына келтіру (ілулі getter/setter қалдырудың немесе мәнді мүлдем жоюдың орнына) кейін глобалды оқитын кез келген нәрсенің қалыпты мінез-құлыққа қайта оралатынын білдіреді. Қасиетті анықтағанда configurable: true орнату дәл осы delete-ті мүмкін ететін нәрсе — онсыз accessor тұрақты болып қалады.
Барлығын біріктіру
Міне, толық паттерн бір React hook ретінде: ерте тексеру, сәйкес getter-і бар ұстап алушы, fallback таймер және қарапайым қасиетті қалпына келтіретін тазалау. done засувкасы қай жол жеңсе де, redirect дәл бір рет іске қосылатынына кепілдік береді.
import { useEffect } from "react";
const FALLBACK_URL = "https://example.com/download";
const FALLBACK_DELAY = 3000;
export function useSmartLinkRedirect() {
useEffect(() => {
let done = false;
let fallbackTimer: ReturnType<typeof setTimeout> | null = null;
let value: SmartLinkResult | undefined = window.SMART_LINK_RESULT;
const go = (url: string) => {
if (done) return;
done = true;
if (fallbackTimer) clearTimeout(fallbackTimer);
window.location.href = url;
};
// 1. Already resolved before we mounted? Go now.
if (value?.url?.trim()) {
go(value.url);
return;
}
// 2. Otherwise, catch the assignment the moment it happens.
Object.defineProperty(window, "SMART_LINK_RESULT", {
configurable: true,
get() {
return value;
},
set(next: SmartLinkResult | undefined) {
value = next;
if (next?.url?.trim()) go(next.url);
},
});
// 3. Never trust the third party to always deliver.
fallbackTimer = setTimeout(() => go(FALLBACK_URL), FALLBACK_DELAY);
// 4. Restore a plain property on unmount.
return () => {
if (fallbackTimer) clearTimeout(fallbackTimer);
const current = value;
delete window.SMART_LINK_RESULT;
if (current !== undefined) window.SMART_LINK_RESULT = current;
};
}, []);
}Дәл сол пішін React-тан тыс та жұмыс істейді — қозғалмалы бөліктер: closure айнымалысы, get/set бар Object.defineProperty, ерте тексеру, fallback және тазалау. Қарапайым модульде сіз мұны функцияға орап, тазалауды pagehide-та немесе фичаңыз аяқталғанда шақыратын едіңіз.
Timeout, полинг және ұстап алушыны салыстыру
| Аспект | Тұрақты timeout | Полинг | Accessor ұстап алушы |
|---|---|---|---|
| Мән орнатылған сәтте әрекет етеді | Жоқ — толық кідірісті күтеді | Дерлік — бір интервал ішінде | Иә — синхронды |
| Мән ерте келгендегі босқа кеткен уақыт | Толық кідіріс | Бір интервалға дейін | Жоқ |
| Күту кезіндегі main-thread шығыны | Жоқ | Таймер бойынша қайталанатын оқулар | Жоқ |
| Сіз бастамас бұрын орнатылған мәнді ұстайды | Иә (соңында бір рет оқиды) | Иә | Тек нақты ерте тексерумен ғана |
| Мәнді ауыстыра немесе нормалдай алады | Жоқ | Жоқ | Иә |
| Мән мүлде келмеу жағдайын өңдейді | Иә (оқиды, fallback-қа өтеді) | Иә (дедлайн тармағы) | Иә (fallback таймермен) |
Ұстап алушы жазуға әрекет ету туралы әрбір жолды ұтады, ал ерте тексеру мен оған қалай да қажет fallback таймерді қосқанда, қалғанында тең түседі. Есте сақтайтын жалғыз нәрсе — оны үшінші тарап скрипті орындалмас бұрын орнату керек; бұл tag manager немесе асинхронды SDK жеткізетін мән үшін, оны бет өмірлік циклінің басында орнатсаңыз, дерлік әрдайым солай болады.
Білуге тұрарлық қиыншылықтар
- Орнату реті маңызды. Ұстап алушы одан кейін келетін жазуларды ғана ұстайды. Accessor-ды мүмкіндігінше ерте анықтаңыз және оны сізден бұрын үлгерген мән үшін ерте тексерумен әрдайым қосыңыз.
- Әрдайым
configurable: trueорнатыңыз. Онсыз тазалау үшін қасиеттіdeleteете алмайсыз, ал оны қайта анықтауға екінші әрекет қате тастайды. - SSR-ден сақтаныңыз. Серверде
windowжоқ. Фреймворкте мұны тек клиентке арналған эффектіде орындаңыз; қарапайым модульде"undefined" !== "undefined"арқылы қорғаныңыз. - Әр қасиетке бір ұстап алушы. Егер қосымшаңыздың екі бөлігі де сол глобалды қайта анықтаса, екіншісі біріншісін басып кетеді. Мұны орталықтандырыңыз немесе орнатпас бұрын
Object.getOwnPropertyDescriptor(window, name)-ды тексеріңіз. - Кейбір глобалдар бұрыннан accessor болып табылады. Ортамен non-configurable (немесе өз getter/setter-імен) анықталған қасиетті ауыстыруға болмайды — алдымен дескрипторды тексеріп, оны шынымен қайта анықтай алмасаңыз, полингке ораліңіз.
getменset-ті жеңіл ұстаңыз. Олар глобалдың әрбір оқуы мен жазуында орындалады. Ауыр жұмысты бір рет,doneзасувкаңыздың артында жасаңыз — әрбір қатынауда емес.
Мұны қашан қолдану керек
Бұл дефолт емес, дәлдік құралы. Ол мыналардың барлығы дұрыс болғанда өз құнын ақтайды:
- Сізге қажет мәнді сіз бақыламайтын код глобалға (немесе кез келген объект қасиетіне) жазады.
- Уақыты болжанбайды, әрі сіз латентіліксіз әрекет еткіңіз келеді — немесе оны басқа ешкім оқымас бұрын мәнді ауыстыруыңыз керек.
- Сіз кодыңызды жазушыдан бұрын орындай аласыз, немесе оны орындай алмайтын жағдай үшін ерте тексерумен қосуға дайынсыз.
Негізгі қорытынды
- Үшінші тарап жиі сізге мәнді сіз болжай алмайтын сәтте глобалға меншіктеу арқылы береді; тұрақты timeout онымен жарысады, ал полинг main thread-ті өртейді.
- Жазушы орындалмас бұрын орнатылған
Object.defineProperty(window, name, { get, set })меншіктеуді синхронды callback-қа айналдырады — нөлдік латентілік, нөлдік босқа жұмсалған еңбек. - Ешқашан тек setter-і бар accessor-ды шығармаңыз: әрбір
set-ті сақталған мәнді қайтаратынget-пен қосыңыз, әйтпесе сіз жазушының өзінің кері оқуын үнсіз бұзасыз. - Accessor-ды орнатпас бұрын орнатылған мән үшін ерте тексеру, ал ешқашан келмейтін мән үшін fallback қосыңыз.
- Сақтау орны сізге тиесілі болғандықтан, мәнді нормалдай немесе ауыстыра аласыз — бұл тыңдаушы емес, ұстап алушы.
configurable: trueорнатыңыз және тазалау кезінде қарапайым деректер қасиетін қалпына келтіріңіз, сонда ағын бойынша ешнәрсе тосынсыймен қалмайды.
Мұның бәрі экзотикалық емес — бұл жай ғана өз объектілеріңіздің орнына глобалға бағытталған Object.defineProperty. Бірақ «мәнді күту» дегенді «мән жазылғанда маған хабарлансын» деп қайта тұжырымдау таймингке байланысты бүкіл бір класс багтарды жояды және сізге мәнді оны өзгертуге жеткілікті ерте қолыңызға береді. Келесі жолы үшінші тарап скриптін күтіп отыру үшін сиқырлы timeout санын таңдап отырғаныңызды байқасаңыз, оның орнына setter-ге жүгініңіз.