基本示例

服务端

使用 Express 并额外安装两个依赖:

  1. npm install express express-graphql graphql --save

其中express用来启动服务器。express-graphql用来构建GraphQL API服务器,响应/graphql的 HTTP 请求。graphql用来创建 schema

  1. var express = require('express');
  2. var { graphqlHTTP } = require('express-graphql');
  3. var { buildSchema } = require('graphql');
  4. // 使用 GraphQL Schema Language 创建一个 schema
  5. var schema = buildSchema(`
  6. type Query {
  7. hello: String
  8. }
  9. `);
  10. // root 提供所有 API 入口端点相应的解析器函数
  11. var root = {
  12. hello: () => {
  13. return 'Hello world!';
  14. },
  15. };
  16. var app = express();
  17. app.use('/graphql', graphqlHTTP({
  18. schema: schema,
  19. rootValue: root,
  20. graphiql: true,
  21. }));
  22. app.listen(4000);

当服务端设置graphiql: true时,可以使用浏览器来手动执行GraphQL查询。

直接访问http://localhost:4000/graphql即可看到界面,然后用输入GraphQL查询语句,即可看到对应的数据类型:

image-20220416222115030

客户端

浏览器端采用fetch也可以查询,在客户端打开开发者工具,输入以下代码:

  1. async function request() {
  2. const res = await fetch('http://localhost:4000/graphql', {
  3. method: 'POST',
  4. headers: {
  5. 'Content-Type': 'application/json',
  6. Accept: 'application/json',
  7. },
  8. body: JSON.stringify({ query: '{ hello }' }),
  9. });
  10. await res.json().then(data => console.log(data));
  11. }
  12. request();

就可以看到对应的数据

  1. {data: {hello: 'hello world'}}

基本类型

基本类型有StringIntFloatBooleanID,这些类型在buildSchema中可以直接使用。

其中 ID 表示一个不会重复的值,下面的例子中使用uuid来生成。

默认情况下,每个类型都是可以为空的——这表示所有标量类型都可以返回 null。

如果不想为空则可以用一个感叹号表示一个类型不可为空,例如:String!表示非空字符串。

如果是列表类型,使用方括号将对应类型包起来,如 [Int] 就表示一个整数列表。

  1. var { v4: uuidv4 } = require('uuid');
  2. var schema = buildSchema(`
  3. type Query{
  4. name:String,
  5. age:Int,
  6. isMarried:Boolean,
  7. hobbies:[String!],
  8. id:ID
  9. }
  10. `);
  11. var rootValue = {
  12. name() {
  13. return 'this is name';
  14. },
  15. age() {
  16. return 123;
  17. },
  18. isMarried() {
  19. return false;
  20. },
  21. hobbies() {
  22. return ['吃饭', '睡觉'];
  23. },
  24. id() {
  25. return uuidv4();
  26. },
  27. };

传递参数

服务端 schema这样写:

  1. var schema = buildSchema(`
  2. type Query{
  3. rollDice(numDice: Int!, numSides: Int): [Int]
  4. }
  5. `);
  6. var rootValue = {
  7. rollDice({ numDice, numSides }) {
  8. let result = [];
  9. for (let i = 0; i < numDice; i++) {
  10. result.push(Math.floor(1 + Math.random() * numSides));
  11. }
  12. return result;
  13. },
  14. };

需要注意的点是rootValue.rollDice方法只接受一个参数,我们对应schema中的语句,

  1. rollDice(numDice: Int!, numSides: Int): [Int]

需要将查询的参数给解构出来。

  1. rollDice({ numDice, numSides }){}

前端通过GraphQL查询:

  1. {
  2. rollDice(numSides:6,numDice:3)
  3. }

通过代码查询,使用$来定义查询中的变量,并将变量作为单独映射来传递。

对象类型

