GraphQL的全栈指南——NodeJs服务器
在Facebook于2015年将GraphQL开源后,许多产品都采用了该技术。我们Dockup公司最近将我们的API改为GraphQL服务,而这正是我的工作。
我以为为我们的应用实现一个完整的GraphQL服务一定会很复杂,但令人惊讶的是,这很容易,也很有趣。
考虑到如何以同样的兴趣和乐趣传达我所学到的东西,我得出结论,通过建立一个应用程序来学习将是很好的。所以,是的!我们将建立一个小的应用程序,供餐馆添加、查看和删除菜单项目,让我们深入了解不同的GraphQL概念。
我不可能在一篇文章中完成所有这些,所以会有一系列的内容:
- 介绍:你可以通过我的另一篇文章了解一些介绍(可选)。
- 用NodeJs作为服务器实现
- 使用Absinthe GraphQL和Phoenix框架在Elixir中实现。
- 连接到GraphQL的React前端应用
设置
让我们从设置项目目录布局开始:
$ nvm install 12.16.1
$ nvm use 12.16.1
# make a project dir tree and cd into server dir
$ mkdir menucard && cd menucard && mkdir nodeserver elixirserver client && cd nodeserver
# init the project
$ yarn init
添加我们需要的包:
$ yarn add graphql apollo-server nodemon
$ touch index.js
将此添加到你的package.json中,以便nodemon ,与变化同步:
"scripts": {
"start": "node index.js""
"start:dev" : "nodemon"
}
并在你的终端运行yarn start:dev 。
我们可以使用apollo-server 包,它是对graphql 包的一个抽象包装。
我们现在先用一个Array 作为数据源,以后再连接数据库。
查询
在GraphQL中,你可以把Query 作为一组用户定义的函数,可以做REST中的GET 。
让我们假设我们想获得这些数据:
const menuItems = [
{
id: 1,
name: "Pizza",
category: "Meal",
price: "4.5"
},
{
id: 2,
name: "Burger",
category: "Meal",
price: "3.2"
},
]
让我写一个查询的基本index.js 设置,并解释一下:
// index.js
const { ApolloServer, gql } = require("apollo-server");
const menuItems = [
{
id: 1,
name: "Pizza",
category: "Meal",
price: "4.5"
},
{
id: 2,
name: "Burger",
category: "Meal",
price: "3.2"
},
]
const typeDefs = gql`
""""
Menu item represents a single menu item with a set of data
"""
type MenuItem {
id: ID!
name: String!
category: String!
price: Float!
}
type Query {
"Get menu Items"
menuItems: [MenuItem]
}
`
const resolvers = {
Query: {
menuItems: () => menuItems
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
好的,发生了什么?
首先,我们正在获得我们需要的服务,ApolloServer 和gql :
- 我们实例化ApolloServer,让服务器运行并与之互动
gql解析GraphQL字符串,JavaScript内部并不支持。
TypeDefs
const typeDefs = gql`
""""
Menu item represents a single menu item with a set of data
"""
type MenuItem {
id: ID!
name: String!
category: String!
price: Float!
}
type Query {
"Get menu Items"
menuItems: [MenuItem]
}
`
typeDefs是一个GraphQL模式。
GraphQL模式是用它自己的类型化语言编写的,模式定义语言(SDL)
我们在Query 类型中定义客户端可以做的查询*(这里是menuItems*),在Mutation 类型中定义客户端可以做的突变*(我们以后会看到)*。
一个type ,不过是一个包含字段的对象,其中键是Field names ,值是字段的data-type 。在GraphQL中,每个字段都应该是类型的。
而你在字段上方看到的字符串是文档字符串。你可以在graphiql 界面上看到文档,我们稍后会讲到。
在GraphQL中,数据类型是:
- 标量类型。
- Int
- 字符串
- Float
- 布尔型
- ID:代表唯一的数字,就像数据库中的id。
- 对象类型
- 类型:在上面的例子中,我们将MenuItem定义为一个类型,并将其作为查询类型中menuItems字段的数据类型。
类型可以表示为:
- [typeName]:应该返回具有该类型的数据数组
typeName - typeName!: typeName中的
!代表nonNullable,即返回字段不应该是null。 - 你也可以结合这些表示方法,如**[typeName!]!**即,你应该返回一个非空数组,其中的非空元素与
typeName类型相符。
解散器
const resolvers = {
Query: {
menuItems: () => menuItems
}
}
解析器地图将模式字段和types 与它必须调用的函数联系起来,通过从你的任何来源获取,将GraphQL操作变成数据。现在,它对我们来说只是一个数组。
让我们在menuItem :reviews ,添加另一个字段,并说明其类型:
const typeDefs = gql`
type Review {
id: ID!
comment: String!
authorId: ID!
}
""""
Menu item represents a single menu item with a set of data
"""
type MenuItem {
id: ID!
name: String!
category: String!
price: Float!
reviews: [Review]
}
type Query {
"Get menu Items"
menuItems: [MenuItem]
}
`
让我们假设有一些数组reviews ,其中有menuItem Id作为外键,那么解析器将是。
const resolvers = {
Query: {
menuItems: () => menuItem,
},
MenuItem: {
reviews: (parent, args, context) => {
return reviews.filter(item => item.menuItemId == parent.id)
}
}
}
查询
query {
menuItems {
reviews {
comment
}
}
}
将首先调用Query.menuItems ,然后把它的返回值作为parent 传递给MenuItem.reviews 。结果将是。
{
data: {
menuItems: [{
reviews: [{
comment: "Some comment"
}]
}]
}
}
解析器可以返回一个Object 或Promise 或scalar 的值,这也应该符合模式中为该字段定义的数据类型。如果返回一个承诺,解析的数据将被发送。
每个解析器函数被调用时都需要四个参数:
-
parent:对象包含来自父解析器的返回值。每个GraphQL查询都是服务器中的一个函数调用树。所以,每个字段的解析器都会得到父解析器的结果,在这种情况下。
- queryakarootQuery是最高级别的Parents之一。
- Query.menuItem中的Parent将是服务器配置传递给rootQuery的任何内容。
- MainItem.reviews中的Parent将是来自解析器Query.MenuItems的返回值。
- Review.id、Review.comment和Review.authorId中的父级将是*MenuItem.*review的解析值。
-
params:对象包含我们在查询中传递的参数,如query { menuItem(id: 12) { name } },参数将是{ id: 12 } -
context:你可以在实例化服务器时传递一个对象并在每个解析器上访问它。例子:
const server = new ApolloServer({ typeDefs, resolvers, context: { menuRefInContext: MenuItem } });Query: { menuItems: (parent, __, { menuRefInContext }) => menuTableInContext.findAll(), }, -
info:这个参数主要包含关于你的模式和当前执行状态的信息。
默认解析器
每个类型都不需要有一个解析器,ApolloServer 提供了一个默认的解析器,它在父对象中寻找相关的字段名,或者如果我们为字段明确定义了一个函数,则调用这个函数。
对于下面的模式,如果reviews 解析器的结果返回一个已经包含comment 字段的对象的列表,那么Review 的comment 字段就不需要一个解析器:
type Review {
comment: String!
authorId: ID!
}
type MenuItem {
reviews: [Review]
}
启动服务器
用typeDefs 和resolvers 实例化ApolloServer ,以监听查询:
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
进行查询
进入localhost:4000 ,在graphiql 的左边输入查询:一个由apollo-server提供的测试界面,在右边,你可以访问你拥有的不同类型的docs 和字段信息:
# This is the root operation: graphql provides three operations
#
# query
# mutation
# subscription
query {
# endpoint with what are the values we need, here we are asking
# for "name, price and, comment and authorId of reviews of all the menuItems"
menuItems {
name
price
reviews {
comment
authorId
}
}
}
结果将密切配合查询,只返回我们要求的内容:
// result
{
"data": {
"menuItems": [
{
"name": "Pizza",
"price": 4.5,
"reviews" : [
{
comment: "Not bad",
authorId: 12,
}
]
},
{
"name": "Burger",
"price": 3.2,
"reviews" : [
{
comment: "Good",
authorId: 90,
}
]
}
]
}
}
一个查询就像一棵获取数据的函数树。比如说。如果我们采用上面的查询。
你可以把字段想象成函数的管道,它可以返回一个数据或调用另一个函数。数据类型为Int,String,Boolean,Float 和ID 的字段返回数据,但type 字段将调用一个函数,取决于其字段的数据类型,返回数据或调用函数将发生:
| - rootQuery()
| - menuItems()
| - return name
| - return price
| - reviews()
| - return comment
| - return authurId
数据库设置
我们将使用一个叫做sequelize的包来使用Postgres数据库和MongoDB一样的函数。它也可以与任何其他数据库一起工作。
设置一个数据库并获得URL,将其作为参数传递给Sequelize构造函数。
停止应用程序,在终端运行以下命令:
$ yarn add pg pg-hstore sequelize && yarn start:dev
我们将使用sequelize连接到数据库,建立一个表,然后与该表交互。
"GraphQL不与任何数据库绑定,不与数据库进行交互,我们必须做所有的查询并返回GraphQL查询所期望的数据。"
我们的Postgres数据库托管在Heroku上。
我们将改变现有的代码以使用sequelize:
// index.js
const { ApolloServer, gql } = require("apollo-server");
const { Sequelize, DataTypes } = require("sequelize");
// connect to database
const sequelize = new Sequelize(
"PASTE YOUR POSTGRES URL HERE"
);
// Expecting a table name "menuItems" with fields name, price and category,
// You'll use "MenuItem" to interact with the table. id, createdAt and
// updatedAt fields will be added automatically
const MenuItem = sequelize.define("menuItems", {
name: {
type: DataTypes.STRING
},
price: {
type: DataTypes.FLOAT
},
category: {
type: DataTypes.STRING
}
});
const Review = sequelize.define("reviews", {
comment: {
type: DataTypes.String
},
authorId: {
type: DataTypes.INTEGER
}
});
MenuItem.hasMany(Review , { foreignKey: "menuItemId", constraints: false })
// `sync()` method will create/modify the table if needed, comment it when not
// needed, uncomment whenever you change the model definition.
// For production you might consider Migration (https://sequelize.org/v5/manual/migrations.html)
// instead of calling sync() in your code.
// MenuItem.sync();
const typeDefs = gql`
type Review {
id: ID!
comment: String!
authorId: ID!
}
""""
Menu item represents a single menu item with a set of data
"""
type MenuItem {
id: ID!
name: String!
category: String!
price: Float!
reviews: [Review]
}
type Query {
"Get menu Items"
menuItems: [MenuItem]
}
`
// Note: We removed the separate resolver for reviews because
// menuItems itself returned reviews for each MenuItem
const resolvers = {
Query: {
menuItems: (parent, __, { menuItem }) => {
return menuItem.findAll({
include: [{ model: Review }]
})
},
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
在graphiql中运行查询
query {
menuItems {
name
price
reviews {
comment
}
}
}
你应该得到一个空数组,因为我们还没有在数据库中创建任何菜单项,我们将使用突变来做到这一点
突变
你可以把Mutations 作为用户定义的函数,它可以做REST中的POST,PUT,PATCH 和DELETE 的工作
将TypeDefs改为:
// index.js
const typeDefs = gql`
type Review {
id: ID!
comment: String!
authorId: ID!
}
""""
Menu item represents a single menu item with a set of data
"""
type MenuItem {
id: ID!
name: String!
category: String!
price: Float!
reviews: [Review]
}
input MenuItemInput {
name: String!
category: String
price: Float
}
input ReviewInput {
comment: String!
authorId: ID!
menuItemId: ID!
}
type Query {
"Get menu Items"
menuItems: [MenuItem]
}
type Mutation {
addMenuItem(menuItem: MenuItemInput): MenuItem
addReview(review: ReviewInput): Review
}
`
在这里,我们引入了两个新字段:Mutation 和input
变异字段
像Query ,Mutation 也是一个特殊的字段,我们将在这里定义所有的突变(Mutation 类型里面的字段),我们也将为我们的突变编写解析函数。
输入对象
输入字段也和type 字段一样,但这定义了客户端查询要传递的参数的类型,如果它是一个对象的话。
例如,要创建一个menuItem的查询:
# Operation we are doing
mutation {
addMenuItem(menuItem: { name: "Pizza", category: "Meal", price: 10.3 }) {
name
price
category
}
}
看,我们正在传递一个对象menuItem ,作为addMenuItem 突变的参数,像一个函数。这个menuItem 应该与我们定义的input MenuItemInput 类型相匹配。
序列化(Resolver)函数
// index.js
const resolvers = {
Query: {
...
},
Mutation: {
addMenuItem: (
_,
{ menuItem: { name, price, category } },
__
) => {
return MenuItem.create({
name,
price,
category
});
},
addReview: (_, { review: { comment, authorId, menuItemId } }) => {
return Review.create({
comment,
menuItemId,
authorId
});
},
}
}
Sequelize函数将总是返回一个承诺,所以我们正在返回函数返回的承诺。
运行这个查询:
mutation{
addMenuItem(params:{name: "asdasd", price: 21, rating: 33}){
id
name
price
}
}
菜单项将使用resolver函数在表中创建,查询返回的值将是:
{
"data": {
"addMenuItem": {
"id": "1",
"name": "Toast",
"price": 3
}
}
}
然后用返回的menuItemId运行这个查询:
mutation{
addReview(review: { comment: "not bad", authorId: 12, menuItemId: 1 }){
id
comment
authorId
}
}
其结果将是:
{
"data": {
"addReview": {
"id": "1",
"comment": "not bad",
"authorId": "12"
}
}
}
你是否注意到,尽管我们没有在模型中定义id ,但我们得到的结果是id ?这是因为sequelize在创建记录时自动将其添加到表中。
我们也可以把参数作为单独的值传递:
mutation {
addMenuItem(name: "Pizza", category: "Meal", price: 10.3) {
...
}
这都是关于我们如何在解析器函数中从参数中获取值。
我这边就说到这里。你有关于创建一个查询getSingleMenuItem 和一个突变deleteMenuItem 的练习。
在下一篇文章中见。如何用Elixir和Phoenix实现这些。
祝您好运!😇