GraphQL ved REST-auranten

En velsmagende introduktion til GraphQL

Det er onsdag aften, der arrangeres et andet stort møde på min favoritbegivenhedsplads: REST-auranten.

Efter samtalerne kan de deltagende få forskellige mad på forskellige ”ruter”.

Forskellige ruter for at få salater eller burgere hos REST-auranten.

De har som regel de samme to valg: avocadosalat og rejerburger. Avocado salat og rejerburger. Men hvad jeg virkelig sulter efter er Rejesalat! Men desværre er der ingen GET / salater? Med = rejerute. Så hvad kan jeg gøre?

Jeg får selvfølgelig rejer fra burgere!

Så jeg går til GET / salater-ruten og står derefter i kø for at hente 3 burgere fra GET / burgere. Jeg går til et frit sæde ved et bord, jeg henter rejer fra burgere og smider resten. Og dreng er jeg glad for at have min Rejer salat.

Under- og overhentning

Når jeg er færdig med min rejesalat, føler jeg mig trist. Ikke kun spildt tid på at stå i linjen to gange i stedet for at hænge ud med mine venner, jeg var nødt til at smide masser af mad.

Men i det mindste har jeg nu et godt eksempel til at forklare begreberne under og overhentning. Lad os lave noget kodning!

(Du kan finde alle kodeeksempler på github.com/gr2m/restaurant-graphql)

Her er en simpel Node.js-server bygget med express, der afslører to ruter for salater og burgere. Begge returnerer henholdsvis en salat / burger som standard og accepterer en valgfri parameter for count-forespørgsel for at hente mere end en med en enkelt anmodning

const express = kræver (“express”);
const app = express ();
// definere en salat og en burger
const salat = {avocado: 1, mango: 1, tomat: 0,2, ruccola: sand, løg: sand};
const burger = {boller: 2, rejer: 1, æg: 1, salat: 2,5, mayo: sand};
// definere matriser på 100 hver
const salater = nyt array (100). fyld (salat);
const burgere = new Array (100). udfyld (burger);
// definere ruterne med den valgfri parameter for tælleforespørgsel
app.get (“/ salater”, ({forespørgsel: {count}}, res) => res.json (få (salater, tælle)));
app.get (“/ burgere”, ({query: {count}}, res) =>
 res.json (få (burgere, tælle))
);
// hjælper metode til at få et udsnit af arrayet baseret på tælling
const get = (what, count) => what.splice (0, parseInt (count) || 1);
// start serveren på localhost: 4000
app.listen (4000);

Åbning af http: // localhost: 4000 / salater ser sådan ud

Firefox gengiver JSON-svar pænt som standard

Skift til klientsiden.

(Bemærk: Hvis du får en fejl i din browser, kan du prøve i Chrome, da det har den bedste understøttelse af async / vente, hvilket gør kodeeksemplerne meget enklere)

// hjælperfunktion til at sende en GET-anmodning til en given rute
funktion get (sti) {
  vende tilbage (vent på hentning (`$ {location.protocol} // $ {location.host} $ {path}`)). json ()
}
lad [salat] = afvente få ("/ salater");
// salat: {"avokado": 1, "mango": 1, "tomat": 0,2, "ruccola": sand, "løg": sand}
slette salat. tomat;
// TODO: Bed teamet om at liste tomat i saldsrute-menuen!
lad burgere = afvente få ("/ burgere? tæller = 3");
// burgere: [
// {"boller": 2, "rejer": 1, "æg": 1, "salat": 2.5, "mayo": sand},
// {"boller": 2, "rejer": 1, "æg": 1, "salat": 2.5, "mayo": sand},
// {"boller": 2, "rejer": 1, "æg": 1, "salat": 2.5, "mayo": sand}]
Object.assign (salat, {
  rejer: burgere. reduktion (
    (numShrimps, burger) => numShrimps + burger.shrimp,
    0
  )
});
// salat: {"avokado": 1, "mango": 1, "ruccola": sandt, "løg": sandt, rejer: 3}

