使用 express 构建简单 GraphQL 接口

本文介绍如何使用Express和GraphQL.js构建RESTful API替代方案,详细解释了GraphQL数据模型、查询接口模型的设计方法,并演示了如何使用Express提供GraphQL服务。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用 express 构建简单 GraphQL 接口

GQL

GQL(Graph Query Language) 是一种查询语言。用来设计出较 Restful api 更易于扩展和升级的接口,可以理解为 Restful api 的替代品。

GQL 服务可以开放在 Restful api 下,不过其逻辑并不依赖任何平台。

在后端,可以使用 GQL 直接描述数据模型,或使用 GraphQL.js 提供的其他接口来描述数据模型。

在前端,使用 GQL 直接描述我们需要的数据结构,然后就可以拿到不多不少、结构相似的数据。

例如我们请求,

{
  user {
    name
  }
}

返回,

{
  "data": {
    "user": {
        "name": "高厉害"
    }
  }
}

desc

使用GraphQL.js

GraphQL.js泛指使用 JavaScript 实现的 GraphQL 库,我们在 node.js 平台学习开发 GQL 接口可以称之为 —— 学习GraphQL.js

GraphQL.js的核心是一个解析器,用来解析 GQL 文本,实现“定义 GQL 数据模型”,和“增删改查数据”,本文仅描述 GQL 的查询接口。

初始化

初始化并安装依赖,

npm init -y
npm i express graphql

graphql中,我们暂时仅关心两个接口:graphqlbuildSchema,前者是 GQL 的解析器,后者用于构造一个GraphQLSchema类型的对象。

来看看解析器都需要什么参数,下面给出一个调用实例:

graphql.graphql(schema, query, root).then((gqlRes) => {
    console.log(gqlRes);
});
  • schema 是查询接口的模型(GraphQLSchema类型)

  • query 是 GQL 的查询文本,例如

    {
      user {
        name
      }
    }
    
  • root 是查询接口模型涉及到的每个字段的函数(GQL 对不同层次函数调用是广度优先的)

使用 graphql 开发查询接口只需要三步,第一步是描述数据模型和查询接口模型(对应 schema),第二步是针对每个字段提供函数(对应 root),第三步是将数据送往解析器并将结果返回给请求端。

数据模型

GraphQL.js里,查询接口模型的类型是GraphQLSchema,我们可以使用buildSchema来构造。

下面开始设计一个对用户开放的接口

假设我们在这个接口可能需要获得两个东西:

  • 根据 id 查询用户信息
  • 当前时间
字段类型
userUserType
nowstring

使用 GQL 描述为:

type Query {
    user(id: String!): UserType
    now: String
}

user 字段括号里是参数,通过 id 唯一确定一个用户,参数末尾的 ! 表示该字段必须提供。

Query 的位置是自定义类型名,但接口模型的类型固定为 Query。

同样地,UserType 也是一种自定义类型,考虑下面这个用户模型,id 为主键:

字段类型
idstring
usernamestring
ageint

使用 GQL 描述为:

type UserType {
    id: String
    username: String
    age: Int
}

调用buildSchema,将返回一个GraphQLSchema对象,实现如下:

const schema = graphql.buildSchema(`
    type UserType {
        id: String
        username: String
        age: Int
    }
    type Query {
        user(id: String!): UserType
    }
`);

这样一来我们就搞定了解析器的第一个参数 schema。

字段函数

我们有了数据模型,下面就要确定数据来源,这里简单手动提供一些数据和接口:

'./db.js'

const data = {
    '1001': { username: '高厉害', age: 21 },
    '1002': { username: '列队猫', age: 90 },
    '1003': { username: '小明', age: 15 },
    '1004': { username: '小红', age: 16 },
}
module.exports = {
    findById(id) {
        if (id in data) {
            return data[id];
        }
        return null;
    }
}

然后提供 root 参数,root 是一个对象,描述了 Query 即接口模型各个字段的来源:

let root = {
    user: (args, context, info) => {
        return db.findById(args.id);
    },
    now: (args, context, info) => {
        return new Date().toLocaleString();
    }
};

对象深层的字段也可以特别指定:

但这样做似乎就无法为外层的 user 提供函数了,所以传入解析器的 root 参数的功能非常局限,一般仅提供根字段的函数。

此外,若不提供深层字段的函数,则默认提供外层对象的对应值,就像上面那个例子那样。

let root = {
    user: {
        username: (args, context, info) => {
            return ...;
        },
        age: (args, context, info) => {
            return ...;
        },
    },

    now: (args, context, info) => {
        return new Date().toLocaleString();
    },

};

提供服务

参数都准备好了。

写一个查询:

let query = `
{
    user(id: "1001"){
        age
    }
    now
}
`;

调用解析器:

graphql.graphql(schema, query, root).then((result) => {
    console.log(result);
});

输出:

{
    "data": {
        "user": {
            "age": 21
        },
        "now": "2021-1-25 16:19:03"
    }
}

下面引入express,在某个路由提供 GQL 服务:

const graphql = require('graphql');
const express = require('express');
const db = require('./db.js');

const app = express();

// 数据模型和查询接口模型
let schema = graphql.buildSchema(`
    type UserType {
        id: String
        username: String
        age: Int
    }
    type Query {
        user(id: String!): UserType
        now: String
        
    }
`);


// 所有字段的解析方法
let root = {
    user: (args, context, info) => {
        return db.findById(args.id);
    },
    now: (args, context, info) => {
        return new Date().toLocaleString();
    }
};


// 路由
app.use('/graphqlAPI', (req, res) => {
    let reqJson = '';
    req.on('data', (data) => reqJson += data);
    req.on('end', () => {
        reqJson = JSON.parse(reqJson);
        graphql.graphql(schema, reqJson.query, root).then((result) => {
            res.send(result);
        });
    })
});

app.listen(80, () => { console.log('listen on 80.'); });

创建模型的推荐方法

刚才我们已经完成了一个简单的 GQL 接口,

  • 通过 GQL 文本描述了数据模型和接口模型
  • 提供了根字段的函数
  • 对外提供 GQL 服务

在给定字段的函数时,我们无法对每个字段精确控制,root 参数仅允许对根字段(或深层的根字段)提供函数。

而文档中给出的字段的函数原型是这样的:

他有四个参数,而我们在 root 参数中提供的函数仅有三个参数

// See below about resolver functions.
type GraphQLFieldResolveFn = (
  source?: any,
  args?: {[argName: string]: any},
  context?: any,
  info?: GraphQLResolveInfo
) => any

缺失的 source 参数是让接口模型的开发更加灵活的关键,该参数是当前字段外层对象的查询结果,例如 username 字段函数的 source 参数指代的是 user 的查询结果:

user {
    id,
    username,
    age,
}

这样,我们可以在外层查询结束后(广度优先的),对内层进行更加精确的控制。

这种转变需要改动前两步,描述接口模型和提供字段函数,或者说,这两步在下面要介绍的推荐方法中是耦合的。

接下来我们需要关注两个类型,GraphQLSchemaGraphQLObjectType,前者是我们熟悉的 schema,而后者则是模型类型,虽然是一个 js 对象,但却用来描述一个 GQL 类型,例如 UserType(很有趣,因为 GQL.js 是 JavaScript 平台下的一个抽象,所以出现了从 js 类型中构造出另一个抽象类型的情况)。

GraphQLSchema对象用来直接提供给解析器,其构造接受一个 option,其中包含模型类型 query,我们现在仅关注 query 即可,其他的是用来实现增删改数据等操作的。

const schema = new GraphQLSchema({
    query: new GraphQLObjectType({...}),
});

现在需要从 GraphQL 提供的 DDL 转化为对 GraphQLObjectType 对象的实例化:

type UserType {
    id: String
    username: String
    age: Int
}

等价于:

下面的代码中,每个字段都拥有一个 resolve 函数,他就是当前字段的(解析)函数。

注意,下面这些 resolve 都是默认实现。

const UserType = new graphql.GraphQLObjectType({
    name: 'UserType',
    fields: {
        id: {
            type: graphql.GraphQLString,
            resolve: (source, args, context, info) => {
                return source.id;
            },
        },
        username: {
            type: graphql.GraphQLString,
            resolve: (source, args, context, info) => {
                return source.username;
            }
        },
        age: {
            type: graphql.GraphQLInt,
            resolve: (source, args, context, info) => {
                return source.age;
            }
        },
    }
});

这就是一个 GQL 的模型类型,可以直接填充到GraphQLSchema对象的实例化操作中:

const schema = new GraphQLSchema({
    query: UserType,
});

此时的 schema 等价于:

type Query {
    id: String
    username: String
    age: Int
}

现在我们利用 UserType 来实现以下 DLL:

type UserType {
    id: String
    username: String
    age: Int
}
type Query {
    user(id: String!): UserType
}

上述代码等价于:

注意看下面代码是如何定义参数的

const schema = new graphql.GraphQLSchema({
    query: new graphql.GraphQLObjectType({
        name: 'queryType',
        fields: {
            user: {
                type: UserType,
                args: {
                    id: {
                        type: graphql.GraphQLString,
                        defaultValue: '1001'
                    },
                },
                resolve: (source, args, context, info) => {
                    console.log(source, args, context, info);
                    return db.findById(args.id);
                }
            },
            now: {
                type: graphql.GraphQLString,
                resolve: (source, args, context, info) => {
                    return new Date().toLocaleString();
                }
            }
        }
    }),
});

