9

这篇文章介绍一下如何搭建一个简单的graphql服务,graphql的特性以及如何在react中使用graphql,graphql给项目开发所带来的益处和它所适应的场景不多说了,可以看下infoq上的这篇文章了解下,这里主要介绍下如何搭建和使用。
首先初始化项目react-graphql

mkdir react-graphql
cd react-graphql
npm init

安装基于koa的后端依赖

npm install koa koa-bodyparser koa-router apollo-server-koa graphql graphql-tools

说一下各个依赖包的用处

  • koa 基于nodejs使用中间件模型的网络服务框架
  • koa-bodyparser 解析请求体数据,form,json等格式数据
  • koa-router 用于配置路由规则,将不同路由请求映射到对应的处理逻辑
  • apollo-server-koa apollo是graphql技术发展及各种解决方案的项目,apollo-server-koa 提供了graphql开发调试的图形化工具,并结合koa对请求路由地址和请求数据进行解析
  • graphql graphql的核心,如何处理类型定义和resolve的执行方式就是由它处理的
  • graphql-tools 用于解析schema定义

graphql-tools这里再多说一下,便于读者理解,如果简单地使用graphql去定义类型和resolve函数也是可行的,它的方式如下

import {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString
} from 'graphql';

new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQuery',       // RootQuery 根节点查询类型
    fields: {                    // 查询包含的字段
      hello: {                   // RootQueryType 查询会返回一个 hello 字段
        type: GraphQLString,     // hello 字段是一个String类型
        resolve() {              // RootQueryType 查询的解析器函数
          return 'world';        // 函数返回值为 world 字符串
        }
      }
    }
  })
});

这里就能看出其类型定义方式比较繁琐,并且和代码耦合度较高,graphql-tools 的意义就在于它会解析一种结构定义语言schema,将其解析为上述方式,这样可以将定义文件分离开来,对组织一个项目大有用处,看看用graphql-tools我们来怎么写

// scheme定义可以写在独立文件中
const schema = `
  schema {
    query: RootQuery
  }

  type RootQuery {
    hello: String
  }
`;

makeExecutableSchema({
  typeDefs: [
    schema,
  ],
  resolvers: {
    hello() {
        return 'world'
    }
  },
});

现在开始我们的graphql服务搭建,在项目目录中,创建server目录,并创建index.js文件,引入依赖

// server/index.js

const Koa = require('koa');
const { ApolloServer, gql } = require('apollo-server-koa');

const typeDefs = gql`
    schema {
        query: Query
    }
    
    type Query {
        hello: String
    }
`; // 定义类型

const resolvers = {
    Query: {
        hello() {
            return 'world'
        }
    }
}; // resolve 定义

const app = new Koa();

const server = new ApolloServer({
    typeDefs,
    resolvers,
    playground: true, // 开启开发UI调试工具
});

server.applyMiddleware({ app });

const port = 8000;
app.listen({ port }, () => console.log(`graphql server start at http://localhost:${port}${server.graphqlPath}`));
// 开启服务,server.graphqlPath 默认为 /graphql

package.json中添加启动命令

 "scripts": {
   "start": "node server/index.js"
 }

启动服务

npm start

访问服务http://localhost:8000/graphql 可以看到如下UI界面,这是graphql的UI调试工具
Playground---http-localhost-8000-graphql
我们简单的graphql服务就搭建好了,在实际开发中schema和resolve会越来越多并且要进行模块分割,现在我们将schema和resolve提取出来
在server目录下创建schema和reslover文件,并添加index.js文件

// schema/index.js

const fs = require('fs');
const path = require('path');
const { gql } = require('apollo-server-koa');

// 包含所有的类型定义
const typeDefs = [];

// 同步读取当前目录下所有 .graphql 文件
const dirs = fs.readdirSync(__dirname);

dirs.forEach((dir) => {
    const filePath = path.join(__dirname, dir);
    if (
        fs.statSync(filePath).isFile && 
        filePath.endsWith('.graphql') // 读取.graphql文件
    ) {

        const content = fs.readFileSync(
            filePath, 
            { 
                encoding: 'utf-8' 
            }
        );

        typeDefs.push(gql`${content}`); // gql字符串模板标签函数会解析schame定义语法
    }

});

// 导出类型定义
module.exports = typeDefs;
// resolver/index.js

const fs = require('fs');
const path = require('path');
const _ = require('lodash'); // npm install lodash 添加依赖

// 包含所有的resolve
const resolvers = {};

// 同步读取当前目录下所有 .js 文件
const dirs = fs.readdirSync(__dirname);

dirs.forEach((dir) => {
    const filePath = path.join(__dirname, dir);
    if (
        fs.statSync(filePath).isFile &&
        filePath.endsWith('.js') &&
        !filePath.endsWith('index.js') // 不包含此文件
    ) {

        const resolver = require(filePath);

        _.merge(resolvers, resolver); // 合并所有的resolver到reslovers中
    }

});

// 导出resolvers
module.exports = resolvers;

然后修改server/index.js引入schema和relover

// server/index.js

const Koa = require('koa');
const { ApolloServer, gql } = require('apollo-server-koa');

// 加载所有的 schema
const typeDefs = require('./schema');

// 加载所有的 resolver
const resolvers = require('./resolver');

const app = new Koa();

const server = new ApolloServer({
    typeDefs,
    resolvers,
    playground: true, // 开启开发UI调试工具
});

server.applyMiddleware({ app });

const port = 8000;
app.listen({ port }, () => console.log(`graphql server start at http://localhost:${port}${server.graphqlPath}`));
// 开启服务,server.graphqlPath 默认为 /graphql