现在我们把nameagehobbies等都封装成一个对象类型,让很多地方可以通过查询 user来获取数据:

  1. var schema = buildSchema(`
  2. type User{
  3. name:String,
  4. age:Int,
  5. isMarried:Boolean,
  6. hobbies:[String!],
  7. id:ID,
  8. }
  9. type Query{
  10. user:User
  11. }
  12. `);
  13. var rootValue = {
  14. user() {
  15. return {
  16. name: rootValue.name,// 可以写函数方法,会自动调用
  17. age: rootValue.age,
  18. isMarried: rootValue.isMarried,
  19. hobbies: rootValue.hobbies(),// 也可以直接返回函数调用后的数据
  20. id: rootValue.id(),
  21. };
  22. },
  23. name() {
  24. return 'this is name';
  25. },
  26. age() {
  27. return 123;
  28. },
  29. isMarried() {
  30. return false;
  31. },
  32. hobbies() {
  33. return ['吃饭', '睡觉'];
  34. },
  35. id() {
  36. return uuidv4();
  37. }
  38. };

这时候在前端graphql查询:

  1. {
  2. user {
  3. id
  4. name
  5. isMarried
  6. hobbies
  7. age
  8. }
  9. }
  10. /*
  11. {
  12. "data": {
  13. "user": {
  14. "id": "a57fcb5d-1a32-4ac6-a047-818d1af3c54b",
  15. "name": "this is name",
  16. "isMarried": false,
  17. "hobbies": [
  18. "吃饭",
  19. "睡觉"
  20. ],
  21. "age": 123
  22. }
  23. }
  24. }
  25. */

官方示例中的结合 class 实现是这样的:

比如现在要获取一个骰子,在后端的Query是这样写的:

  1. type RandomDie {
  2. numSides: Int!
  3. rollOnce: Int!
  4. roll(numRolls: Int!): [Int]
  5. }
  6. type Query{
  7. getDie(numSides: Int): RandomDie,
  8. }

然后定义一个RandomDie的类

  1. class RandomDie {
  2. numSides: number;
  3. constructor(numSides) {
  4. this.numSides = numSides;
  5. }
  6. rollOnce() {
  7. return 1 + Math.floor(Math.random() * this.numSides);
  8. }
  9. roll({ numRolls }) {
  10. var output = [];
  11. for (var i = 0; i < numRolls; i++) {
  12. output.push(this.rollOnce());
  13. }
  14. return output;
  15. }
  16. }

然后在rootValue中返回这个类的实例,也就是对象

  1. var rootValue = {
  2. getDie: ({ numSides }) => {
  3. return new RandomDie(numSides || 6);
  4. },
  5. };

此时可以通过clientGraphQL来查询:

  1. {
  2. getDie{
  3. roll(numRolls:3)
  4. numSides
  5. rollOnce
  6. }
  7. }

当对一个返回对象类型的 API 发出 GraphQL 查询时,可以通过嵌套 GraphQL 字段名来一次性调用对象上的多个方法。

这种方式的好处是将相同类别的查询封装到一个 class 中,适合于面向类思维的编程方式。

变更和输入类型

如果对数据进行修改,比如插入或者更新操作,这时候需要使用Mutation类型而不是Query

比如现在要添加一个信息和查询一个信息,分别对应的schema是这样的:

  1. type Mutation {
  2. setMessage(message: String): String
  3. }
  4. type Query {
  5. getMessage: String
  6. }

mutation会返回数据库所存的数据

例如,我把信息都存到一个变量里,最后返回出去的rootValue需要这么写:

  1. var fakeDatabase = {};
  2. var root = {
  3. setMessage: ({message}) => {
  4. fakeDatabase.message = message;
  5. return message;
  6. },
  7. getMessage: () => {
  8. return fakeDatabase.message;
  9. }
  10. };

前端根据上面的配置需要使用mutation来发送:

  1. mutation{
  2. setMessage(message:"这是前端发送的数据")
  3. }
  4. /*
  5. {
  6. "data": {
  7. "setMessage": "这是前端发送的数据"
  8. }
  9. }
  10. */

不过很多时候后端都会接受多个输入,但是输出是一致的。比如当 create 时,会判断前端有没有发送 id,如果没有则会新建一条消息,如果有则会更新与 id 相匹配的数据,最后返回给前端的都是同样的数据类型,这时候就要用输入类型来简化 schema,使用input关键字来定义输入:

  1. input MessageInput {
  2. content: String
  3. author: String
  4. }

然后定义新增、更新和删除的schema

  1. type Message {
  2. id: ID!
  3. content: String
  4. author: String
  5. }
  6. type DeleteStatus{
  7. success:Boolean
  8. }
  9. type Mutation {
  10. createMessage(createInput:messageInput):Message
  11. updateMessage(id:ID!,updateInput: messageInput): Message
  12. deleteMessage(id:ID!):DeleteStatus
  13. }
  14. type Query {
  15. getMessage(id:ID!): Message
  16. }

rootValue 中,我们都返回一个 class作为输出

  1. let dataBase: any = {
  2. '1': {
  3. content: '初始数据库的内容',
  4. author: 'qiuyanxi',
  5. },
  6. };
  7. // 如果 Message 拥有复杂字段,我们把它们放在这个对象里面。
  8. class Message {
  9. id: string;
  10. content: string;
  11. author: string;
  12. constructor(id, { content, author }) {
  13. this.id = id;
  14. this.content = content;
  15. this.author = author;
  16. }
  17. }
  18. var rootValue = {
  19. getMessage({ id }) {
  20. return new Message(id, dataBase[id]);
  21. },
  22. createMessage({ createInput }) {
  23. let id = uuidv4();
  24. dataBase.id = createInput;
  25. return new Message(id, createInput);
  26. },
  27. updateMessage({ id, updateInput }) {
  28. dataBase[id] = updateInput;
  29. return new Message(id, updateInput);
  30. },
  31. deleteMessage({ id }) {
  32. if (!dataBase[id]) {
  33. return { success: false };
  34. }
  35. delete dataBase[id];
  36. return { success: true };
  37. },
  38. };

最后我们在GraphQL浏览器端使用mutation进行查询

  1. mutation {
  2. createMessage(createInput: {author: "qiuyanxi", content: "这是内容"}) {
  3. id
  4. author
  5. content
  6. }
  7. updateMessage(id: "1", updateInput: {author: "qiuyanxi", content: "这是另一个内容"}) {
  8. author
  9. content
  10. }
  11. deleteMessage(id:"1") {
  12. success
  13. }
  14. }
  15. /*
  16. {
  17. "data": {
  18. "createMessage": {
  19. "id": "5ab856bf-da39-4afd-8b6a-7c8ebee8c4e3",
  20. "author": "qiuyanxi",
  21. "content": "这是内容"
  22. },
  23. "updateMessage": {
  24. "author": "qiuyanxi",
  25. "content": "这是另一个内容"
  26. },
  27. "deleteMessage": {
  28. "success": true
  29. }
  30. }
  31. }
  32. */

客户端携带参数请求

上面的案例中,客户端发送query请求并携带参数查询信息是这样写的:

  1. async function request() {
  2. const id="1"
  3. const res = await fetch('http://localhost:4000/graphql', {
  4. method: 'POST',
  5. headers: {
  6. 'Content-Type': 'application/json',
  7. Accept: 'application/json',
  8. },
  9. body: JSON.stringify({
  10. query: `
  11. {
  12. getMessage(id:${id}){
  13. id
  14. content
  15. author
  16. }
  17. }
  18. `,
  19. }),
  20. });
  21. await res.json().then(data => console.log(data));
  22. }

更加符合开发要求的写法是带上服务器中写的类型:

  1. body: JSON.stringify({
  2. query: `
  3. query GetMessage($id:ID!){
  4. getMessage(id:$id){
  5. id
  6. content
  7. author
  8. }
  9. }
  10. `,
  11. variables: {
  12. id,
  13. },
  14. }),

通过$id来作为GraphQL 的变量,然后它的类型就是服务端 GraphQL服务端的类型ID!,最后通过variables将声明好的变量给传递进去。

对应createMessage的客户端查询是这么写的

  1. body: JSON.stringify({
  2. query: `
  3. mutation CreateMessage($createInput:MessageInput){
  4. createMessage(createInput:$createInput){
  5. id
  6. content
  7. author
  8. }
  9. }
  10. `,
  11. variables: {
  12. createInput: {
  13. content: '内容',
  14. author: 'qyx',
  15. },
  16. },
  17. }),

客户端开发中,不可能去看服务端的代码,所以一般都会按照GraphQL的客户端 doc 来写对应的类型。

image-20220417132914867