Two Ways to Script and Modularize GraphQL Schema

GraphQL Schema 的两种写法及模块化

上个礼拜学习了一下 GraphQL。我觉得入门的一个难点就是写 Schema,跟着 YouTube 教学视频和文档,学习了两种写法,一种是用 GraphQL 的 Schema 语法写,另一种是用 GraphQLSchema 构造器写。

Using GraphQL Schema Language

内置的 Schema 语法是写在模板字符串里的,由 buildSchema() 创建。

举个例子,有两个数据类型:UserPost,要求可以查询 User 和 Posts、可以创建 User 和 Post:

const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');

var rootSchema = buildSchema(`
  type User {
    id: String
    name: String
  }
  
  type Post {
    id: String
    title: String
    content: String
  }
  
  input PostInput {
    title: String,
    content: String
  }

  type RootQuery {
    user(id: String): User,
    posts: [Posts]
  }
  
  type RootMutation {
    createUser(name: String): User,
    createPost(newPost: PostInput): Post
  }
`);

const rootResolvers = {
  user: (id) => {
    return user;
  },
  posts: () => {
    return posts;
  },
  createUser: (name) => {
    return user;
  }
  createPost: (args) => {
    return post;
  }
};

const app = express();
app.use('/graphql', graphqlHTTP({
  schema: rootSchema,
  rootValue: rootResolvers,
  graphiql: true,
}));
app.listen(4000);

rootResolvers 中的 userposts 是查询的方法,方法名要和 schemaRootQuery 一样,前者是具体实现,后者类似于登记声明。createUsercreatePost 对数据进行了变更,所以要放在 RootMutation 中定义。对 createPost 的参数输入定义了一个新的类型 PostInput,也写在了 schema 中,用 input 定义,如果输入参数多,可用此法。

rootResolvers 中方法的具体内容省略了。

再看:

app.use('/graphql', graphqlHTTP({
  schema: rootSchema,
  rootValue: rootResolvers,
  graphiql: true,
}));

把定义好的 rootSchemarootResolvers 填进去,GraphQL 服务就可以使用了。

Modularize for Schema Language

上文例子简单明了,但并不适于一般正经项目。因为数据不可能这么少,难道各种定义和方法都写在一个文件里?必须模块化。

首先 rootSchemarootResolvers 要分成两个文件吧。然后继续细分。rootSchema 可分为 userSchemapostSchema 两种。以 postSchema 为例:

module.exports.postSchema = `
type Post {
  id: String
  title: String
  content: String
}

input PostInput {
  title: String
  content: String
}`;

module.exports.postQuery = `posts: [Post]`;
module.exports.postMutation = `createPost(newPost: PostInput): Post`

因为 schema 实际由 Type、Query、Mutation 三种定义构成,所以每个 schema 模块最好定义三块内容。

全部定义好,在 rootSchema 中整合:

const { userSchema, userQuery, userMutation } = require('./user.schema');
const { postSchema, postQuery, postMutation } = require('./post.schema');

const schemas = buildSchema(
  ``.concat(
    userSchema,
    postSchema,
    `
    type RootQuery {
      ${userQuery},
      ${postQuery}
    }

    type RootMutation {
      ${userMutation},
      ${postMutation}
    }

    schema {
      query: RootQuery
      mutation: RootMutation
    }
    `
  )
);

module.exports = schemas;

因为是字符串,所以可用 concat 来连接。一拼接就像样多了。

接着是 rootResolvers 的模块化。这个相对简单,就是定义好每种类型的 Query 和 Mutation 方法,然后整合起来:

const userResolver = require('./user.resolver');
const postResolver = require('./post.resolver');

const resolvers = {
  ...userResolver,
  ...postResolver
}

module.exports = resolvers;

最后,原来的写法就变成了:

const express = require('express');
const graphqlHTTP = require('express-graphql');
const rootSchema = require('./schemas/index');
const rootResolvers = require('./resolvers/index');

const app = express();
app.use('/graphql', graphqlHTTP({
   schema: rootSchema,
   rootValue: rootResolvers,
   graphiql: true,
}));

