最小的linux服务器_学习以最小的努力构建GraphQL服务器

余弘新
2023-12-01

最小的linux服务器

Today in web development, we will be learning how to:

在Web开发的今天,我们将学习如何:

  • Easily setup a GraphQL Server with NodeJS

    使用NodeJS轻松设置GraphQL服务器
  • Mock data without a database using json-server

    使用json-server模拟没有数据库的数据
  • Build a CRUD app that speaks GraphQL

    构建一个讲GraphQL的CRUD应用
  • How Apollo saves us a lot of time and effort

    阿波罗如何为我们节省大量时间和精力

If any of these items interest you, read on! Be sure to check out the source code for this repo if you would like to refer to the completed example.

如果您对这些项目感兴趣,请继续阅读! 如果您想参考完整的示例,请确保签出此仓库源代码

温柔的介绍 (Gentle Introduction)

A couple years ago, I spun up my first Node HTTP server with Express. It took only 6 lines of code on my end.

几年前,我使用Express调试了我的第一台Node HTTP服务器。 我只用了6行代码。

const express = require('express')
const app = express()

app.get('/', function(req, res) { 
  res.send({ hello: 'there' })
})

app.listen(3000, () => 'Listening at http://localhost:3000')

This reduced the necessary effort for building server side apps greatly, especially considering we could use our familiar JavaScript.

这大大减少了构建服务器端应用程序所需的精力,尤其是考虑到我们可以使用我们熟悉JavaScript时。

The floodgates were opened for countless tutorials and videos on setting up a Node server, usually for building some sort of CRUD REST API in record time.

打开了闸门,以获取有关设置节点服务器的大量教程和视频,通常用于在创纪录的时间内构建某种CRUD REST API。

CRUD refers to an app, server, or backend that can create, read, update, and delete — perhaps from a real database.

CRUD是指可以创建,读取,更新和删除(可能从真实数据库中删除)的应用程序,服务器或后端。

But this is 2018, we can do much cooler things.

但这是2018年,我们可以做很多更酷的事情。

Let’s replace REST with GraphQL.

让我们用GraphQL替换REST。

输入GraphQL (Enter GraphQL)

GraphQL is a declarative data fetch and manipulation layer that makes consuming APIs more client friendly.

GraphQL是声明性的数据获取和操作层,使使用API​​的客户端更加友好。

Some benefits of consuming data via a GraphQL server are:

通过GraphQL服务器使用数据的一些好处是:

  • You get exactly the data you are requesting by specifying the fields you need.

    通过指定所需的字段,您可以准确获取所需的数据。
  • Fewer requests and less over-fetching. GraphQL queries are usually specific enough to avoid grabbing unnecessary records or fields.

    更少的请求和更少的超量获取。 GraphQL查询通常足够具体,可以避免获取不必要的记录或字段。
  • Strongly typed schemas, as opposed to raw JSON fields that have no opinion on the type of data being returned.

    与原始JSON字段相对的强类型模式,该原始JSON字段对返回的数据类型没有任何意见。
  • GraphQL playground for data exploration that comes with autocomplete and built-in documentation. If you like working with Postman, you would be right at home with this interface.

    带有自动完成功能和内置文档的GraphQL游乐场,用于数据探索。 如果您喜欢使用Postman ,则可以使用此界面在家中。

That last point in particular makes on-boarding new developers much easier.

最后一点尤其使新入职的开发人员更加容易。

They no longer have to study your hundreds of endpoints on swagger, because they can explore the types and relations between them in this interface.

他们不再需要研究您成百上千的端点,因为他们可以在此界面中探索它们之间的类型和关系。

More on this soon, let’s get to coding.

很快,我们开始编码。

入门和安装依赖项 (Getting Started and Installing Dependencies)

Let us start by creating a directory and initializing a package.json file.

让我们从创建目录并初始化package.json文件开始。