Jeg får en salat ved at hente ruten GET / salater. Efter denne første anmodning mangler jeg stadig ingredienser, så jeg er nødt til at sende en anden anmodning. Dette kaldes underhentning.

Derefter henter jeg 3 burgere fra GET / burgere? Count = 3. Derefter reducerer jeg burgere til det samlede antal rejer. 3 rejer er hvad jeg havde brug for til min rejesalat, men hvad jeg modtog i stedet var 3 burgere med alle ingredienser. Dette kaldes overhentning.

Sammenfattende

  1. Under-hentning
    FÅ / salater fik mig salaten, men ingen rejer
  2. Over-hentning
    GET / burgere? Tælling = 3 fik mig rejer, men jeg må smide resten af ​​burgere væk.

Introduktion af GraphQL

REST-aurant-teamet er meget venlige og miljøbevidste mennesker. De ønsker ikke at se mad gå til spilde, og efter nogle undersøgelser finder de ud af GraphQL, som ser ud til at tackle det problem perfekt.

For det næste møde sætter teamet en tredje rute: POST / grafql.

Der er ikke behov for en skriftlig menu til GraphQL, i stedet konfigurerer de en terminal med indlejret dokumentation, som de deltagende kan bruge til at skrive og sende deres forespørgsel. De kalder det GraphQL Query-aitor 3000!

GraphQL Query-aitor 3000 er bare et smukt navn på GraphiQL (bemærk i), en simpel webformular til at sende GraphQL-forespørgsler med indbygget auto-komplet. GraphiQL er lidt svært at udtale, hvis du ikke kender det, så folk giver det forskellige navne, ligesom GitHubs opdagelsesrejsende. Det udtales som "grafisk", forresten :)

GraphiQL webapplikationen

Graphiql viser alle tilgængelige indstillinger, mens du skriver. Der er ikke mere gætte, ikke mere at slå op efter ejendomsnavne, og vigtigst af alt, ikke mere ud af synkroniseringsdokumentation, fordi dokumentationen er genereret fra det samme skema, som serveren og klienterne bruger. For eksempel ser du “tomat” i rullemenuen med autofuldførelse i GraphiQL, mens den manglede i menuen til ruten GET / salater.

Den fulde forespørgsel om at anmode om alle ingredienser men tomat fra en salat og kun rejer fra 3 burgere ser sådan ud

{
  salater {
    avocado
    arugula
    mango
    løg
  }
  burgere (tæller: 3) {
    reje
  }
}

Svaret fra serveren følger træstrukturen i min forespørgsel, og det inkluderer nøjagtigt det, jeg bad om, ikke mindre, ikke mere.

{
  "data": {
    "salater": [{"avokado": 1, "ruccola": sand, "mango": 1, "løg": sand}],
    "burgere": [{"rejer": 1}, {"rejer": 1}, {"rejer": 1}]
  }
}

GraphQL: hvad du har brug for er hvad du får.

Her er et par fakta om GraphQL:

  • Ligesom REST er GraphQL en specifikation, ikke et værktøj.
  • Det er sprogagnostisk for både servere og klienter
  • Et GraphQL API er bygget op omkring et skema
  • Et skema er et simpelt tekstdokument og bruges som kontrakt mellem klient og server.

Vi vil dykke ned i GraphQL-skemaet til vores eksempelapplikation på et minut. Men jeg vil understrege, at GraphQL er sprogagnostisk, da det ofte ses sammen med React og Node.js. Årsagen er, at både React og GraphQL er projekter af Facebook. Mens React er et faktisk JavaScript-bibliotek, er GraphQL bare en specifikation, der er implementeringer på mange programmeringssprog allerede.

Lad os oprette en enkel tekstfil kaldet schema.graphql. Et GraphQL-skema skal følge syntaksen i den nævnte GraphQL-specifikation. Til vores eksempelapp er hele skemaet kun 20 linjer langt:

type forespørgsel {
  burgere (tæller: Int = 1): [Burger]
  salater (tæller: Int = 1): [Salat]
}
type Burger {
  boller: Int!
  rejer: Float!
  æg: Float!
  salat: Boolsk!
  mayo: Boolsk!
}
type salat {
  avokado: Float!
  mango: Float!
  tomat: Float!
  ruccola: Boolsk!
  løg: Boolsk!
}

Forespørgselstypen definerer, hvad der kan anmodes om ved roden, i dette tilfælde er det salater og burgere. Et valgfrit tælles heltal kan bestås. Det er standard til 1. Du kan se det afspejles i GraphQL-forespørgslen vist ovenfor.

salater returnerer en matrix med genstande af type salat. Salat-typen definerer alle dens ingredienser. For eksempel er avocado et floatnummer, der giver mulighed for et decimalpoint. Det samme med mango, agurk og tomat. Løg er en boolsk, den kan enten være sand eller falsk.

Burgerboller er af typen Int, for hvem vil have en burger med en halv bolle? Rejer og æg er floats, salat og mayo er booleans.

På serveren er kodetillægningerne følgende.

const {readFileSync} = kræve ("fs");
const bodyParser = kræver ("body-parser");
const {graphqlExpress, graphiqlExpress} = kræver ("apollo-server-express");
const {makeExecutableSchema} = kræve ("graphql-tools");
const schema = makeExecutableSchema ({
  typeDefs: readFileSync ("schema.graphql", "utf8"),
  beslutningstagere: {
    Forespørgsel: {
      salater: (_, {count}) => få (salater, tælle),
      burgere: (_, {count}) => get (burgere, count)
    }
  }
});
app.use ("/ grafql", bodyParser.json (), grafqlExpress ({skema}));
app.use ("/ grafiql", grafiqlExpress ({endpointURL: "/ graphql"}));

Du kan se den fulde kode på github.com/gr2m/restaurant-graphql/tree/master/02-graphql.

Størstedelen af ​​arbejdet udføres af to npm-moduler, som vi først skal installere: apollo-server-express og graphql-tool.

Schema.graphql skal omdannes til en JavaScript-repræsentation, så det kan behandles i / grafql-rutehåndtereren. Jeg læste den rå fil og videregiver den som typeDefs-egenskab til makeExecutableSchema-funktionen.

Den anden egenskab, resolver, definerer forespørgselopløsninger, der fungerer svarende til rutehandlere. Jeg genbruger get-hjælperen vist i den første server.js-kode i begyndelsen af ​​dette indlæg for at returnere et udsnit af henholdsvis salater eller burgere, baseret på det valgfri tællerargument.

Endelig definerer jeg endelig GraphQL middleware, som afslører POST / grafql-ruten såvel som GraphiQL-webapplikationen på / grafiql.

Placer forespørgslen til `/ graphql`-endepunktet i` forespørgsel`-tasten for et JSON-objekt.

// hjælperfunktion til at sende en POST-anmodning til den givne rute
async funktion post (sti, data) {
  vende tilbage (vent på hentning (`$ {location.protocol} // $ {location.host} $ {path}`, {
    metode: 'post',
    organ: JSON.stringify (data),
    overskrifter: {
      'Content-Type': 'applikation / json'
    }
  })). JSON ()
}
lad {data: {salater: [salat], burgere}} = afvente indlæg ('/ grafql', {
  forespørgsel: `{
    burgere (tæller: 3) {
      reje
    }
    salater {
      avocado
      arugula
      mango
      løg
    }
  } `
})
// salat: {"avokado": 1, "mango": 1, "ruccola": sand, "løg": sand}
// burgere: [{"rejer": 1}, {"rejer": 1}, {"rejer": 1}]
Object.assign (salat, {
  rejer: burgers.reduce ((numShrimps, burger) => numShrimps + burger.shrimp, 0)
})
// salat: {"avokado": 1, "mango": 1, "ruccola": sandt, "løg": sandt, rejer: 3}

