JavaScript >> Javascript tutorial >  >> Tags >> URL

Jeg byggede min egen TinyURL. Her er hvordan jeg gjorde det

At designe en URL-forkorter som TinyURL og Bitly er et af de mest almindelige systemdesign-interviewspørgsmål inden for softwareudvikling.

Mens jeg blandede mig med Cloudflare Worker for at synkronisere den daglige LeetCode Challenge til min Todoist, gav det mig en idé at bygge en egentlig URL-forkorter, der kan bruges af alle.

Det følgende er min tankeproces med kodeeksempler på, hvordan vi kan oprette en URL-forkorter ved hjælp af Cloudflare Worker. Hvis du gerne vil følge med, skal du have en Cloudflare-konto og bruge Wrangler CLI.

TL;DR

  • Opbygning af en URL-forkorter gratis med Cloudflare Worker og KV
  • Planlægning af projektkrav og begrænsninger
  • Kort URL-UUID-genereringslogik
  • Live demo på s.jerrynsh.com
  • GitHub-lager

Før vi begynder, skal du ikke sætte dine forhåbninger op. Dette er IKKE en guide til:

  • Sådan tackles et egentligt systemdesigninterview
  • Opbygning af en URL-forkorter med kommerciel karakter som TinyURL eller Bitly

Men snarere et proof of concept (POC) af, hvordan man bygger en faktisk URL-forkorter-tjeneste ved hjælp af serverløs computing. Så smid "skalerbarhed", "opdeling", "replikaer" osv. ud af vinduet og spænd op.

Jeg håber, du vil finde dette indlæg indsigtsfuldt og underholdende at læse!

Krav

Som ethvert systemdesigninterview, lad os starte med at definere nogle funktionelle og ikke-funktionelle krav.

Funktionel

  • Givet en URL, bør vores tjeneste returnere en unik og kort URL af den. For eksempel. https://jerrynsh.com/how-to-write-clean-code-in-python/s.jerrynsh.com/UcFDnviQ
  • Hver gang en bruger forsøger at få adgang til s.jerrynsh.com/UcFDnviQ , vil brugeren blive dirigeret tilbage til den oprindelige URL.
  • UUID'et (jeg kalder det nogle gange URL-nøgle, fordi det er nøglen til vores lagerobjekt) skal overholde Base62-kodningsskemaet (26 + 26 + 10):
1. A lower case alphabet 'a' to 'z', a total of 26 characters
2. An upper case alphabet 'A' to 'Z', a total of 26 characters
3. A digit '0' to '9', a total of 10 characters
4. In this POC, we will not be supporting custom short links
  • Længden af ​​vores UUID skal være ≤ 8 tegn, da 62⁸ ville give os omkring ~218 billioner muligheder.
  • Den korte URL, der genereres, må aldrig udløbe.

Ikke-funktionel

  • Lav latenstid
  • Høj tilgængelighed

Planlægning af budget, kapacitet og begrænsninger

Målet er enkelt - jeg vil gerne være vært for denne tjeneste gratis. Som et resultat heraf afhænger vores begrænsninger i høj grad af Cloudflare Workers prissætning og platformsgrænser.

På det tidspunkt, hvor dette skrives, er begrænsningerne pr. konto for at hoste vores tjeneste gratis:

  • 100.000 anmodninger/dag ved 1.000 anmodninger/min.
  • CPU-kørselstid ikke overstiger 10 ms

Ligesom de fleste URL-forkortere forventes vores applikation at støde på høj læsning, men relativt lav skrivning. Til at gemme vores data vil vi bruge Cloudflare KV, et datalager med nøgleværdier, der understøtter høj læsning med lav latenstid – perfekt til vores brug.

Går vi videre fra vores tidligere begrænsninger, giver det gratis niveau af KV og limit os mulighed for at have:

  • 100.000 aflæsninger/dag
  • 1k skriver/dag
  • 1 GB lagrede data (nøglestørrelse på 512 bytes; værdistørrelse på 25 MiB)

Hvor mange korte URL'er kan vi gemme

Med 1 GB gratis maksimalt lagrede datagrænse i tankerne, lad os prøve at estimere, hvor mange URL'er vi kan gemme. Her bruger jeg dette værktøj til at estimere bytestørrelsen på URL'en:

  • 1 tegn er 1 byte
  • Da vores UUID kun bør være på højst 8 tegn, har vi absolut ingen problemer med nøglestørrelsesgrænsen.
  • Værdistørrelsesgrænsen på den anden side — jeg laver et beregnet gæt på, at den maksimale URL-størrelse i gennemsnit skal være på omkring 200 tegn. Jeg mener således, at det er sikkert at antage, at hvert lagret objekt i gennemsnit bør være ≤400 bytes, hvilket er meget langt under 25 MiB.
  • Og endelig, med 1 GB at arbejde med, kan vores URL-forkorter understøtte op til i alt 2.500.000 (1 GB divideret med 400 bytes) korte URL'er.
  • Jeg ved det, jeg ved det. 2,5 millioner URL'er er ikke meget.