mkdir social-graphql && cd social-graphql && npm init -y

Our tech stack will look like this:

我们的技术堆栈将如下所示:

  • JavaScript running with Node (no client side code today)

    运行NodeJavaScript(今天没有客户端代码)
  • Babel for writing modern ES6

    通天塔写现代ES6
  • Express for quickly setting up an HTTP server

    Express,用于快速设置HTTP服务器
  • Apollo Server for all of the useful GraphQL utilities that help us set up the server and build schemas

    Apollo Server提供所有有用的GraphQL实用程序,可帮助我们设置服务器和构建架构
  • json-server for testing on a fake dataset (much easier than querying a real database)

    用于在假数据集上进行测试的json服务器(比查询真实数据库要容易得多)
npm install -S express apollo-server-express graphql json-server axios

In addition, we’ll have some dev dependencies that’ll assist us.

另外,我们将有一些开发依赖关系将对我们有帮助。

npm install -D babel-cli babel-preset-env nodemon npm-run-all

With the dependencies out of the way, we can get into coding.

不用依赖,我们就可以开始编码了。

从基本的HTTP服务器开始 (Starting with a basic HTTP server)

Let’s create an HTTP server that handles the index route. That is, if I run the server and navigate to http://localhost:3500 I should see the JSON message, as opposed to ‘Cannot GET “/”’.

让我们创建一个处理索引路由的HTTP服务器。 也就是说,如果我运行服务器并导航到http:// localhost:3500 ,则应该看到JSON消息,而不是“无法获取“ /””。

Create an index.js file:

创建一个index.js文件:

import express from 'express'

const PORT = process.env.PORT || 3500
const app = express()

app.get('/', function(req, res) {
  res.send({ hello: 'there!' })
})

app.listen(PORT, () => `Listening at http://localhost:${PORT}`)

This is very similar to the code at the beginning of the article, with the exception of the import syntax and the port being configurable through environment variables.

除了导入语法和可通过环境变量配置的端口外,这与本文开头的代码非常相似。

To get the import syntax working here, we’ll need to take advantage of our babel preset. Create a file called .babelrc and:

为了使导入语法在这里起作用,我们需要利用我们的babel预设。 创建一个名为.babelrc的文件,然后:

{
  "presets": ["env"]
}

Finally, to run the server, update the start script in package.json to this:

最后,要运行服务器,请将package.json的启动脚本更新为:

"scripts": {
  "dev:api": "nodemon --exec 'babel-node index.js'"
}

And then enter npm run dev:api in your terminal. By navigating to http://localhost:3500 you will be able to see a response that says “hello: there!”.

然后在终端中输入npm run dev:api 。 通过导航到http:// localhost:3500,您将看到显示“ hello:there!”的响应。

Unlike the more typical node index.js in an npm start script, we are using a dev command along with nodemon executing babel-node.

npm start脚本中更典型的node index.js不同,我们将dev命令与nodemon一起使用来执行babel-node。

Nodemon restarts your dev server whenever you save files so that you don’t have to. Usually it executes with node, but we are telling it to execute with babel-node so it handles our fancy ES6 imports.

每当您保存文件时,Nodemon都会重新启动您的开发服务器,而无需这样做。 通常它使用node执行,但是我们告诉它使用babel-node执行,因此它可以处理我们喜欢的ES6导入。

升级到阿波罗 (Upgrading to Apollo)

Alright, we have put together a basic HTTP server that can serve REST endpoints. Let us update it in order to serve GraphQL.

好的,我们已经建立了一个可以为REST端点提供服务的基本HTTP服务器。 让我们对其进行更新以服务GraphQL。

import express from 'express'
import { ApolloServer } from 'apollo-server-express'
import { resolvers, typeDefs } from './schema'

const PORT = process.env.PORT || 3500
const app = express()

const server = new ApolloServer({
  typeDefs,
  resolvers,
  playground: true
})

server.applyMiddleware({ app })