Svaret returnerer nøjagtigt de ingredienser, jeg har brug for, jeg skal bare reducere burgere til 3 rejer og tildele det til salatgenstanden.

Sammenfattende

  • Et GraphQL API er normalt bare et andet REST-endepunkt, f.eks. POST / grafql
  • En forespørgsel uddrager et datatræ for at få nøjagtigt, hvad der er nødvendigt
  • serversvaret svarer til træstrukturen i forespørgslen

Yay! Jeg kan nyde min avocado-rejesalat i god samvittighed nu.

Vedvarende forespørgsler

Ved det næste møde ønsker alle at afprøve GraphQL Query-aitor 3000. Og resultatet er en meget lang række

Lange linjer på GraphQL-terminalen

Det er fantastisk at få nøjagtigt det, vi ønsker, men det tager meget længere tid at indtaste forespørgslen så bare hente en burger fra GET / burgere. Og efter at have sendt POST / graphql-anmodningen, skal serveren behandle den nye ordre på stedet.

REST-aurant-teamet mødes igen og tænker over en løsning for at gøre POSTing og behandling af GraphQL-forespørgsler mere effektive. Og de kommer med en løsning: huske forespørgsler! Hver gang nogen poster en forespørgsel, bliver de spurgt, om de vil have det husket til næste gang og modtage et reference-ID til gengæld.

Holdet sender også en e-mail forud for den næste begivenhed, der beder deltagerne om at forregistrere deres forespørgsler. Med sådan information på forhånd kan teamet allerede tilberede noget af maden, hvilket også reducerer behandlingstiden.

Vedvarende forespørgsler er forespørgselsstrenge, der er gemt med et unikt ID i en nøgle / værdilager. I vores servereksempel opretter jeg en persisted-queries.js, der definerer en enkelt forespørgsel med id 1:

module.exports = {
  1: `{
  burgere (tæller: 3) {
    reje
  }
  salater {
    avocado
    arugula
    mango
    løg
  }
} `
};

På serveren skal jeg tilføje et par linjer for at indlæse den persisted-queries.js-fil og tilføje middleware, der kontrollerer, om en id-egenskab blev POSTet

const persistedQueries = kræver ("./ persisted-queries");
app.use ("/ grafql", bodyParser.json (), (req, res, næste) => {
  if (persistedQueries [req.body.id]) {
    req.body.query = persistedQueries [req.body.id];
  }
  Næste();
});

Hvis der er blevet sendt en id, og hvis der eksisterer en vedvarende forespørgsel med det bestået ID, skal du indstille forespørgslen til det id fra den vedvarende forespørgselslager og fortsætte forespørgslen som tidligere.

Jeg har ikke længere brug for at sende forespørgslen, i stedet for sender jeg bare mit id, som er et. For at få det samme resultat som vist ovenfor i browseren behøver jeg ikke længere at sende den fulde forespørgsel:

lad {data: {salater: [salat], burgere}} = afvente indlæg (‘/ grafql’, {
  id: 1
})

Resultatet er det samme som før, men størrelsen på anmodningen er minimal i sammenligning. Dette kan have store ydeevneeffekter, da GraphQL-forespørgsel kan blive meget kompleks, og derfor bliver anmodningen stor. Og opstrømsforbindelsen er som regel meget værre end downstreamforbindelsen også.

For teamet REST-aurant er det dejligt at vide forespørgsler på forhånd. Forespørgsler kan prækompileres, og data kan cacheres bedre. Nogle GraphQL API'er går endda så langt som at deaktivere ikke-vedvarende forespørgsler helt for at forbedre sikkerheden og maksimere effektiviteten.

Sammenfattende

  • Vedvarende forespørgsler er ikke en del af GraphQL-specifikationen, men en fælles implementeringsdetalje
  • Vedvarende forespørgsler gemmes på serveren, klienter sender kun forespørgsels-id'er
  • Afsendelse af kun et ID i stedet for en kompleks forespørgsel reducerer anmodningsstørrelsen
  • Vedvarende forespørgsler kan prækompileres på serveren
  • For at forbedre sikkerheden kan ikke-vedvarende forespørgsler helt deaktiveres