Når vi ser tilbage, kunne vi have gjort længden af ​​vores UUID til ≥ 4 i stedet for 8, da 62⁴ muligheder er langt mere end 2,5 millioner. Når det er sagt, lad os holde os til et UUID med en længde på 8.

Samlet set vil jeg sige, at det gratis niveau for Cloudflare Worker og KV er ret generøst og bestemt anstændigt nok til vores POC. Bemærk, at grænserne anvendes pr. konto.

Opbevaring og database

Som jeg nævnte tidligere, vil vi bruge Cloudflare KV som databasen til at gemme vores forkortede URL'er, da vi forventer flere læsninger end skrivninger.

Til sidst Konsistent
En vigtig note - mens KV er i stand til at understøtte exceptionelt høj læsning globalt, er det en efterhånden ensartet lagringsløsning. Med andre ord kan enhver skrivning (dvs. oprettelse af en kort URL) tage op til 60 sekunder at sprede sig globalt – dette er en ulempe, vi er okay med.

Gennem mine eksperimenter har jeg endnu ikke mødt noget mere end et par sekunder.

Atomisk drift

Når man læser om, hvordan KV fungerer, er KV ikke ideel til situationer, der kræver atomare operationer (f.eks. en banktransaktion mellem to kontosaldi). Heldigvis for os, bekymrer det os overhovedet ikke.

For vores POC ville nøglen til vores KV være et UUID, der følger efter vores domænenavn (f.eks. s.jerrynsh.com/UcFDnviQ ), mens værdien ville bestå af den lange URL givet af brugerne.

Oprettelse af en KV

For at oprette en KV skal du blot køre følgende kommandoer med Wrangler CLI.

# Production namespace:
wrangler kv:namespace create "URL_DB"

# This namespace is used for `wrangler dev` local testing:
wrangler kv:namespace create "URL_DB" --preview

For at oprette disse KV-navneområder skal vi også opdatere vores wrangler.toml fil for at inkludere navnerumsbindingerne i overensstemmelse hermed. Du kan se dit KV's dashboard ved at besøge https://dash.cloudflare.com/<your_cloudflare_account_id>/workers/kv/namespaces .

Kort URL UUID Generation Logic

Dette er nok det vigtigste aspekt af hele vores applikation.

Baseret på vores krav er målet at generere et alfanumerisk UUID for hver URL, hvor længden af ​​vores nøgle ikke bør være mere end 8 tegn.

I en perfekt verden bør UUID'et for det korte link, der genereres, ikke have nogen kollision. Et andet vigtigt aspekt at overveje er - hvad hvis flere brugere forkorter den samme URL? Ideelt set bør vi også tjekke for duplikering.

Lad os overveje følgende løsninger:

1. Brug af en UUID-generator

Denne løsning er forholdsvis ligetil at implementere. For hver ny URL, vi støder på, ringer vi blot til vores UUID-generator for at give os et nyt UUID. Vi vil derefter tildele den nye URL med den genererede UUID som vores nøgle.

I det tilfælde, hvor UUID allerede har eksisteret (kollision) i vores KV, kan vi fortsætte med at prøve igen. Vi vil dog være opmærksomme på at prøve igen, da det kan være relativt dyrt.

Desuden ville brug af en UUID-generator ikke hjælpe os, når det kommer til at håndtere duplikationer i vores KV. At slå den lange URL-værdi op i vores KV ville være relativt langsomt.

2. Hashing URL'en

På den anden side giver hashing af en URL os mulighed for at tjekke for duplikerede URL'er, fordi at sende en streng (URL) gennem en hashing-funktion altid vil give det samme resultat. Vi kan derefter bruge resultatet (nøgle) til at slå op i vores KV for at tjekke for duplikering.

Hvis vi antager, at vi bruger MD5, ville vi ende med ≥ 8 tegn for vores nøgle. Så hvad nu hvis vi bare kunne tage de første 8 bytes af den genererede MD5-hash? Problem løst ikke?

Ikke nøjagtigt. Hashing-funktion ville altid producere kollisioner. For at reducere sandsynligheden for kollision kunne vi generere en længere hash. Men det ville ikke være særlig brugervenligt. Vi ønsker også at beholde vores UUID ≤ 8 tegn.

