Skip to main content
Tilbake til bloggen
JavaScriptWebThird-Party ScriptsFrontendPatterns

Fang en tredjepartsverdi i det den lander på window

Et skript du ikke kontrollerer skriver resultatet sitt til en window-global på et tidspunkt du ikke kan forutsi. Det meste av koden kjører om kapp med det via et timeout eller sløser med main thread ved polling. Her er accessor-property-trikset som gjør skrivingen om til en hendelse — pluss getteren du ikke må glemme, og hvordan du kan erstatte verdien helt.

Publisert 27. juli 20269 min lesing

Før eller senere integrerer du et skript du ikke eier — en attribution-SDK, et consent-verktøy, et A/B-rammeverk, en deep-link-resolver sluppet inn via en tag manager — og det kommuniserer tilbake til siden din ved å tilordne en verdi til en global på window. Noe i stil med window.SMART_LINK_RESULT = { url: … }. Din jobb er å lese denne verdien og handle på den så snart den finnes.

Hemmeligheten: du har ingen kontroll over når tilordningen skjer. Den kan skje før koden din kjører, noen hundre millisekunder etter, eller — hvis nettverket er tregt eller skriptet aldri lastes — ikke i det hele tatt. Denne artikkelen går gjennom de to tilnærmingene alle griper til først, hvorfor begge er kompromisser, og en tredje som er strengt bedre: å avskjære selve tilordningen med en accessor property.

Oppsettet: en verdi som kommer når den vil

Konkret: se for deg en lettvekts redirect-side. En tag manager injiserer et skript som beregner den korrekte, fullt attribuerte mål-URL-en og, når det er ferdig, skriver den til window.SMART_LINK_RESULT. Du vil omdirigere til denne URL-en i det øyeblikket den blir tilgjengelig, og falle tilbake på en enkel standard-URL hvis den aldri dukker opp.

Verdien er et lite objekt, og vi eier ikke skriptet som produserer det — vi vet bare hvilken form vi forventer:

global.d.ts
// 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øk 1: et fast timeout

Den første instinktive reaksjonen er å vente en «trygg» mengde tid på at skriptet skal ferdigstille, deretter lese globalen én gang og kjøre:

attempt-1-timeout.ts
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 fungerer ofte nok til å skipes, og begynner deretter stille å koste deg. Én enkelt lesing bak en fast forsinkelse er et kappløp du taper i begge retninger:

  • For tidlig: hvis skriptet ikke har skrevet verdien innen 3-sekundersmerket, leser du undefined og omdirigerer til fallback — og kaster bort den korrekte URL-en som ville ha kommet et øyeblikk senere.
  • For sent: hvis skriptet var ferdig etter 200 ms, får brukeren likevel stirre på en blank redirect-side i hele 3 sekunder før noe skjer.
  • Den «riktige» forsinkelsen er umulig å vite, fordi den avhenger av nettverket og tredjeparten — så du ender opp med å velge et tall som samtidig er for langt for raske klienter og for kort for trege.

Forsøk 2: polling

Den åpenbare fiksen for «for sent»-halvdelen er å slutte å vente på et fast tidspunkt og i stedet sjekke gjentatte ganger til verdien dukker opp, med en deadline for når man skal gi opp:

attempt-2-polling.ts
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);

Dette er bedre — det reagerer innen ett intervall etter at verdien har ankommet, i stedet for å vente hele forsinkelsen. Men det er fortsatt et kompromiss: du kjører en timer som ikke gjør noe nyttig på de fleste av sine tick, du har byttet latency mot et justerbart intervall (for grovt føles seint, for fint brenner main thread), og du leser en property gjentatte ganger i en hot loop for noe som skjer nøyaktig én gang.

Trikset: avskjær selve skrivingen

Begge tilnærmingene behandler tilordningen som noe som skal oppdages i etterkant — ved å gjette når det skjedde, eller ved å spørre gang på gang om det har skjedd ennå. Men en tilordning til en property er noe JavaScript gjerne forteller deg om, hvis du definerer propertyen som en accessor før skriptet kjører. Erstatt den vanlige data-slotten på window.SMART_LINK_RESULT med et getter/setter-par:

interceptor.ts
// 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
    }
  },
});

Nå, i det øyeblikket tredjepartsskriptet utfører window.SMART_LINK_RESULT = …, skriver det ikke til en vanlig slot — det kaller din set-funksjon, synkront, med verdien som argument. Ingen forsinkelse, ingen polling, ingen gjetting. Du reagerer i det eksakte øyeblikket verdien finnes, og ikke ett tick før eller etter. Timeouten slutter å være din primære mekanisme og blir det den alltid burde ha vært: en fallback for tilfellet der verdien aldri kommer.

Et fast timeout gjetter når verdien ankommer. Polling spør gjentatte ganger om den har gjort det. En accessor property blir ganske enkelt fortalt — tilordningen blir en synkron callback, med null latency og null bortkastet arbeid.

Hvorfor getteren ikke er valgfri

Det er fristende å bare definere en set — reaksjonen på skrivingen er tross alt hele poenget. Ikke gjør det. En accessor property med en setter men uten getter returnerer undefined på hver lesing. Tredjepartsskriptet (og alt annet på siden) leser ofte sin egen global tilbake — for å sjekke den, oppdatere et felt på den, gi den videre til en annen modul. En set-only accessor bryter alt dette i det stille:

set-only.ts
// 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

Regelen er enkel: hvis du avskjærer skriving, må du også betjene lesing. Hold den siste verdien i en closure-variabel, returner den fra get, og oppdater den i set. For alle andre oppfører propertyen seg akkurat som den vanlige data-propertyen den erstattet — du har bare lagt til en sideeffekt på tilordning.

Tett kappløpet fra den andre siden: den tidlige sjekken

En interceptor fanger bare skriving som skjer etter at du installerer den. Hvis tredjepartsskriptet er raskt — eller ble inlinet før koden din — kan verdien allerede ligge på window når du kjører. Sjekk derfor for den først, og installer accessoren bare hvis den ikke er der ennå:

early-check.ts
// 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 sjekken og setteren sammen har du dekket hele tidslinjen: verdien var allerede der (les den nå), eller den ankommer senere (setteren utløses), eller den ankommer aldri (fallback-timeren). Det finnes ikke noe vindu der en reell verdi kan glippe forbi deg.

Å erstatte verdien, ikke bare observere den

Fordi propertyens lagring nå lever i ditt closure, er du ikke begrenset til å observere verdien — du bestemmer hva den faktisk inneholder. Det er forskjellen mellom en listener og en interceptor. Du kan normalisere en feilformet payload, fjerne et felt, eller erstatte tredjepartens standardverdi med din egen før noen nedstrøms noensinne leser den:

substitute.ts
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 };
  },
});

Alt skriptet tilordner flyter gjennom din set, og alt din get returnerer er det resten av siden ser. Standardverdien skriptet hadde tenkt å installere trenger aldri å tre i kraft — bare den verdien du velger å beholde.

Rydding: sett propertyen tilbake

En accessor installert på en global overlever komponenten som la den til, så riv den ned når du er ferdig — spesielt i single-page apps, React Strict Mode, eller hvor som helst samme kode kan kjøre to ganger. Slett accessoren og, hvis det fantes en verdi, gjenopprett den som en ordinær data property:

teardown.ts
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;
  }
}

Å gjenopprette en vanlig property (i stedet for å la en løs getter/setter henge igjen, eller slette verdien helt) betyr at alt som leser globalen etterpå får normal oppførsel tilbake. Det er configurable: true, satt da du definerte propertyen, som gjør denne delete-en mulig i det hele tatt — uten den er accessoren permanent.

Å sette det hele sammen

Her er hele mønsteret som én enkelt React-hook: tidlig sjekk, interceptor med en tilhørende getter, en fallback-timer, og rydding som gjenoppretter en vanlig property. En done-lås garanterer at redirecten utløses nøyaktig én gang, uansett hvilken vei som vinner.

useSmartLinkRedirect.ts
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 fungerer utenfor React — de bevegelige delene er en closure-variabel, en Object.defineProperty med get/set, en tidlig sjekk, en fallback og en teardown. I en vanlig modul ville du pakket det inn i en funksjon og kalt teardown på pagehide eller når funksjonen din avsluttes.

Timeout mot polling mot interceptor