app.get('/', (req, res) => {
  res.send({ hello: 'there!' })
})

app.listen(PORT, () =>
  console.log(`Listening at http://localhost:${PORT}/graphql`)
)

Then, inside a new file that I will call schema.js, insert:

然后,在一个我称为schema.js的新文件中,插入:

import { gql } from 'apollo-server-express'

export const typeDefs = gql`
  type Query {
    users: String
  }
`

export const resolvers = {
  Query: {
    users() {
      return "This will soon return users!"
    }
  }
}

解析器和架构(类型定义) (The Resolvers and Schema (type definitions))

Here, if you are new to working with GraphQL, you’ll see this funny syntax we’re assigning to typeDefs.

在这里,如果您不熟悉GraphQL,您会看到我们分配给typeDefs这个有趣的语法。

In ES6 JavaScript, we can invoke a function using backticks as we are with gql. In terms of vanilla JavaScript, you can read it like this:

在ES6 JavaScript中,我们可以像使用gql一样使用反引号来调用函数。 对于普通JavaScript,您可以这样阅读:

gql.apply(null, ["type Query {\n users: String \n }"])

Essentially, it calls gql with an array of arguments. It just so happens that writing multiline strings is convenient when expressing a JSON-like query.

本质上,它使用参数数组调用gql 。 碰巧的是,在表达类似JSON的查询时编写多行字符串很方便。

If you are still running the server, head over to http://localhost:3500/graphql. Here you’ll be able to see a fantastic interface for testing our queries.

如果仍在运行服务器,请转到http:// localhost:3500 / graphql 。 在这里,您将能够看到一个出色的界面,用于测试我们的查询。

That’s right, no more tossing cURLs at an obscure endpoint, we can test our queries with autocomplete, prettifying, and built-in documentation. It’s also out-of-the-box with Apollo, so you don’t need to install additional packages or apps.

没错,不再需要在晦涩的端点上抛出cURL了,我们可以使用自动完成,修饰和内置文档来测试查询。 它还与Apollo兼容,因此您无需安装其他软件包或应用程序。

Now, let’s make this query a little bit more interesting.

现在,让我们使该查询更有趣。

实施真实世界的GraphQL查询:列出用户 (Implementing a Real-World GraphQL Query: List Users)

Before diving too deeply into this section, be sure to copy db.json from this repository into your working directory alongside index.js and schema.js.

在深入研究本节之前,请确保将db.json从此存储库中复制到index.js和schema.js一起到您的工作目录中。

Then, update the scripts in package.json:

然后,更新package.json的脚本:

"scripts": {
  "dev": "npm-run-all --parallel dev:*",
  "dev:api": "nodemon --exec 'babel-node index.js' --ignore db.json",
  "dev:json": "json-server --watch db.json"
}

Re-run the server with npm run dev and press on.

使用npm run dev重新运行服务器,然后按。

In a GraphQL server, there is a concept of the root query. This query type is the entry point for any data fetch requests to our GraphQL schema. For us, it looks like this:

在GraphQL服务器中,存在根查询的概念。 此查询类型是对我们的GraphQL模式的任何数据获取请求的入口点。 对于我们来说,看起来像这样:

type Query {
  users: String
}

If we are serving users, posts, or airplanes, the client that is requesting data must do it by going through the root query.

如果我们正在为用户,职位或飞机提供服务,则请求数据的客户端必须通过根查询来完成。

type Query {
  users: [User] # here the "[]"s mean these are returning lists
  posts: [Post]
  airplanes: [Airplane]
}

For instance, if we wanted to define a new query on our server, we would have to update at least two places.

例如,如果要在服务器上定义一个新查询,则必须至少更新两个位置。

  1. Add the query under the Query type within our type definitions.

    在我们的类型定义内的“查询”类型下添加查询。
  2. Add a resolver function under the Query object in our resolvers object.

    在我们的resolvers对象的Query对象下添加一个resolver函数。

