Lucrez cu GraphQL de câteva luni acum, dar abia recent am început să folosesc biblioteca de instrumente grafql Apollo. După ce am învățat câteva moduri de expresie, sunt capabil să-mi machiez rapid un API funcțional. Acest lucru se datorează în mare măsură abordării sale declarative cu cod redus a definițiilor de tip.

Începând cu exemplul lor

Apollo are un interactiv Platforma de lansare site-ul web, precum cele acoperite în Seria Swagger. Există mai multe exemple de scheme care le puteți folosi, iar pentru acest articol le voi folosi Schema Post și Autori. Puteți descărca sau furniza codul.

Voi rearanja folderele proiectului. Pentru această postare o voi descărca și stoca în Github, astfel încât să pot ramifica și modifica codul prin fiecare pas. Pe parcurs, voi lega ramurile de acest post.

Cele elementare

  • declararea tipurilor de schemă

În Launchpad, veți vedea un typeDefs literal șablon:

const typeDefs = `
  type Author {
    id: Int!
    firstName: String
    lastName: String
    posts: [Post] # the list of Posts by this author
  }

type Post {
    id: Int!
    title: String
    author: Author
    votes: Int
  }

# the schema allows the following query:
  type Query {
    posts: [Post]
    author(id: Int!): Author
  }

# this schema allows the following mutation:
  type Mutation {
    upvotePost (
      postId: Int!
    ): Post
  }
`;

Sunt două entități definit, Author și Post. În plus, există două „magii” tipuri: Query și Mutation. Tipul de interogare definește rădăcina accessors. În acest caz, există un accesor care să le aducă pe toate Posts, și altul să aducă un singur Author de ID.

Rețineți că nu există nicio modalitate de interogare directă pentru o listă de autori sau pentru o singură postare. Este posibil să adăugați astfel de interogări ulterior.

  • declarând rezolvatori

Rezolvatorii oferă logica necesară pentru a susține schema. Sunt scrise ca un obiect JavaScript cu chei care se potrivesc tipurilor definite în schemă. resolver prezentat mai jos funcționează împotriva datelor statice, pe care le voi acoperi într-o clipă.

const resolvers = {
  Query: {
    posts: () => posts,
    author: (_, { id }) => find(authors, { id: id }),
  },
  Mutation: {
    upvotePost: (_, { postId }) => {
      const post = find(posts, { id: postId });
      if (!post) {
        throw new Error(`Couldn't find post with id ${postId}`);
      }
      post.votes += 1;
      return post;
    },
  },
  Author: {
    posts: (author) => filter(posts, { authorId: author.id }),
  },
  Post: {
    author: (post) => find(authors, { id: post.authorId }),
  },
};

A lega schema și resolver împreună, vom crea o instanță de schemă executabilă:

export const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
});
  • sursa de date

Pentru acest exemplu simplu, datele provin din două tablouri de obiecte definite ca constante: authors și posts:

const authors = [
  { id: 1, firstName: 'Tom', lastName: 'Coleman' },
  { id: 2, firstName: 'Sashko', lastName: 'Stubailo' },
  { id: 3, firstName: 'Mikhail', lastName: 'Novikov' },
];

const posts = [
  { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },
  { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },
  { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },
  { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },
];
  • server-ul

Puteți difuza schema executabilă prin graphql_express, apollo_graphql_express, sau graphql-server-express. Vedem asta în acest exemplu.

Biții importanți sunt:

import { graphqlExpress, graphiqlExpress } from 'graphql-server-express';
import { schema, rootValue, context } from './schema';

const PORT = 3000;
const server = express();

server.use('/graphql', bodyParser.json(), graphqlExpress(request => ({
  schema,
  rootValue,
  context: context(request.headers, process.env),
})));

server.use('/graphiql', graphiqlExpress({
  endpointURL: '/graphql',
}));

server.listen(PORT, () => {
  console.log(`GraphQL Server is now running on 
http://localhost:${PORT}/graphql`);
  console.log(`View GraphiQL at 
http://localhost:${PORT}/graphiql`);
});

Rețineți că există două bucăți de middleware GraphQL în uz:

  • graphqlExpress
    serverul GraphQL care gestionează interogări și răspunsuri
  • graphiqlExpress
    serviciul web interactiv GraphQL care permite interogări interactive printr-o interfață HTML

Reorganizarea