Mutationer

REST-auranten vokser i popularitet og oftere end ikke begivenheden løber tør for mad, før alle kunne få en bid. Indtil videre blev 100 salater og 100 burgere leveret af en catering, men holdet beslutter, at de vil ansætte et madlavningsteam, der kan skabe flere burgere og salater, der imødekommer den voksende efterspørgsel.

Mutationer er en del af GraphQL-specifikationen og skal defineres
i GraphQL-skemaet. Det er kun 4 ekstra linjer

type mutation {
  addBurgers (tæller: Int = 1): Int
  addSalads (tæller: Int = 1): Int
}

Først tilføjer jeg en mutation for at tilføje salater. Mutationen accepterer et valgfrit tællerargument, der som standard er 1. Mutationen returnerer et heltal, som vil være den samlede mængde tilgængelige salater. Det samme for burgere.

En GraphQL-forespørgsel, der tilføjer en salat og 3 burgere, ser sådan ud:

mutation {
  addSalads
  addBurgers (tæller: 3)
}

En mutation skal starte med mutationsnøgleordet. Du kan sende flere mutationer i en enkelt anmodning, en anden fordel i forhold til REST API'er. Du kan ikke kombinere en mutation med en forespørgsel, men du kan definere mutationsresponsen, hvis du vil.

For ovenstående forespørgsel vil svaret se sådan ud

{
  "data": {
    "addSalads": 1,
    "addBurgers": 3
  }
}

I serverkoden indstiller vi nu salater og burgere til at tømme arrays og ændre resolverobjektet med en Mutation-egenskab:

// start med tom salat og burger-arrays nu
const salater = [];
const burgere = [];
// tilføj mutationsopløsere
const schema = makeExecutableSchema ({
  typeDefs: readFileSync ("schema.graphql", "utf8"),
  beslutningstagere: {
    Forespørgsel: {
      salater: (_, {count}) => få (salater, tælle),
      burgere: (_, {count}) => get (burgere, count)
    },
    Mutation: {
      addSalads: (_, {count}) => {
        salads.push (... nyt Array (tælle). udfyld (salat));
        retur salater. længde;
      },
      addBurgers: (_, {count}) => {
        burgers.push (... nye Array (tælle). udfyld (burger));
        retur burgere. længde;
      }
    }
  }
});

Funktionen addSalads og addBurgers kaldes med tællerargumentet, der som standard er 1 som defineret i skemaet. Tilføj en eller flere salater / burgere baseret på tællerargumentet. Begge funktioner returnerer derefter længden af ​​den respektive matrix.

Afsendelse af en mutation fra browseren svarer meget til at sende en forespørgsel:

afvente indlæg ('/ grafql', {
  forespørgsel: `mutation {
    addSalads
    addBurgers (tæller: 3)
  } `
})

Sammenfattende

  • Mutationer bruges til at oprette, opdatere eller slette data.
  • Flere mutationer kan sendes med en enkelt anmodning. De behandles sekventielt.
  • Mutationer kan ikke kombineres med forespørgsler, men mutationssvarene kan filtreres

GraphQL-abonnementer

Når REST-auranten er ude af salat eller burgere, er det ret irriterende at være foran linjen og spørge gentagne gange: “kan jeg have min salat endnu?”. I stedet vil jeg, at serveren skal fortælle mig, hvornår der er nok mad til rådighed til at opfylde min forespørgsel. Dette er en almindelig brugssag for abonnementer.

Abonnement er den tredje operationstype af GraphQL. GraphiQL har indbygget
support til abonnementer via websockets, de kan indsendes ligesom
som forespørgsler og mutationer. Når den er indsendt, svarer serveren, at dataene vises, når der er sket en ændring. Så lad os foretage ændringer!

Abonnement til venstre viser opdateringer fra mutationer til højre.