We would then need to make sure we have the correct type of the return data. For a lists of users, that means returning an array of objects, each with a name, email, age, friends, and ID.

然后,我们需要确保返回数据的类型正确。 对于用户列表,这意味着返回对象的数组,每个对象都有名称,电子邮件,年龄,朋友和ID。

Our current schema has our users query returning a simple string. This is no good, as we expect user data to come back from this route.

当前的模式让我们的用户查询返回一个简单的字符串。 这不好,因为我们希望用户数据会从此路由返回。

Update schema.js as follows:

如下更新schema.js

export const typeDefs = gql`
  type User {
    id: ID
    name: String
    age: Int
    email: String
    friends: [User]
  }

  type Query {
    users: [User]
  }
`

Great, we have the user type, and the root query that returns some list of users.

很好,我们有用户类型,还有返回一些用户列表的根查询。

Let us update the resolver:

让我们更新解析器:

export const resolvers = {
  Query: {
    users() {
      return userModel.list()
    }
  }
}

Inside of our resolver, we call list from the userModel, which we have yet to define.

在解析器内部,我们从尚未定义的userModel调用列表。

Inside a new file called models.js, add the following:

在名为models.js的新文件中,添加以下内容:

import axios from 'axios'

class User {
  constructor() {
    this.api = axios.create({
      baseURL: 'http://localhost:3000' // json-server endpoint
    })
  }

  list() {
    return this.api.get('/users').then(res => res.data)
  }
}

export default new User()

This class forms an abstraction layer over the logic that directly handles our data.

此类在直接处理我们的数据的逻辑上形成了一个抽象层。

Finally, at the top of schema.js, add this import:

最后,在schema.js的顶部,添加以下导入:

import userModel from './models'

Back to http://localhost:3500/graphql, paste and run this query:

返回http:// localhost:3500 / graphql,粘贴并运行以下查询:

query Users {
  users {
    id
    name
    email
  }
}

The user query now looks a little more exciting! For each user in our db.json file, we have returned their id, name, and email.

用户查询现在看起来更加令人兴奋! 对于db.json文件中的每个用户,我们都返回了他们的ID,名称和电子邮件。

Since we are using json-server hosted on a local port, we use the model as if it were retrieving data from a remote API.

由于我们使用的是本地端口上托管的json服务器,因此我们使用该模型,就好像它是从远程API检索数据一样。

In many cases, our model would be making database calls or retrieving data from a key-value store like firebase.

在许多情况下,我们的模型将是进行数据库调用或从诸如Firebase之类的键值存储中检索数据。

However, from the perspective of a client, they have no idea how the model is retrieving the data — they only know about the shape of the data.

但是,从客户的角度来看,他们不知道模型如何检索数据-他们只知道数据的形状。

This abstraction makes GraphQL an ideal tool for resolving data from multiple sources into a single query.

这种抽象使GraphQL成为将来自多个源的数据解析为单个查询的理想工具。

朋友之友:更激烈的查询 (Friends of Friends: A More Intense Query)

Getting a list of users is neat, and so is the GraphQL playground. But so far, you could easily do the same work with a REST endpoint.

获取用户列表很简单,GraphQL游乐场也是如此。 但是到目前为止,您可以轻松地使用REST端点执行相同的工作。

What if you wanted to retrieve the users, as well as all of the friends for a particular user? We want to run a query like this:

如果您想检索用户以及特定用户的所有朋友怎么办? 我们要运行这样的查询:

query UsersAndFriends {
  users {
    id
    name
    friends {
      id
      name
    }
  }
}

In order to do this, note the shape of data in our db.json file: each user has a friends field which is an array of objects keyed by ID.

为此,请注意db.json文件中的数据形状:每个用户都有一个friends字段,该字段是由ID键控的对象数组。

Basically, we are going to make some sort of request for each ID that we find, for each user.