Pentru aplicațiile mari, vă sugerăm să împărțiți codul serverului GraphQL în 4 componente: Schemă, Rezolutori, Modele și Conectori, care se ocupă fiecare de o anumită parte a lucrării. (http://dev.apollodata.com/tools/graphql-tools/)

Punerea fiecărui tip de componentă în propriul fișier are sens. Voi merge unul mai bine și voi pune fiecare set de componente într-un folder propriu „domeniu”.

De ce domenii?

Domeniile sunt o modalitate convenabilă de a împărți un sistem mare în zone de operare. În cadrul fiecărui domeniu pot exista subdomenii. În general, subdomeniile au un context delimitat. Într-un context delimitat, numele entităților, proprietățile și procesele au semnificație precisă.

Consider că contextele delimitate sunt utile în timpul analizei, mai ales atunci când vorbesc cu experți din domeniu.

Mersul în unguent este că tipurile GraphQL ocupă un singur spațiu de nume, astfel încât pot exista conflicte de denumire. Mai multe despre asta mai târziu.

Declarativ GraphQL scrieti mai putin cod si faceti mai mult

Voi numi acest domeniu posturi de autor, și introduceți componentele aferente în authorposts folder. În acest sens, voi crea câte un fișier pentru datasource, resolvers, și schemă. Să aruncăm și un index.js fișier pentru a simplifica importul. Schema și fișierele de server originale vor rămâne în folderul rădăcină, dar fișierul schema.js codul va fi scheletic. find și filter metode importate din lodash va fi eliminat în favoarea metodelor ES6 native native. Sursa rezultată este aici.

Fișierul schemei principale a devenit mai simplu. Oferă structura scheletică pentru extinderea ulterioară de către scheme în domeniile noastre.

import {
    makeExecutableSchema
} from 'graphql-tools';

import {
    schema as authorpostsSchema,
    resolvers as authorpostsResolvers
} from './authorposts';

const baseSchema = [
    `
    type Query {
        domain: String
    }
    type Mutation {
        domain: String
    }
    schema {
        query: Query,
        mutation: Mutation
    }`
]

// Put schema together into one array of schema strings and one map of resolvers, like makeExecutableSchema expects
const schema = [...baseSchema, ...authorpostsSchema]

const options = {
    typeDefs: schema,
    resolvers: {...authorPostResolvers}
}

const executableSchema = makeExecutableSchema(options);

export default executableSchema;
rootSchema.js

A domain schema este importată pe liniile 7-8 și base schema pe liniile 11–23. Veți observa că există un domeniu proprietate. Acest lucru este arbitrar, dar GraphQL, sau graphql-tools, insistă ca o proprietate să fie definită.

Schema completă este construită pe linia 26 și un executableSchema instanța este creată având în vedere schema și resolvers definit până acum pe liniile 28–33. Acesta este ceea ce este importat de server.js cod, care este în mare parte neschimbat față de original.

Există un truc pentru a împărți o schemă în acest fel. Hai să aruncăm o privire:

import {
    authors,
    posts
} from './dataSource';

const rootResolvers = {
    Query: {
        posts: () => posts,
        author: (_, {
            id
        }) => authors.find(a => a.id === id)
    },
    Mutation: {
        upvotePost: (_, {
            postId
        }) => {
            const post = posts.find(p => p.id === postId);
            if (!post) {
                throw new Error(`Couldn't find post with id ${postId}`);
            }
            post.votes += 1;
            return post;
        }
    },
    Author: {
        posts: (author) => posts.filter(p => p.authorId === author.id)
    },
    Post: {
        author: (post) => authors.find(a => a.id === post.authorId)
    }
};


export default rootResolvers;
authorpostResolvers.js
const typeDefs = [
    `
  type Author {
    id: Int!
    firstName: String
    lastName: String
    posts: [Post] # the list of Posts by this author
  }
  type Post {
    id: Int!
    title: String
    author: Author
    votes: Int
  }
  # the schema allows the following query:
  extend type Query {
    posts: [Post]
    author(id: Int!): Author
  }
  # this schema allows the following mutation:
  extend type Mutation {
    upvotePost (
      postId: Int!
    ): Post
  }
`
];


export default typeDefs;
authorpostSchema.js

Prima listă, authorpostResolvers.js, este cam o treabă cut’n’paste din original schema.js sursă din exemplul lui Apollo. Cu toate acestea, în authorpostSchema.js cod, noi extinde Query și Mutator definiții care sunt declarate în schema de bază. Dacă nu utilizați extinde cuvânt cheie, constructorul schemei executabile se va plânge de două Interogare definiții.

Continuare …

Acesta este un început bun pentru organizarea mai multor scheme, una pentru fiecare domeniu de interes (atâta timp cât aveți în vedere spațiul de nume global pentru tipuri), dar o schemă completă, chiar și pentru un singur domeniu, poate deveni imensă. Din fericire, puteți descompune fiecare schemă și mai departe, chiar până la nivelul entității, daca este necesar.

Iată o structură de directoare modificată și listări ale conținutului nou:

1611492966 476 Declarativ GraphQL scrieti mai putin cod si faceti mai mult
export default `
  type Author {
    id: Int!
    firstName: String
    lastName: String
    posts: [Post] # the list of Posts by this author
}`
author.js
export default `
type Post {
  id: Int!
  title: String
  author: Author
  votes: Int
}`
post.js
import Author from './components/author'
import Post from './components/post'

const typeDefs =
    `
  # the schema allows the following query:
  extend type Query {
    posts: [Post]
    author(id: Int!): Author
  }
  # this schema allows the following mutation:
  extend type Mutation {
    upvotePost (
      postId: Int!
    ): Post
  }
`;

export default [typeDefs, Author, Post];
schema.js

Putem obține granularitatea definind două fișiere componente, apoi importându-le într-o schemă de domeniu.

Nu trebuie să faceți o componentă per fișier. Dar doriți să fiți siguri că schema exportă aceste componente împreună cu schema în sine, așa cum se arată în linia 20 din schema.js. În caz contrar, probabil că veți pierde o dependență mai jos în lanțul de incluziune.

Scheme multiple și rezolutori

Adăugarea unei noi scheme pentru un domeniu nou este simplă. Creați un folder nou de domeniu și adăugați fișiere dataSource, rezolutori, schemă și index.js. De asemenea, puteți adăuga un folder de componente opțional cu definiții de tip de componentă.

1611492966 192 Declarativ GraphQL scrieti mai putin cod si faceti mai mult
const myLittleTypes = [{
    id: 1,
    description: 'This is good',
}, {
    id: 2,
    description: 'This is better',
}, {
    id: 3,
    description: 'This is the best!',
}];

export {
    myLittleTypes
};
dataSource.js
export default `
  type MyLittleType {
    id: Int!
    description: String
}`
myLittleType.js
import {
    myLittleTypes
} from './dataSource';

const rootResolvers = {
    Query: {
        myLittleType: (_, {
            id
        }) => myLittleTypes.find(t => t.id === id)
    },
};


export default rootResolvers;
resolvers.js
import MyLittleType from './components/myLittleType'

const typeDefs =
    `
  # the schema allows the following query:
  extend type Query {
    myLittleType(id: Int!): MyLittleType
  }
`;

export default [typeDefs, MyLittleType];
schema.js

În cele din urmă, fișierul rădăcină schema.js trebuie să combine schemele și rezoluțiile de pe ambele domenii:

//...
import {
    schema as myLittleTypoSchema,
    resolvers as myLittleTypeResolvers
} from './myLittleDomain';

import {
    merge
} from 'lodash';
//...
const schema = [...baseSchema, ...authorpostsSchema, ...myLittleTypoSchema]

const options = {
    typeDefs: schema,
    resolvers: merge(authorpostsResolvers, myLittleTypeResolvers)
}

Rețineți că a trebuit să includ lodash combina aici din cauza nevoii unei fuziuni profunde a celor două rezolvatori importurile.

Gestionarea coliziunilor spațiului de nume

Dacă vă aflați într-un proiect mare, veți întâlni coliziuni de nume de tip. S-ar putea să credeți că Contul dintr-un domeniu ar însemna același lucru cu Contul dintr-un alt domeniu. Cu toate acestea, chiar dacă ele înseamnă lucruri mai mult sau mai puțin similare, este posibil ca proprietățile și relațiile să fie diferite. Deci, din punct de vedere tehnic, nu sunt de același tip.

La momentul scrierii acestui articol, GraphQL folosește un singur spațiu de nume pentru tipuri.

Cum să rezolvați problema? Se pare că Facebook folosește un convenție de denumire pentru cele 10.000 de tipuri ale acestora. Oricât de ciudat pare, funcționează pentru ei.

Stiva Apollo graphql-tools pare să prindă duplicări de nume de tipuri. Deci ar trebui să fii bun acolo.

Există o discuție în curs despre dacă pentru a include spații de nume în GraphQL. Nu este o decizie simplă. Îmi amintesc complexitatea cauzată de introducerea Spații de nume XML acum 10 ani.

Unde să merg de aici?

Această postare zgârie doar suprafața modului în care s-ar putea organiza un set mare de scheme GraphQL. Următoarea postare va fi despre batjocorirea rezoluților GraphQL și despre modul în care este posibil să amestecați atât valori reale cât și valori batjocorite în răspunsurile la interogare.