app.listen(4000);

Using GraphQLSchema Constructor

用 Schema 语法有时觉得还是太 low 了,能不能更面向对象一些,更 programmatically 一些?可以的,用 GraphQLSchema() 来构造 schema。

写在一个文件里是这样的:

const express = require('express');
const graphqlHTTP = require('express-graphql');
const graphql = require('graphql');

const UserType = new graphql.GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: graphql.GraphQLString },
    name: { type: graphql.GraphQLString },
  }
});

const PostType = new graphql.GraphQLObjectType({
  name: 'Post',
  fields: {
    id: { type: graphql.GraphQLString },
    title: { type: graphql.GraphQLString },
    content: { type: graphql.GraphQLString }
  }
});

const PostInput = new GraphQLInputObjectType({
  name: 'PostInput',
  fields: {
    title: { type: GraphQLString },
    content: { type: GraphQLString }
  }
});

const queryType = new graphql.GraphQLObjectType({
  name: 'Query',
  fields: {
    users: {
      type: new GraphQLList(UserType),
      args: null,
      resolve: (_, {id}) => {
        return user;
      }
    },
    createUser: {
      type: {
        name: { type: GraphQLString }
      },
      resolve: (_, args) => {
        return newUser;
      }
    },
    posts: {
      type: new GraphQLList(PostType),
      args: null,
      resolve: async () => {
        return posts;
      }
    },
    createPost: {
      type: PostType,
      args: {
        newPost: { type: PostInput }
      },
      resolve: (_, args) => {
        return newPost;
      }
    }
  }
});

var schema = new graphql.GraphQLSchema({query: queryType});

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');

注意它定义属性类型是用 { type: graphql.GraphQLString } 这种方式的,还有数组类型是 new GraphQLList(YourType)

Modularize for GraphQLSchema

接下来用模块化的方式重构。方式很简单,Type 放在一个文件夹里,每个 Model 一个文件,Query 也是按 Model 分,再用一个 Index 整合。

Type 的例子:

// post.type.js

const PostType = new GraphQLObjectType({
  name: 'Post',
  fields: () => ({
    id: { type: GraphQLString },
    title: { type: GraphQLString },
    content: { type: GraphQLString }
  })
});

module.exports.PostType = PostType;

输入参数类型可用 GraphQLInputObjectType() 定义:

const PostInput = new GraphQLInputObjectType({
  name: 'PostInput',
  fields: {
    title: { type: GraphQLString },
    content: { type: GraphQLString }
  }
});

module.exports.PostInput = PostInput;

Query 的例子(具体定义省略):

const { PostType, PostInput } = require('../types/post.query');

const postQueries = {
  posts: {
    type: new GraphQLList(PostType),
    args: null,
    resolve: async () => {
      return posts;
    }
  },
  createPost: {
    type: PostType,
    args: {
      newPost: { type: PostInput }
    },
    resolve: (_, args) => {
      return newPost;
    }
  }
}

module.exports.postQueries = postQueries;

整合 Query 如下所示:

// queries/index.js

const { GraphQLObjectType } = require('graphql');
const { userQueries } = require('../queries/user.query');
const { postQueries } = require('../queries/post.query');

const QueryType = new GraphQLObjectType({
  name: 'QueryType',
  fields: {
    ...userQueries,
    ...postQueries
  }
});

module.exports = QueryType;

然后再用来构造 Schema:

// schema.constructor.js

const { GraphQLSchema } = require('graphql');
const QueryType = require('./queries/index');

module.exports = new GraphQLSchema({
  query: QueryType
});

最后:

const express = require('express');
const graphqlHTTP = require('express-graphql');
const schemaConstructor = require('./schema.constructor');

const app = express();
app.use('/graphql', graphqlHTTP({
   schema: schemaConstructor
   graphiql: true,
}));

app.listen(4000);

注意此时已经不需要 rootValue 了。

To Be Continued

写了这么多,只是刚好用来查询而已,只能算入了个门。觉得 GraphQL 真的挺方便,我将继续深入学习和使用。

NEXTHow to Use React-Redux Basically