基本上,我们将为发现的每个用户的每个ID发出某种请求。

Does it sound like an intense computation?

听起来像是一次激烈的计算吗?

It is, we would be executing a new query to our data store for every single friend of every single user we retrieve.

就是说,我们将对检索到的每个用户的每个朋友执行一个对数据库的新查询。

Implementing some sort of cache would help tremendously in reducing the amount of work done to complete the query — but let us not worry about optimizing it for now.

实现某种缓存将极大地减少完成查询所需的工作量,但现在我们不必担心对其进行优化。

In models.js, and this find method to the User class:

models.js ,该find方法用于User类:

class User {
  constructor() {
    this.api = axios.create({
      baseURL: 'http://localhost:3000' // json-server endpoint
    })
  }

  list() {
    return this.api.get('/users').then(res => res.data)
  }

  find(id) {
    return this.api.get(`/users/${id}`).then(res => res.data)
  }
}

Now we can use this method in a new User resolver. The difference in this resolver is that it gets used when it is trying to resolve connections to a particular type, friends here.

现在,我们可以在新的用户解析器中使用此方法。 此解析器的不同之处在于,它在尝试解析与特定类型(此处为friends连接时会使用它。

Otherwise, the query would not know how to resolve a list of Users when it sees friends.

否则,查询看到friends时将不知道如何解析用户列表。

export const resolvers = {
  Query: {
    users() {
      return userModel.list()
    }
  },
  User: {
    friends(source) {
      if (!source.friends || !source.friends.length) {
        return
      }

      return Promise.all(
        source.friends.map(({ id }) => userModel.find(id))
      )
    }
  },
}

In the friends method, source is the parent value that the resolver function gets called with. That is, for the user with id 0, Peck Montoya, the value of source is the whole object with the list of friend ids.

在friends方法中,source是调用解析器函数的父值。 也就是说,对于ID为0的用户Peck Montoya,source的值是带有朋友id列表的整个对象。

For root queries, source is most often undefined, because the root query is not resolved from a particular source.

对于根查询,源通常是未定义的,因为不能从特定源解析根查询。

The friends method returns when all of the requests to find individual users have been resolved.

当所有查找单个用户的请求均得到解决时,friends方法将返回。

Now try running this query if you didn’t try earlier:

如果您之前没有尝试过,现在尝试运行此查询:

query UsersAndFriends {
  users {
    id
    name
    friends {
      id
      name
    }
  }
}

突变:创建用户 (Mutations: Creating a User)

So far we have just been getting data. What if we wanted to mutate data?

到目前为止,我们一直在获取数据。 如果我们想突变数据怎么办?

Let’s start by creating a user with a name and age.

首先创建一个具有名称和年龄的用户。

Take a look at this mutation:

看一下这个突变:

mutation CreateUser($name: String!, $email: String, $age: Int) {
  createUser(name: $name, email: $email, age: $age) {
    name
    email
    age
  }
}

Some differences at first glance:

乍一看有些区别:

  • we denote this code with “mutation” rather than “query”

    我们用“变异”而不是“查询”来表示该代码
  • we pass two sets of similar looking arguments

    我们传递了两组相似的参数

The arguments are basically type declarations for the variables expected by our query.

参数基本上是查询所期望的变量的类型声明。

If there is a mismatch between those types and the ones passed by a client such as a web or mobile app, the GraphQL server will throw an error.

如果这些类型与客户端(如Web或移动应用程序)传递的类型不匹配,则GraphQL服务器将引发错误。

To get this query to work now, let us first update the User class in model.js:

为了使此查询立即model.js ,让我们首先更新model.js的User类:

create(data) {
  data.friends = data.friends 
    ? data.friends.map(id => ({ id })) 
    : []

  return this.api.post('/users', data).then(res => res.data)
}

When we fire off this request, json-server will append a new user with the data we passed up.

当我们触发此请求时,json-server将向新用户添加我们传递的数据。

Now update schema.js to the following:

现在将schema.js更新为以下内容:

export const typeDefs = gql`

  # other types...

  type Mutation {
    createUser(name: String!, email: String, age: Int): User
  }
`

export const resolvers = {
  // other resolvers...
  Mutation: {
    createUser(source, args) {
      return userModel.create(args)
    }
  }
}

At this point, the query should work. But we can do a little better.

此时,查询应该可以工作了。 但是我们可以做得更好。

简化查询和变异参数 (Simplifying Query & Mutation Arguments)

Rather than write out every single argument for the mutation, we can define input types. This will make future mutations and queries we write more composable.

我们可以定义输入类型 ,而不是为突变写出每个参数。 这将使我们编写的将来的变异和查询变得更容易组合。

export const typeDefs = gql`

  # other types...

  input CreateUserInput {
    id: Int
    name: String
    age: Int
    email: String
    friends: [Int]
  }

  type Mutation {
    createUser(input: CreateUserInput!): User
  }
`

export const resolvers = {
  // other resolvers...
  Mutation: {
    createUser(source, args) {
      return userModel.create(args.input)
    }
  }
}

See that if we wanted to implement an UpdateUser mutation, we could probably take advantage of this new input type.

看到如果我们想实现UpdateUser突变,我们可能可以利用这种新的输入类型。

Now try out this mutation:

现在尝试这种突变:

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    name
    email
    age
    friends {
      id
      name
    }
  }
}