然后我们以用户发帖为例子,定义schema和reslove,并介绍graphql中的一些特性,如结构定义,常用类型,枚举,自定义类型,定义指令,mock数据,自定义请求上下文等是如何使用的
首先我们先实现创建用户,发帖,查看所有帖子这几个功能
在server/schema中创建文件schame.graphql文件,vscode有对应.graphql文件的语法提示插件

# server/schema/schema.graphql

# #号用于写注释

schema { # schema关键字用于定义schema入口
    query: Query # query是规定的入口字段,这里表示所有的查询都是查询 Query 类型下的字段
}

type Query { # type 关键字用于定义结构
    node: Node # 因为我们要将Post和User分离出去,由于结构定义不允许空结构,Node在这里只是作为占位
}

type Node {
    id: ID # ID 类型,ID类型代表唯一性,值可以是数字或字符串
}

再创建user.graphql和post.graphql

# server/schema/user.graphql

type User {             # 定义User结构
    id: ID              # ID 类型,原始类型
    name: String        # String 字符串类型,原始类型
    age: Int            # Int 整数类型,原始类型
    available: Boolean  # Boolean 类型,值为 true/false,原始类型
    money: Float        # Float 浮点类型,原始类型
    gender: Gender      # Gender 性别枚举类型
    birthday: Date      # Date 自定义日期类型
}                       # 标量类型可参见 http://spec.graphql.cn/#sec-Scalars-

enum Gender {           # enum 用于定义枚举类型
    FEMALE              # 枚举值,若没有对应的resolve值,将会是字符串
    MALE
}

extend type Query {     # extend 扩展 Query 结构,相当于 type Query { user(id: ID!): User }
    user(id: ID!): User # user表示查询字段,id: ID! 表示查询时的输入值,!表示必须由此输入值,返回 User结构的数据,可以将这种写法当作函数来理解,user函数名,id 输入值,User 返回类型
}
# server/schema/post.graphql

type Post {
    id: ID!
    title: String
    content: String
    userId: ID
    user: User
}

extend type Query {
    post(id: ID!): Post
}

注意要想查询出自定义结构,就必须将自定义结构与根节点Query结构关联起来,graphql是一层套一层的方式,最终组织成一个大的结构,例如我们现在应用的结构是这样子的

# 伪代码
schema {
    query: {
        user: User {
            ...
        }
        post: Post {
            ...
            User {
                ...
            }
        }
    }
}

到这里,如果直接运行服务,会报Date类型未找到的错误,因此我们要手动实现自定义类型Date
首先在schema.graphql最后一行声明自定义类型

# server/schema/schema.graphql

# #号用于写注释

schema { # schema关键字用于定义schema入口
    query: Query # query是规定的入口字段,这里表示所有的查询都是查询 Query 类型下的字段
}

type Query { # type 关键字用于定义结构
    node: Node # 因为我们要将Post和User分离出去,由于结构定义不允许空结构,Node在这里只是作为占位
}

type Node {
    id: ID # ID 类型,ID类型代表唯一性,值可以是数字或字符串
}

scalar Date # 自定义Date类型

创建server/scalar.js文件,并添加如下内容

// server/scalar.js

const { GraphQLScalarType } = require('graphql');

const resolvers = {
    Date: new GraphQLScalarType({
        name: 'Date',
        description: '日期类型',
        parseValue(value) {
            // 这个函数用于转换客户端传过来的json值
            // json值是类似这样传过来的 { query: "user(date: $date): User", variables: { $date: "2017-07-30" }}
            return new Date(value);
        },
        parseLiteral(ast) { 
            // 这个函数用于转换客户端传过来的字面量值
            // 字面量值是类似这样传过来的 { query: "user(date: '2017-07-30'): User" }
            return new Date(ast.value);
        },
        serialize(value) {
            // 发送给客户端时将Date类型的值转换成可序列化的字符串值
            return value.toISOString();
        },
    }),
};

module.exports = resolvers;

最后一步,加载我们的自定义类型

// server/index.js

...
// 加载所有的 resolver
const resolvers = require('./resolver');

// 加载自定义类型
// 自定义类型,其本质也是个resolve
const scalar = require('./scalar.js');
// 将自定义类型合并到resolver中
_.merge(resolvers, scalar);

const app = new Koa();
...

现在我们的自定义Date类型有了,可以运行服务,但此时还没有定义resolve,因此查询将不会获取到数据,在resolve未实现之前,我们可以先用mock数据来验证我们的查询,apollo项目提供了强大又易用的mock集成,原始类型都已经实现了mock,因此我们只需要实现自定义类型Date的mock,创建server/mock.js文件并添加如下内容

// server/mock.js

module.exports = {
    Date() {
        return new Date()
    }
}

将mock引入到index.js文件中

// server/index.js

...
_.merge(resolvers, scalar);

// mock 数据
const mocks = require('./mock');

const app = new Koa();

const server = new ApolloServer({
    typeDefs,
    resolvers,
    mocks,            // 配置mock数据
    playground: true, // 开启开发UI调试工具
});

server.applyMiddleware({ app });
...

现在我们可以启动服务,并能查询出mock数据了
Screenshot_2018-09-09-Playground---http-localhost-8000-graphql
自此,我们实现了一个简单的graphql服务,将schema,resolve定义进行分离,实现了自定义类型和mock数据,可以写查询语句来验证我们定义的schema结构了,后续我们会继续完成resolver,自定义指令来实现鉴权,扩展请求上下文加入保存数据的功能,等我们的graphql服务都搭建完成,再来实现react与graphql的集成


一番
86 声望2 粉丝

引用和评论

0 条评论