GraphQL的全栈指南——NodeJs服务器

159 阅读5分钟

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}`);
});

好的,发生了什么?

首先,我们正在获得我们需要的服务,ApolloServergql

  • 我们实例化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操作变成数据。现在,它对我们来说只是一个数组。

让我们在menuItemreviews ,添加另一个字段,并说明其类型:

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"
      }]
    }]
  }
}

解析器可以返回一个ObjectPromisescalar 的值,这也应该符合模式中为该字段定义的数据类型。如果返回一个承诺,解析的数据将被发送。

每个解析器函数被调用时都需要四个参数:

  1. parent:对象包含来自父解析器的返回值。

    每个GraphQL查询都是服务器中的一个函数调用树。所以,每个字段的解析器都会得到父解析器的结果,在这种情况下。

  • queryakarootQuery是最高级别的Parents之一。
  • Query.menuItem中的Parent将是服务器配置传递给rootQuery的任何内容。
  • MainItem.reviews中的Parent将是来自解析器Query.MenuItems的返回值。
  • Review.id、Review.commentReview.authorId中的父级将是*MenuItem.*review的解析值。
  1. params:对象包含我们在查询中传递的参数,如query { menuItem(id: 12) { name } } ,参数将是{ id: 12 }

  2. context:你可以在实例化服务器时传递一个对象并在每个解析器上访问它。

    例子:

    const server = new ApolloServer({
      typeDefs,
      resolvers,
      context: { menuRefInContext: MenuItem }
    });
    
    Query: {
      menuItems: (parent, __, { menuRefInContext }) => menuTableInContext.findAll(),
    },
    
  3. info:这个参数主要包含关于你的模式和当前执行状态的信息

默认解析器

每个类型都不需要有一个解析器,ApolloServer 提供了一个默认的解析器,它在父对象中寻找相关的字段名,或者如果我们为字段明确定义了一个函数,则调用这个函数。

对于下面的模式,如果reviews 解析器的结果返回一个已经包含comment 字段的对象的列表,那么Reviewcomment 字段就不需要一个解析器:

type Review {
  comment: String!
  authorId: ID!
}

type MenuItem {
  reviews: [Review]
}

启动服务器

typeDefsresolvers 实例化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,FloatID 的字段返回数据,但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,PATCHDELETE 的工作

将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
  }
`

在这里,我们引入了两个新字段:Mutationinput

变异字段

QueryMutation 也是一个特殊的字段,我们将在这里定义所有的突变(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实现这些。

祝您好运!😇