In order to populate the variables that go into the query, click and expand the tab labeled “Query Variables“ in the lower left of the GraphQL playground.

为了填充查询中要使用的变量,请单击并展开GraphQL游乐场左下角标有“查询变量”的选项卡。

Then, input this JSON:

然后,输入以下JSON:

{
  "input": {
    "name": "Indigo Montoya",
    "email": "indigomontoya@gmail.com",
    "age": 29,
    "id": 13,
    "friends": [1,2]
  }
}

Assuming all went well, you should see a response with the user we just created. You should also see the two users with ids 1 and 2.

假设一切顺利,您应该看到我们刚刚创建的用户的响应。 您还应该看到两个ID为1和2的用户。

Now our create method isn’t totally complete — the friends of our newly created user have no idea that our new user is their friends.

现在,我们的创建方法还不够完善-我们新创建的用户的朋友并不知道我们的新用户就是他们的朋友。

In order to create a user with references to their friends, we would need to update the friends list of the users who were referenced as well.

为了创建参考他们朋友的用户,我们需要更新参考用户的朋友列表。

I will opt to leave that as an exercise to the reader if they are so inclined.

如果愿意,我选择将其留给读者练习。

连接点(包装) (Connecting the Dots (Wrapping Up))

Be sure to check out the source code for this repo if you would like to see how I implemented the deleteUser and updateUser mutations.

如果您想了解我如何实现deleteUserupdateUser突变,请务必签出 deleteUser源代码

Using GraphQL with Apollo in my own projects has been a blast. I can honestly say its just more fun to develop GraphQL schemas and resolvers than it is to implement HTTP route handlers.

在我自己的项目中将GraphQL与Apollo结合使用是一个爆炸。 老实说,开发GraphQL模式和解析器比实现HTTP路由处理程序更有趣。

If you’d like to learn more about GraphQL, check out these publications on Medium:

如果您想了解有关GraphQL的更多信息,请查看Medium中的以下出版物:

If you enjoyed this article and would like to see more in the future, let me know in the comments and give me a follow on Twitter and Medium!

如果您喜欢这篇文章,并希望在以后看到更多内容,请在评论中让我知道,然后在TwitterMedium中关注我!

翻译自: https://www.freecodecamp.org/news/learn-to-build-a-graphql-server-with-minimal-effort-fc7fcabe8ebd/

最小的linux服务器

 类似资料: