Fang en tredjeparts-værdi i det øjeblik den lander på window
Et script du ikke kontrollerer, skriver sit resultat til en global variabel på window på et tidspunkt du ikke kan forudsige. Det meste kode kører om kap med det via en timeout eller spilder main thread på polling. Her er accessor-property-tricket, der gør skrivningen til en event — plus den getter du ikke må glemme, og hvordan du helt kan substituere værdien.
Før eller siden integrerer du et script, du ikke selv ejer — et attribution SDK, et consent-værktøj, et A/B-framework, en deep-link resolver sat ind via en tag manager — og det kommunikerer tilbage til din side ved at tildele en værdi til en global variabel på window. Noget i stil med window.SMART_LINK_RESULT = { url: … }. Din opgave er at læse den værdi og reagere på den, så snart den findes.
Hagen er, at du ikke har kontrol over hvornår tildelingen sker. Det kan ske før din kode kører, nogle hundrede millisekunder efter — eller, hvis netværket er langsomt eller scriptet aldrig loader, slet ikke. Denne artikel gennemgår de to tilgange, alle griber til først, hvorfor de begge er kompromiser, og en tredje, der er strengt bedre: at opfange selve tildelingen med en accessor property.
Opsætningen: en værdi, der ankommer, når den har lyst
Konkret: forestil dig en let redirect-side. En tag manager injicerer et script, der beregner den korrekte, fuldt attribuerede destinations-URL og, når det er færdigt, skriver den til window.SMART_LINK_RESULT. Du vil redirecte til den URL i det øjeblik, den er tilgængelig, og falde tilbage til en simpel standard-URL, hvis den aldrig dukker op.
Værdien er et lille objekt, og vi ejer ikke scriptet, der producerer det — vi kender kun den form, vi forventer:
// 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 {};Forsøg 1: en fast timeout
Det første instinkt er at vente en "sikker" mængde tid på, at scriptet bliver færdigt, og derefter læse den globale variabel én gang og fyre den af:
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);Det virker ofte nok til at kunne shippes, og begynder derefter stille at koste dig. En enkelt læsning bag en fast delay er et kapløb, du taber i begge retninger:
- For tidligt: hvis scriptet ikke har skrevet værdien inden 3-sekunders-markeringen, læser du
undefinedog redirecter til fallback'en — og smider dermed den korrekte URL væk, som ville være ankommet et øjeblik senere. - For sent: hvis scriptet var færdigt på 200 ms, får du stadig brugeren til at stirre på en tom redirect-side i alle 3 sekunder, før der sker noget.
- Den "rigtige" delay er umulig at vide på forhånd, fordi den afhænger af netværket og tredjeparten — så du ender med at vælge et tal, der samtidig er for langt for hurtige klienter og for kort for langsomme.
Forsøg 2: polling
Den oplagte løsning på "for sent"-halvdelen er at stoppe med at vente på et fast tidspunkt og i stedet tjekke gentagne gange, indtil værdien dukker op, med en deadline for at give op:
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);Det er bedre — det reagerer inden for ét interval efter, at værdien ankommer, i stedet for at vente hele delay'en. Men det er stadig et kompromis: du kører en timer, der ikke gør noget nyttigt på de fleste af sine ticks, du har byttet latenstid for et justerbart interval (for groft føles laggy, for fint brænder main thread), og du busy-læser en property i et hot loop for noget, der sker præcis én gang.
Tricket: opfang selve skrivningen
Begge tilgange behandler tildelingen som noget, man skal opdage efterfølgende — ved at gætte, hvornår det skete, eller ved at spørge igen og igen, om det er sket endnu. Men en tildeling til en property er noget, JavaScript gerne fortæller dig om, hvis du definerer property'en som en accessor, inden scriptet kører. Erstat den almindelige data-slot på window.SMART_LINK_RESULT med et getter/setter-par:
// 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
}
},
});Nu, i det øjeblik det tredjeparts-script udfører window.SMART_LINK_RESULT = …, skriver det ikke til en almindelig slot — det kalder din set-funktion, synkront, med værdien som argument. Ingen delay, ingen polling, ingen gætteri. Du reagerer i det præcise øjeblik, værdien findes, og ikke et tick før eller efter. Timeout'en stopper med at være din primære mekanisme og bliver det, den hele tiden skulle have været: en fallback til det tilfælde, hvor værdien aldrig kommer.
En fast timeout gætter på, hvornår værdien ankommer. Polling spørger gentagne gange, om den er ankommet. En accessor property bliver simpelthen fortalt det — tildelingen bliver et synkront callback, med nul latenstid og nul spildt arbejde.
Hvorfor getteren ikke er valgfri
Det er fristende kun at definere en set — reaktionen på skrivningen er trods alt hele pointen. Gør det ikke. En accessor property med en setter men ingen getter returnerer undefined ved hver læsning. Tredjeparts-scriptet (og alt andet på siden) læser ofte sin egen globale variabel tilbage — for at tjekke den, opdatere et felt på den, give den videre til et andet modul. En set-only accessor ødelægger stille og roligt alt det:
// 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); // undefinedReglen er simpel: hvis du opfanger skrivninger, skal du også servicere læsninger. Behold den seneste værdi i en closure-variabel, returner den fra get, og opdater den i set. For alle andre opfører property'en sig præcis som den almindelige data-property, den erstattede — du har blot tilføjet en sideeffekt ved tildeling.
Luk kapløbet fra den anden side: den tidlige tjek
En interceptor opfanger kun skrivninger, der sker efter, du installerer den. Hvis tredjeparts-scriptet er hurtigt — eller var inlinet før din kode — kan værdien allerede ligge på window, når din kode kører. Tjek derfor for den først, og installer kun accessor'en, hvis den ikke allerede er der:
// 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
}Med den tidlige tjek og setteren tilsammen har du dækket hele tidslinjen: værdien var allerede der (læs den nu), eller den ankommer senere (setteren fyrer af), eller den ankommer aldrig (fallback-timeren). Der er intet tidsvindue, hvor en reel værdi kan glide forbi dig.
At substituere værdien, ikke bare overvåge den
Fordi property'ens storage nu bor i din closure, er du ikke begrænset til at observere værdien — du bestemmer, hvad den faktisk indeholder. Det er forskellen på en listener og en interceptor. Du kan normalisere en fejlformet payload, fjerne et felt eller erstatte tredjepartens default med din egen, inden nogen nedstrøms overhovedet læser den:
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 };
},
});Hvad end scriptet tildeler, flyder igennem din set, og hvad end din get returnerer, er det, resten af siden ser. Den default, scriptet havde tænkt sig at installere, behøver aldrig at træde i kraft — kun den værdi, du vælger at beholde.
Ryd op: sæt property'en tilbage
En accessor installeret på en global overlever den komponent, der tilføjede den, så riv den ned igen, når du er færdig — især i single-page apps, React Strict Mode, eller hvor som helst den samme kode kan køre to gange. Slet accessor'en, og hvis der var en værdi, genskab den som en almindelig data-property:
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;
}
}At genskabe en almindelig property (i stedet for at lade et dinglende getter/setter-par hænge, eller at slette værdien helt) betyder, at alt, der læser den globale variabel bagefter, får normal adfærd tilbage. At sætte configurable: true, da du definerede property'en, er det, der overhovedet gør dette delete muligt — uden det er accessor'en permanent.
At samle det
Her er hele mønstret som et enkelt React-hook: tidlig tjek, interceptor med en matchende getter, en fallback-timer og cleanup, der genskaber en almindelig property. En done-lås garanterer, at redirecten fyrer af præcis én gang, uanset hvilken vej der vinder.
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;
};
}, []);
}Samme form virker uden for React — de bevægelige dele er en closure-variabel, et Object.defineProperty med get/set, en tidlig tjek, en fallback og en teardown. I et almindeligt modul ville du pakke det ind i en funktion og kalde teardown'en ved pagehide eller når din feature slutter.
Timeout vs. polling vs. interceptor
| Aspekt | Fast timeout | Polling | Accessor-interceptor |
|---|---|---|---|
| Reagerer i det øjeblik værdien sættes | Nej — venter hele delay'en | Næsten — inden for ét interval | Ja — synkront |
| Spildt tid når værdien kommer tidligt | Hele delay'en | Op til ét interval | Ingen |
| Main-thread-omkostning under ventetiden | Ingen | Gentagne læsninger via en timer | Ingen |
| Fanger en værdi sat før du starter | Ja (læser én gang til sidst) | Ja | Kun med en eksplicit tidlig tjek |
| Kan substituere eller normalisere værdien | Nej | Nej | Ja |
| Håndterer at værdien aldrig ankommer | Ja (læser, falder tilbage) | Ja (deadline-gren) | Ja (med en fallback-timer) |
Interceptoren vinder hver række, der handler om at reagere på skrivningen, og går lige op med resten, når du tilføjer den tidlige tjek og den fallback-timer, den alligevel har brug for. Den ene ting, du skal huske, er, at den skal installeres før, tredjeparts-scriptet kører — hvilket, for en værdi leveret af en tag manager eller et asynkront SDK, næsten altid er tilfældet, hvis du installerer den tidligt i din sides livscyklus.
Faldgruber, det er værd at kende
- Installationsordenen betyder noget. Interceptoren opfanger kun skrivninger, der kommer efter den. Definer accessor'en så tidligt som muligt, og kombiner den altid med den tidlige tjek for den værdi, der nåede før dig.
- Sæt altid
configurable: true. Uden det kan du ikkedeleteproperty'en til oprydning, og et andet forsøg på at omdefinere den kaster en fejl. - Beskyt mod SSR. Der findes ingen
windowpå serveren. I et framework skal du køre dette i en client-only effect; i et almindeligt modul skal du beskytte med"undefined" !== "undefined". - Én interceptor pr. property. Hvis to dele af din app begge omdefinerer den samme globale variabel, overskriver den anden den første. Centraliser det, eller tjek
Object.getOwnPropertyDescriptor(window, name)inden installation. - Nogle globaler er allerede accessors. En property defineret af miljøet som non-configurable (eller med sit eget getter/setter) kan ikke erstattes — tjek descriptoren først, og fald tilbage til polling, hvis du virkelig ikke kan omdefinere den.
- Hold
getogsetbillige. De kører ved hver læsning og skrivning af den globale variabel. Udfør det tunge arbejde én gang, bag dindone-lås — ikke ved hver adgang.
Hvornår du skal gribe til dette
Dette er et præcisionsværktøj, ikke et default-valg. Det er sit besvær værd, når alt følgende er sandt:
- En værdi, du har brug for, skrives til en global variabel (eller en anden object-property) af kode, du ikke kontrollerer.
- Timingen er uforudsigelig, og du vil reagere uden latenstid — eller du skal substituere værdien, inden noget andet læser den.
- Du kan køre din kode, før skriveren gør det, eller du er villig til at kombinere den med en tidlig tjek for de tilfælde, hvor du ikke kan.
Det vigtigste at tage med
- En tredjepart giver dig ofte en værdi ved at tildele en global variabel på et tidspunkt, du ikke kan forudsige; en fast timeout kører om kap med det, og polling brænder main thread.
Object.defineProperty(window, name, { get, set }), installeret før skriveren kører, gør tildelingen til et synkront callback — nul latenstid, nul spildt arbejde.- Send aldrig en set-only accessor i produktion: kombiner hver
setmed enget, der returnerer den lagrede værdi, ellers ødelægger du stille og roligt skriverens egen read-back. - Tilføj en tidlig tjek for en værdi, der blev sat, inden du installerede accessor'en, og en fallback for en værdi, der aldrig ankommer.
- Fordi du ejer storage'en, kan du normalisere eller erstatte værdien — en interceptor, ikke bare en listener.
- Sæt
configurable: true, og genskab en almindelig data-property ved oprydning, så intet nedstrøms bliver overrasket.
Intet af dette er eksotisk — det er bare Object.defineProperty rettet mod en global variabel i stedet for dine egne objekter. Men at omformulere "vent på en værdi" til "bliv fortalt, når værdien skrives" fjerner en hel klasse af timing-bugs og giver dig værdien tidligt nok til at kunne ændre den. Næste gang du finder dig selv i at vælge et magisk timeout-tal for at vente et tredjeparts-script ud, skal du gribe til setteren i stedet.