这篇文章介绍一下如何搭建一个简单的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调试工具
我们简单的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数据了
自此,我们实现了一个简单的graphql服务,将schema,resolve定义进行分离,实现了自定义类型和mock数据,可以写查询语句来验证我们定义的schema结构了,后续我们会继续完成resolver,自定义指令来实现鉴权,扩展请求上下文加入保存数据的功能,等我们的graphql服务都搭建完成,再来实现react与graphql的集成
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。