HensynFast timeoutPollingAccessor-interceptor
Reagerer i det øyeblikket verdien settesNei — venter hele forsinkelsenNesten — innen ett intervallJa — synkront
Bortkastet tid når verdien kommer tidligHele forsinkelsenOpp til ett intervallIngen
Main-thread-kostnad under ventingIngenGjentatte lesinger på en timerIngen
Fanger en verdi satt før du starterJa (leser én gang til slutt)JaBare med en eksplisitt tidlig sjekk
Kan erstatte eller normalisere verdienNeiNeiJa
Håndterer at verdien aldri ankommerJa (leser, faller tilbake)Ja (deadline-gren)Ja (med en fallback-timer)

Interceptoren vinner hver rad som handler om å reagere på skrivingen, og stiller likt i resten så snart du legger til den tidlige sjekken og fallback-timeren den likevel trenger. Den ene tingen å huske er at den må installeres før tredjepartsskriptet kjører — noe som, for en verdi levert av en tag manager eller en asynkron SDK, nesten alltid er tilfellet hvis du installerer den tidlig i sidens livssyklus.

Fallgruver verdt å kjenne til

  • Installasjonsrekkefølgen betyr noe. Interceptoren fanger bare skriving som kommer etter den. Definer accessoren så tidlig som mulig, og kombiner den alltid med den tidlige sjekken for verdien som kom før deg.
  • Sett alltid configurable: true. Uten den kan du ikke gjøre delete på propertyen for å rydde opp, og et andre forsøk på å redefinere den kaster en feil.
  • Sikre deg mot SSR. Det finnes ingen window på serveren. I et rammeverk kjører du dette i en client-only effect; i en vanlig modul sikrer du med "undefined" !== "undefined".
  • Én interceptor per property. Hvis to deler av appen din begge redefinerer samme global, overskriver den andre den første. Sentraliser det, eller sjekk Object.getOwnPropertyDescriptor(window, name) før du installerer.
  • Noen globaler er allerede accessorer. En property definert av miljøet som non-configurable (eller med sin egen getter/setter) kan ikke erstattes — sjekk deskriptoren først og fall tilbake på polling hvis du virkelig ikke kan redefinere den.
  • Hold get og set billige. De kjører på hver lesing og skriving av globalen. Gjør det tunge arbeidet én gang, bak done-låsen din — ikke ved hver tilgang.

Når du bør gripe til dette

Dette er et presisjonsverktøy, ikke et standardvalg. Det er verdt bruken når alt dette stemmer:

  • En verdi du trenger skrives til en global (eller en hvilken som helst object property) av kode du ikke kontrollerer.
  • Tidspunktet er uforutsigbart og du vil reagere uten latency — eller du må erstatte verdien før noe annet leser den.
  • Du kan kjøre koden din før skriveren gjør det, eller du er villig til å kombinere det med en tidlig sjekk for tilfellet der du ikke kan.

Det viktigste

  • En tredjepart gir deg ofte en verdi ved å tilordne en global på et tidspunkt du ikke kan forutsi; et fast timeout kjører om kapp med det, og polling brenner main thread.
  • Object.defineProperty(window, name, { get, set }), installert før skriveren kjører, gjør tilordningen til en synkron callback — null latency, null bortkastet arbeid.
  • Skip aldri en set-only accessor: kombiner hver set med en get som returnerer den lagrede verdien, ellers bryter du i det stille skriverens egen tilbakelesing.
  • Legg til en tidlig sjekk for en verdi som ble satt før du installerte accessoren, og en fallback for en verdi som aldri ankommer.
  • Fordi du eier lagringen, kan du normalisere eller erstatte verdien — en interceptor, ikke bare en listener.
  • Sett configurable: true og gjenopprett en vanlig data property ved rydding, så ingenting nedstrøms blir sittende igjen med en overraskelse.

Ingenting av dette er eksotisk — det er bare Object.defineProperty rettet mot en global i stedet for dine egne objekter. Men å omformulere «vent på en verdi» til «bli fortalt når verdien skrives» fjerner en hel klasse timing-bugs, og gir deg verdien tidlig nok til å endre den. Neste gang du tar deg selv i å velge et magisk timeout-tall for å vente ut et tredjepartsskript, grip heller til setteren.