3. Brug af en trinvis tæller

Muligvis den enkleste, men mest skalerbare løsning efter min mening. Ved at bruge denne løsning vil vi ikke løbe ind i problemer med kollision. Når vi bruger hele sættet (fra 00000000 til 99999999), kan vi blot øge antallet af tegn i vores UUID.

Ikke desto mindre ønsker jeg ikke, at brugere skal kunne gætte en kort URL tilfældigt ved blot at besøge s.jerrynsh.com/12345678 . Så denne løsning er udelukket.

Hvilken du skal vælge

Der er en masse andre løsninger (f.eks. generer en liste over nøgler på forhånd og tildel en ubrugt nøgle, når der kommer en ny anmodning) derude med deres egne fordele og ulemper.

For vores POC går vi med løsning 1 da det er ligetil at implementere og jeg har det fint med dubletter. For at klare dubletter kunne vi cache vores brugeres anmodninger om at forkorte webadresser.

Nano-id

For at generere et UUID bruger vi nanoid pakke. For at estimere vores kollisionshastighed kan vi bruge Nano ID kollisionsberegneren:

Okay nok snak, lad os skrive noget kode!

For at håndtere muligheden for kollision skal vi bare fortsætte med at prøve igen:

// utils/urlKey.js
import { customAlphabet } from "nanoid";

const ALPHABET =
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

/*
Generate a unique `urlKey` using `nanoid` package.
Keep retrying until a unique urlKey which does not exist in the URL_DB.
*/
export const generateUniqueUrlKey = async () => {
    const nanoId = customAlphabet(ALPHABET, 8);
    let urlKey = nanoId();
    while ((await URL_DB.get(urlKey)) !== null) {
        urlKey = nanoId();
    }
    return urlKey;
};

API

I dette afsnit vil vi definere de API-endepunkter, som vi gerne vil understøtte. Dette projekt initialiseres ved hjælp af itty-router arbejderskabelon - det hjælper os med al routinglogikken:

wrangler generate <project-name> https://github.com/cloudflare/worker-template-router

Indgangspunktet for vores projekt ligger i index.js:

// index.js
import { Router } from "itty-router";
import { createShortUrl } from "./src/handlers/createShortUrl";
import { redirectShortUrl } from "./src/handlers/redirectShortUrl";
import { LANDING_PAGE_HTML } from "./src/utils/constants";

const router = Router();

// GET landing page html
router.get("/", () => {
    return new Response(LANDING_PAGE_HTML, {
        headers: {
            "content-type": "text/html;charset=UTF-8",
        },
    });
});

// GET redirects short URL to its original URL.
router.get("/:text", redirectShortUrl);

// POST creates a short URL that is associated with its an original URL.
router.post("/api/url", createShortUrl);

// 404 for everything else.
router.all("*", () => new Response("Not Found", { status: 404 }));

// All incoming requests are passed to the router where your routes are called and the response is sent.
addEventListener("fetch", (e) => {
    e.respondWith(router.handle(e.request));
});

I navnet på en bedre brugeroplevelse har jeg lavet en simpel HTML-landingsside, som alle kunne bruge; du kan hente landingssidens HTML her.

Oprettelse af kort URL

For at starte har vi brug for et POST-slutpunkt (/api/url ), der kalder createShortUrl der parser originalUrl fra brødteksten og genererer en kort URL fra den.

Her er kodeeksemplet:

// handlers/createShortUrl.js
import { generateUniqueUrlKey } from "../utils/urlKey";

export const createShortUrl = async (request, event) => {
    try {
        const urlKey = await generateUniqueUrlKey();

        const { host } = new URL(request.url);
        const shortUrl = `https://${host}/${urlKey}`;

        const { originalUrl } = await request.json();
        const response = new Response(
            JSON.stringify({
                urlKey,
                shortUrl,
                originalUrl,
            }),
            { headers: { "Content-Type": "application/json" } },
        );

        event.waitUntil(URL_DB.put(urlKey, originalUrl));

        return response;
    } catch (error) {
        console.error(error, error.stack);
        return new Response("Unexpected Error", { status: 500 });
    }
};

For at prøve dette lokalt (du kan bruge wrangler dev for at starte serveren lokalt), skal du bruge curl kommando nedenfor:

curl --request POST \\
  --url http://127.0.0.1:8787/api/url \\
  --header 'Content-Type: application/json' \\
  --data '{
    "originalUrl": "https://www.google.com/"
}'

Omdirigerer kort URL