通过这种方法,我们将前两步合到了一起,且提供了更灵活的解析函数结构。

完整代码:

虽然形式上更加复杂了,但功能更加强大。

另外,注意,我没有为解析器提供 root 参数,因为没有必要,解析函数的结构已经体现在了接口模型的创建过程中,当然我们可以提供 root,不过 GQL.js 不会优先使用它。

const graphql = require('graphql');
const express = require('express');
const db = require('./db.js');

const app = express();

// 数据模型和查询接口模型
let UserType = new graphql.GraphQLObjectType({
    name: 'UserType',
    fields: {
        id: {
            type: graphql.GraphQLString,
            resolve: (source, args, context, info) => {
                return source.id;
            },
        },
        username: {
            type: graphql.GraphQLString,
            resolve: (source, args, context, info) => {
                return source.username;
            }
        },
        age: {
            type: graphql.GraphQLInt,
            resolve: (source, args, context, info) => {
                return source.age;
            }
        },
    }
});

let schema = new graphql.GraphQLSchema({
    query: new graphql.GraphQLObjectType({
        name: 'queryType',
        fields: {
            user: {
                type: UserType,
                args: {
                    id: {
                        type: graphql.GraphQLString,
                        defaultValue: '1001'
                    },
                },
                resolve: (source, args, context, info) => {
                    console.log(source, args, context, info);
                    return db.findById(args.id);
                }
            },
            now: {
                type: graphql.GraphQLString,
                resolve: (source, args, context, info) => {

                    return new Date().toLocaleString();
                }
            }
        }
    }),
});


// 路由
app.use('/graphqlAPI', (req, res) => {
    let reqJson = '';
    req.on('data', (data) => reqJson += data);
    req.on('end', () => {
        reqJson = JSON.parse(reqJson);
        graphql.graphql(schema, reqJson.query).then((result) => {
            res.send(result);
        });
    })
});

app.listen(80, () => { console.log('listen on 80.'); });

GraphQLObjectType的 option 结构

完整结构见 https://graphql.org/graphql-js/type/#graphqlobjecttype

new GraphQLObjectType({
    name: 'string',
    fields: {
        fieldName: {
            type: GraphQLOutputType,
            args: {
                argName: {
                    type: GraphQLInputType,
                    defaultValue: any,
                    description: 'string',
                },
                ...
            },
            resolve: (source, args, context, info) => any,
            deprecationReason: 'string',
            description: 'string'
        },
        ...
    },
});

使用express-graphql

我们是如何让 GQL 与 express 交互的?

从 express 的一个路由拿到数据,然后交给 graphql 解析并查询,最后返回解析的结果。

接下来我们使用express-graphql来完成这个中间操作,express-graphqlexpressgraphql提供了一个薄薄的中间层,这个中间层以graphqlHTTP中间件形式实现。

安装

npm i express-graphql

express-graphql解构出中间件graphqlHTTP,然后开放在某个路由,

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

const app = express();

// 描述接口模型
const schema = ...;

// 路由
app.use('/graphqlAPI', graphqlMiddleware({
    // 接口模型
    schema, 
    
    // 传递给 graphql 函数
    rootValue,
    context,
    
    // 将 web 调试应用开放在该路由,配置为 true 后访浏览器问该路由即可
    graphiql: true,
}));

app.listen(80, () => { console.log('listen on 80.'); });

context

刚才我们一直忽略了一个参数 context,他是 graphql 解析器的一个实参,也是字段解析函数的一个形参,用于不同解析函数间的通信,或是解析器过程需要使用到的一些参数,可以通过 context 从外部向解析器传入。

例如,想要在每个字段解析函数中拿到 req, res,我们可以向外包一层 lambda 用来接收参数,随后使用参数创建一个新的中间件并触发他:

app.use('/graphqlAPI', (req, res) => graphqlMiddleware({
    schema: schema,
    context: { req, res },
    graphiql: true,
})(req, res));

variables

一般一次 GQL 查询总包含两个东西,一个是 queryString,一个是 variables,我们刚才一直忽略了后者,

无伤大雅,我们只需要在前两个实现中接收并传入解析器即可,

解析器的完整参数:

graphql(
  schema: GraphQLSchema,
  requestString: string,
  rootValue?: ?any,
  contextValue?: ?any,
  variableValues?: ?{[key: string]: any},
  operationName?: ?string
): Promise<GraphQLResult>
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高厉害

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值