Graphql + Node 实践(二):基础知识储备~

990 阅读9分钟

2. 基本的graphql + express使用

官方文档

  • 依赖的包:
    • graphql
    • express
    • express-graphql -> 支持express的graphql服务器

1. 建立一个graphql的基础服务器配置

1. 服务端配置
import express from "express";
import { buildSchema } from "graphql";
import { graphqlHTTP } from "express-graphql";
// @ts-ignore
import nunjucks from "nunjucks";
import path from "path";

const app = express();

app.set("view engine", "njk");
nunjucks.configure(path.resolve(__dirname, "./view/"), {
  autoescape: true,
  express: app,
});

// 关键的配置代码
// 用来定义返回类型
var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// 用来定义返回值的操作
var root = {
  hello: () => {
    return "get value";
  },
};

// 将从前端传来的grapyql请求通过graphql服务器进行解析
app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true,
  })
);

app.listen(3000, () => {
  console.log("server is work on 3000");
});

2. 前端请求配置
fetch("/graphql", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
    },
    body: JSON.stringify({ query: "{hello}" }),
})
    .then((r) => r.json())
    .then((data) => console.log(data));
// 这里我们会得到data -> get value

从上面的例子可以看到graphql配置中的几个关键点

  • scheme -> 用于定义返回值的类型
  • root -> 用于定义需要返回值返回的值得获取方法
  • graphqlHTTP一个支持graphql api的返回的服务器
3. 使用构建类型定义schema的方法

我们先来看下上面代码中对schema的配置方法

// 关键的配置代码
// 用来定义返回类型
var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// 用来定义返回值的操作
var root = {
  hello: () => {
    return "get value";
  },
};

在这里有几个让人别扭(没错就是我感觉难受)的点:

  • 我们通过模板字符串来配置schema,在书写的时候没有提示,很容易写错
  • 当我们的业务很复杂的时候,schema的字符串列表会很长,不易于维护和复用
  • 返回值得类型定义和返回值的业务逻辑分离,通过名字相同来进行匹配,感觉上不容易维护(当然也有人觉得分开维护更加清晰,仁者见仁智者见智吧~)

基于此,我更喜欢文档中的第二种,利用构建类型,操作和定义schema,废话不多说我们先来看代码

import { GraphQLObjectType, GraphQLString } from 'graphql'
const queryString = new GraphQLObjectType({
  name: "Query",
  fields: {
    hello: {
      type: GraphQLString,
      args: {},
      resolve: () => {
        return "Hello";
      },
    },
  },
});

在构建类型中,其实是将 schema定义 + root进行整合 方法如下:

  • 每一个fields对应了一个支持的graphql的查询方法
  • type用于定义该查询语句的返回类型
  • args用于定义查询语句获得的参数类型(后面会详细的演示)
  • resolve用于处理该查询语句获取参数的业务逻辑(一般来讲graphql其实应该更多负责获取数据,而不是处理太多奇怪的业务逻辑)

为了对比结果我们增加一个新的graphql查询服务器,放在/graphql2这个路径下

const schema2 = new GraphQLSchema({
  query: queryString,
});

app.use(
  "/graphql2",
  graphqlHTTP({
    schema: schema2,
  })
);

// 前端请求方法
fetch("/graphql2", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
    },
    body: JSON.stringify({ query: "{ hello }" }),
})
    .then((res) => res.json())
    .then((data) => console.log("new Hello = ", data));
4. 结果

从结果看到,两种方法其实都是可以获得对应的参数的,只看你喜欢哪种啦~

2. 复杂对象graphql返回方法

我们有时需要返回的不仅仅是一个简单的字符串或者数字,我们可能需要返回一个复杂对象,这个时候我们要怎么操作呢?我们继续看下面的例子

1. 服务端的配置
  • 首先,从模板字符串来看,我们来配置schema 和 root
  • 注意点,作为一个复杂类型(对象类型),我们可以定义一个Student来类来进行返回 (原因:因为我理解,其实最后我们通过graphql返回给前端是一个对象,比如我们返回 const res = { name: '张三', age: 18 }, 但是其实我们前端拿的时候,只想拿到name字段,其实这个时候,graphql http在处理的时候会直接拿res.name),我们通过定义一个Student类,更有利于代码的维护,
  • 另外,比如我们要返回一个Student对象,我们在返回值的时候,其实并不是返回string, int这么简单,我们需要返回一个Student的类,那么,我们在graphql的schema中定义returnType的时候就应该返回这个Student类型,因此我们需要在Schema中就定义这个类型
// 定义了一个学生类别
// 用于返回学生信息

// 用于在js中处理对应的取参逻辑
class Student {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

// 这个type Student 为了让graphql用于数据的校验
var schema = buildSchema(`
  type Student {
    name: String!
    age: Int!
  }

  type Query {
    hello: String
    studentInfo: Student
  }
`);

var root = {
  hello: () => {
    return "get value";
  },
  studentInfo: () => {
    return new Student("张三", 14);
  },
};

构建类型的定义方法

  • 这里我们依然给出构建类型的定义方法:
  1. 定义对应的Student的query类型
// 1. 定义一个Student的Query类型 -> scehma中的类型定义
const studentType = new GraphQLObjectType({
  name: "Student",
  fields: {
    name: {
      type: GraphQLString,
    },
    age: {
      type: GraphQLInt,
    },
  },
});
  1. 在schema中定义相应的fields(type 即为刚才定义的studentType)
const queryString = new GraphQLObjectType({
  name: "Query",
  fields: {
    studentInfo: {
      type: studentType,
      resolve: () => {
        return new Student("张三-新", 20);
      },
    },
  },
});
2. 前端调用的规则
  • 注意点:
    • 对于复杂对象的查询来说,因为返回的是一个复杂类型,因此我们需要告诉graphql,最终我们在复杂类型中需要取的字段名,因此在查询语句中写明最终需要返回参数的名称
// 定义查询语句 
let query3 = `query StudentInfo {
    studentInfo {
        name,
        age
    }
}`;

// 调用模板字符串定义的schema
fetch("/graphql", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
    },
    body: JSON.stringify({ query: query3 }),
})
    .then((res) => res.json())
    .then((data) => console.log(data));

// 调用构造类型的schema
fetch("/graphql2", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
    },
    body: JSON.stringify({ query: query3 }),
})
    .then((res) => res.json())
    .then((data) => console.log("get Student = ", data));
3. 结果

3. 传参的例子

有些场景下我们需要根据参数来获取对应的返回值,这个时候我们可以通过传参的方式来解决

例如,我们想获取一个学生列表中的学生,我们可以根据学生的学号进行匹配,获取对应学生的信息的场景

1. 服务端配置
  1. 为模板字符串添加配置

    我们在root中的param中可以获取前端传过来的查询参数

    // 模拟一个假的数据库
    const studentList = [
      new Student("张三", 14),
      new Student("李四", 14),
      new Student("王五", 20),
    ];
    
    // schema
    // 新增getStudentById
    var schema = buildSchema(`
      type Student {
        name: String!
        age: Int!
      }
    
      type Query {
        // .....省略之前的
        getStudentById(id: Int!): Student
      }
    `);
    
    // root配置
    var root = {
      // ....省略之前的
      // 新增root配置
      getStudentById: (params) => {
        // 所有传递的参数在params中可以拿到
        const stus = [new Student("张三", 14), new Student("李四", 14)];
        if (stus[params.id]) {
          return stus[params.id];
        } else {
          return null;
        }
      },
    };
    
    
  2. 为构建类型添加类型

    • 构建时通过args来定义resolve中查询参数的类型
    • 如果args是一个复杂类型的话,可以利用之前的先定义变量类型的方法,将类型赋值给对应args的type
    const queryString = new GraphQLObjectType({
      name: "Query",
      fields: {
        // 省略之前的。。。。
        getStudentById: {
          type: studentType,
          args: {
            id: { type: GraphQLInt },
          },
          resolve: (_, { id }) => {
            // 解构赋值拿到对应的参数进行操作
            return studentList[id];
          },
        },
      },
    });
    
2. 前端调用方法
  • 如果需要传参先要定义一个query方法,之后通过xxx定义对应形参的类型,使用xxx定义对应形参的类型,使用**是为了转义**
  • 之后需要调用对应的schema中的方法,然后如果返回是复杂类型,要最终写到需要返回的参数名
  • 需要将形参$xxx赋值给实际在graphql resolve解构参与的逻辑的实参xxx
  • 定义时的输入输出的类型要和传入传出的参数类型保持一致,不然会报错
  • 目前Graphql支持的标量类型:String,Int,Float,Boolean和ID
// 这里先通过一个id进行传递,然后将该id传给对应的在shema中配置的方法即可
// 这里推荐用$符号 完成自动转义
let query4 = `query GetStudentInfo($id: Int!) {
    getStudentById(id: $id) {
        name,
        age
    }
}`;

fetch("/graphql", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
    },
    body: JSON.stringify({ query: query4, variables: { id: 0 } }),
})
    .then((res) => res.json())
    .then((data) => console.log(data));

// 调用构建类型的代码
      fetch("/graphql2", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
        body: JSON.stringify({ query: query4, variables: { id: 1 } }),
      })
        .then((res) => res.json())
        .then((data) => console.log(data));
3. 结果

4. 修改请求发送

在常用的增删改查的过程中,刚才考虑了查,增删改 的过程我们还没有学习,graphql是通过Mutation关键字来发起修改数据库数据的请求的,让我们来看看怎么操作吧~其实都一样,只不过我们需要一个新的声明mutation

1. 服务端的配置
  • 模板字符串配置
    • 我们重新定义一个type为Mutation的方法,之后我们在前端请求带有mutation的会直接打到对应的方法上
    • 这里我们将mutation的处理函数与query的写在一起,通过与type中相同的方法名字来进行查询
    • 如果是返回一个复杂类型的列表,模板字符串中只需要写成 [Student]即可
// 创建一个新的学生~
// 为了便于查看操作的结果,我们将操作完成后的所有学生列表返回
// 定义一个新的mutation类型
var schema = buildSchema(`
  // 省略上面的内容
  type Query {
	// 省略query中的内容
  }

  type Mutation {
    addStudent(name: String!, age: Int!): [Student]
  }
`);

var root = {
  // 省略之前的query的方法
  addStudent: ({ name = "", age = -1 }) => {
    const newStu = new Student(name, age);
    studentList.push(newStu);
    return studentList;
  },
};

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true,
  })
);

  • 使用类型构建的方法
    • 从上面的方法可以看到其实这种混合的写法,混在一起其实后期要进行维护辨认也是比较难的,用类型构建就会好许多
    • 其实操作和之前的query并没有什么不一样
    • 复杂类型列表,在构建类型中我们需要调用 new GraphqlList来定义它是一个列表类型
// 定义一个mutationString
const mutationString = new GraphQLObjectType({
  name: "Mutation",
  fields: {
    addStudent: {
      type: new GraphQLList(studentType),
      args: {
        name: { type: GraphQLString },
        age: { type: GraphQLInt },
      },
      resolve: (_, param): Student[] => {
        console.log(123);
        const { name, age } = param;
        try {
          studentList.push(new Student(name, age));
        } catch (error) {}
        return studentList;
      },
    },
  },
});

// 在schema中直接将其作为mutation传入,后续前端请求mutation语句时直接打到对应的mutation中进行操作
const schema2 = new GraphQLSchema({
  query: queryString,
  mutation: mutationString,
});
2. 前端请求编写
  • 对于一个复杂类型的列表,我们在定义返回值的类型,我们只需要按照复杂类型中需要的值进行填写,他会将每个复杂类型中我们需要的参数取出组成一个列表进行返回
let _query1 = `
    mutation AddStudent($name: String!, $age: Int!) {
        addStudent(name: $name, age: $age) {
            name,
            age
    }
}
`;
fetch("/graphql", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
    },
    body: JSON.stringify({
        query: _query1,
        variables: { name: "王六", age: 19 },
    }),
})
    .then((res) => res.json())
    .then((data) => console.log("add student 1 = ", data));

fetch("/graphql2", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
    },
    body: JSON.stringify({
        query: _query1,
        variables: { name: "王七", age: 20 },
    }),
})
    .then((res) => res.json())
    .then((data) => console.log("add student 2 = ", data));
3. 结果

好了我们已经学会了 增删改查相应的功能~~~已经可以开心的用起来了,后面我会对node + orm + graphql的实践进行后续的探索