Jeg forlader abonnementet, som det er i en browser, mens jeg poster mutationer i en anden. Umiddelbart efter jeg har sendt mutationerne, er antallet på
det første vindue ændres i overensstemmelse hermed.

Lad os se, hvordan implementeringen ser ud.

Tilsætningerne til schema.graphql er igen ret enkle:

type abonnement {
  madTilføjet: Statistik
}
type statistik {
  burgere: Int
  salater: Int
}

FoodAdded-abonnementet kaldes med type Stats, der har heltalegenskaber for det samlede antal tilgængelige salater og burgere

Tilføjelserne til serveren er lidt mere komplekse:

// indlæse yderligere biblioteker, der er nødvendige for abonnementer
const {udføre, abonnere} = kræve ("grafql");
const {SubscriptionServer} = kræve ("abonnementer-transport-ws");
const {PubSub} = kræver ("graphql-abonnementer");
const pubsub = ny PubSub ();
const getStats = () => ({salater: salater. længde, burgere: burgere.længde});
const schema = makeExecutableSchema ({
  typeDefs: readFileSync ("schema.graphql", "utf8"),
  beslutningstagere: {
    Forespørgsel: {
      salater: (_, {count}) => få (salater, tælle),
      burgere: (_, {count}) => get (burgere, count)
    },
    Mutation: {
      addSalads: (_, {count}) => {
        salads.push (... nyt Array (tælle). udfyld (salat));
        // offentliggøre madTilføjet og videregive statistik
        pubsub.publish ("foodAdded", {foodAdded: getStats ()});
        retur salater. længde;
      },
      addBurgers: (_, {count}) => {
        burgers.push (... nye Array (tælle). udfyld (burger));
        // offentliggøre madTilføjet og videregive statistik
        pubsub.publish ("foodAdded", {foodAdded: getStats ()});
        retur burgere. længde;
      }
    },
    // tilføj Abonnementsejendom til skemaopløsere
    Abonnement: {
      madtilføjet: {
        abonner: () => pubsub.asyncIterator ("foodAdded")
      }
    }
  }
});
// tilføj abonnementerEndpoint til / graphiql middleware
app.use (
  "/ Graphiql",
  graphiqlExpress ({
    endpointURL: "/ graphql",
    abonnementerEndpoint: "ws: // localhost: 4000 / abonnementer"
  })
);
// Opret en ny serverforekomst i stedet for app.listen (4000)
const server = createServer (app);
server.listen (4000);
// ... som kan overføres til SubscriptionServer-konstruktøren
ny AbonnementServer (
  {skema, eksekver, abonner},
  {sti: "/ abonnementer", server}
);

Du kan se den fulde kode på https://github.com/gr2m/restaurant-graphql/tree/master/05-graphql.

Et GraphQL-abonnement skubber data til klienten, når der sker en ændring i stedet for at klienten trækker dem baseret på et interval, der venter på, at en ændring skal ske. Kernen i vores implementering er modulerne abonnementer-transport-ws og grafql-abonnementer. Sidstnævnte giver os PubSub, som vi bruger til at offentliggøre foodAdded-begivenheden i begge vores mutationer. Derefter returnerer vi i abonnementsopløseren en asynkron iterator, der skubber de videregivne data til foodAdded-begivenheden gennem den åbne web-socket-forbindelse. Vi videregiver det kompilerede skema til den nye SubscriptionServer, der implementerer ruten til websocket.

Jeg ved, det er ganske meget at tage i, men det er også hella cool, når det fungerer :)

Sammenfattende

  • Når du venter på ændringer, i stedet for at anmode om data baseret på et interval, abonnerer du på en datastrøm.
  • Serveren offentliggør data, så snart de bliver tilgængelige.

Sulten?

  • Se & remix eksemplet app på glitch
  • Se dette indlæg som screencast med live kodning
  • Udforsk kildekoden
  • Følg mig på twitter for at få flere GraphQL-godbidder