Som en URL-forkortelsestjeneste ønsker vi, at brugere skal kunne omdirigere til deres oprindelige URL, når de besøger en kort URL:

// handlers/redirectShortUrl.js
export const redirectShortUrl = async ({ params }) => {
    const urlKey = decodeURIComponent(params.text);
    const originalUrl = await URL_DB.get(urlKey);
    if (originalUrl) {
        return Response.redirect(originalUrl, 301);
    }
    return new Response("Invalid Short URL", { status: 404 });
};

Hvad med sletning? Da brugeren ikke kræver nogen autorisation for at forkorte en URL, blev beslutningen truffet for at gå videre uden en sletnings-API, da det ikke giver mening, at enhver bruger blot kan slette en anden brugers korte URL.

For at prøve vores URL-forkorter lokalt skal du blot køre wrangler dev.

Bonus:håndtering af duplikering med caching

Hvad sker der, hvis en bruger beslutter sig for gentagne gange at forkorte den samme URL? Vi ønsker ikke, at vores KV ender med duplikerede URL'er med unikke UUID tildelt dem vel?

For at afbøde dette kunne vi bruge en cache-middleware, der cacher den originale URL, der er indsendt af brugere ved hjælp af Cache API:

import { URL_CACHE } from "../utils/constants";

export const shortUrlCacheMiddleware = async (request) => {
    const { originalUrl } = await request.clone().json();

    if (!originalUrl) {
        return new Response("Invalid Request Body", {
            status: 400,
        });
    }

    const cache = await caches.open(URL_CACHE);
    const response = await cache.match(originalUrl);

    if (response) {
        console.log("Serving response from cache.");
        return response;
    }
};

For at bruge denne cache-middleware skal du blot opdatere vores index.js derfor:

// index.js
...
router.post('/api/url', shortUrlCacheMiddleware, createShortUrl)
...

Endelig skal vi sørge for, at vi opdaterer vores cache-forekomst med den originale URL, når vi forkorter den:

// handlers/createShortUrl.js
import { URL_CACHE } from "../utils/constants";
import { generateUniqueUrlKey } from "../utils/urlKey";

export const createShortUrl = async (request, event) => {
    try {
        const urlKey = await generateUniqueUrlKey();

        const { host } = new URL(request.url);
        const shortUrl = `https://${host}/${urlKey}`;

        const { originalUrl } = await request.json();
        const response = new Response(
            JSON.stringify({
                urlKey,
                shortUrl,
                originalUrl,
            }),
            { headers: { "Content-Type": "application/json" } },
        );

        const cache = await caches.open(URL_CACHE); // Access our API cache instance

        event.waitUntil(URL_DB.put(urlKey, originalUrl));
        event.waitUntil(cache.put(originalUrl, response.clone())); // Update our cache here

        return response;
    } catch (error) {
        console.error(error, error.stack);
        return new Response("Unexpected Error", { status: 500 });
    }
};

Under min test med wrangler dev , det ser ud til, at Worker-cachen ikke virker lokalt eller på noget worker.dev-domæne.

Løsningen for at teste dette er at køre wrangler publish at udgive applikationen på et brugerdefineret domæne. Du kan validere ændringerne ved at sende en anmodning til /api/url endepunkt, mens du observerer loggen via wrangler tail .

Implementering

Intet sideprojekt bliver nogensinde udført uden at være vært for det vel?

Før du udgiver din kode, skal du redigere wrangler.toml fil og tilføj din Cloudflare account_id inde. Du kan læse mere information om konfiguration og publicering af din kode kan findes i den officielle dokumentation.

For at implementere og udgive nye ændringer til din Cloudflare Worker skal du blot køre wrangler publish . For at implementere din applikation til et tilpasset domæne, se dette korte klip.

Hvis du er fortabt halvvejs, kan du altid tjekke GitHub-depotet her. Og det er det!

Afsluttende tanker

Helt ærligt, dette er det sjoveste, jeg har haft i et stykke tid - at researche, skrive og bygge denne POC på samme tid. Der er meget mere i mit sind, som vi kunne have gjort for vores URL-forkorter; bare for at nævne nogle få:

  • Lagring af metadata såsom oprettelsesdato, antal besøg
  • Tilføjelse af godkendelse
  • Håndter kort URL sletning og udløb
  • Analytics for brugere
  • Tilpasset link

Et problem, som de fleste URL-forkortelsestjenester står over for, er, at korte URL'er ofte misbruges til at føre brugere til ondsindede websteder. Jeg synes, det ville være et interessant emne at se nærmere på.

Det var alt for i dag! Tak fordi du læste med og hej!

Denne artikel blev oprindeligt publiceret på jerrynsh.com