这是「进击的Coder」的第 356 篇技术分享

作者:Tyler Hawkins

译者:samyu2000

校对者:PassionPenguin, k8scat

来源:掘金开发者社区

阅读本文大概需要 28 分钟。

你的 dad jokes 放在哪儿?当然是在 dadabase 里。我们来想象一下,你是全世界最受欢迎的 dad jokes 数据库的管理员。项目的技术概况是:使用 REST API 与数据库通信,这种 REST API 具有搜索笑话和对笑话进行评分的功能;网站的访问者可以通过一个简单的用户界面对每条笑话进行评分。

最近你了解到一种新技术,它叫做 GraphQL,它具有一定的灵活性,可以精准获取你需要的数据,而且是使用单一的 API 结点。这听上去很不错,于是你打算在应用程序中使用这种技术。但是,你不希望对原有的 REST API 作过多的改动。能否让你的项目同时支持 REST API 和 GraphQL API?

在本文中,我们会讨论如何基于已有的 REST API 来实现 GraphQL API。你使用这种方法,不需要对基于原有的 REST API 框架进行调整,就可以在项目的未完成的模块中使用 GraphQL。

如果你想看到最终的结果,可以访问 REST API 代码 和 前端和 GraphQL API 代码。还要记得浏览一下网站,那些笑话很值得看哦。

初始架构

项目的后台原先是使用 Node 和 JSON Server 开发的。JSON Server 利用 Express 为一个模拟的数据库提供了完整的 REST API,并且这个数据库是由一个简单的 JSON 文件生成的。前端是使用 Vanilla JS 实现的,并使用浏览器内嵌的 Fetch API 发出 API 请求。该应用程序托管在 Heroku 上,可以方便地对它进行部署和监控。

我们使用的 JSON 文件含有一些笑话和评分信息。下面,我们把它完整地复制出来:

{"jokes": [{"id": 1,"content": "I don't often tell dad jokes, but when I do, sometimes he laughs."},{"id": 2,"content": "Why was the scarecrow promoted? For being outstanding in his field."},{"id": 3,"content": "What did the grape do when someone stepped on him? He let out a little whine."},{"id": 4,"content": "Einstein, Pascal, and Newton are playing hide and seek. Einstein covers his eyes and begins counting. While Pascal runs off and hides, Newton takes out some chalk and marks a square on the ground with side lengths of exactly 1 meter, then sits down inside the square. When Einstein is finished counting and sees Newton sitting on the ground, he yells, \"Ha, I've found you, Newton!\". Newton replies, \"No you haven't! You've found one Newton over a square meter. You've found Pascal!"}],"ratings": [{ "id": 1, "jokeId": 1, "score": 8 },{ "id": 2, "jokeId": 2, "score": 3 },{ "id": 3, "jokeId": 3, "score": 6 },{ "id": 4, "jokeId": 1, "score": 7 },{ "id": 5, "jokeId": 2, "score": 6 },{ "id": 6, "jokeId": 3, "score": 4 },{ "id": 7, "jokeId": 1, "score": 9 },{ "id": 8, "jokeId": 2, "score": 10 },{ "id": 9, "jokeId": 3, "score": 2 },{ "id": 10, "jokeId": 4, "score": 10 },{ "id": 11, "jokeId": 4, "score": 10 },{ "id": 12, "jokeId": 4, "score": 10 },{ "id": 13, "jokeId": 4, "score": 10 },{ "id": 14, "jokeId": 4, "score": 10 },{ "id": 15, "jokeId": 4, "score": 10 }]
}
复制代码

JSON Server 系统把这个文件中的数据作为数据库的初始数据,接着实现一套 REST API,其中包括对 GET, POST, PUT, PATCH 和 DELETE 请求的支持。JSON Server 的神奇之处在于,使用这套 API 就能实现对 JSON 文件的修改,因此数据库就是完全交互式的。JSON Server 不经安装就可以直接由 npm 脚本启动,但为了对它进行一些配置以及端口的设置,我们可以写下几行代码并运行它,代码如下:

const jsonServer = require('json-server')
const server = jsonServer.create()
const router = jsonServer.router('db.json')
const middlewares = jsonServer.defaults()server.use(middlewares)
server.use(router)
server.listen(process.env.PORT || 3000, () => {console.log(`???? JSON Server is running on port ${process.env.PORT || 3000}`)
})复制代码

欲对这个模拟的数据库进行测试,你可以把 API 有关的仓库克隆到本地,并运行 npm installnpm start。在浏览器中访问 http://localhost:3000/jokes ,页面会显示所有的笑话。访问 http://localhost:3000/ratings ,页面会显示所有的评分信息。

太棒了。我们可以在浏览器上运行应用程序的后台。现在我们把 API 托管在 Heroku 中。首先需要安装 Heroku 命令行工具。然后,我们可以进行这些操作:登录,创建项目,推送到 Heroku 服务端,在浏览器中打开项目的操作界面。

# 登录你的 Heroku 账户
heroku login# 创建项目
heroku create dad-joke-dadabase-rest-api# 将代码部署到 Heroku 服务端
git push heroku master# 打开项目的后台页面
heroku open
复制代码

看,现在我们把 API 发布到公网上了!

构建用户界面

既然我们已经部署了一个运行中的 REST API,就可以制作前端页面,并使用 API 把这些笑话数据呈现在页面上,还可以对这些笑话进行评分。下面的 HTML 页面代码实现了一个显示笑话内容的容器,笑话内容由 JavaScript 代码加载进来。

<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta ><title>Dad Joke Dadabase</title><meta Where do you keep your dad jokes? In a dadabase of course!"><meta ><link rel="stylesheet" href="./style.css">
</head>
<body><h1>Dad Joke Dadabase</h1><div class="project"><h2 class="jokeContent"></h2><div class="rateThisJokeContainer"><p>Rate this joke:</p><div class="rateThisJokeOptions"><span class="formGroup"><input type="radio" id="score-1" >1</label></span><span class="formGroup"></span><input type="radio" id="score-2" >2</label></span><span class="formGroup"></span><input type="radio" id="score-3" >3</label></span><span class="formGroup"></span><input type="radio" id="score-4" >4</label></span><span class="formGroup"></span><input type="radio" id="score-5" >5</label></span><span class="formGroup"></span><input type="radio" id="score-6" >6</label></span><span class="formGroup"></span><input type="radio" id="score-7" >7</label></span><span class="formGroup"></span><input type="radio" id="score-8" >8</label></span><span class="formGroup"></span><input type="radio" id="score-9" >9</label></span><span class="formGroup"></span><input type="radio" id="score-10" >10</label></span></div></div><p class="averageRating">Average Rating: <span class="jokeRatingValue">7.8</span></p><button id="nextJoke">See Next Joke</button>
</div><script src="./script.js"></script>
</body>
</html>
复制代码

JavaScript 代码如下。跟 REST API 交互的关键代码在于两个获取数据的请求。第一个请求通过访问 /jokes?_embed=ratings 获取数据库中所有的笑话,第二个请求是 POST 类型的,它通过访问 /ratings 提交对某个笑话的评分。

const jokeContent = document.querySelector('.jokeContent')
const jokeRatingValue = document.querySelector('.jokeRatingValue')
const nextJokeButton = document.querySelector('#nextJoke')const jokes = []
let currentJokeIndex = -1const displayNextJoke = () => {currentJokeIndex++if (currentJokeIndex >= jokes.length) {currentJokeIndex = 0}const joke = jokes[currentJokeIndex]jokeContent.textContent = joke.contentconst totalScore = joke.ratings.reduce((total, rating) => (total += rating.score),)const numberOfRatings = joke.ratings.lengthconst averageRating = totalScore / numberOfRatingsjokeRatingValue.textContent = averageRating.toFixed(1)
}const submitJokeRating = () => {const ratingInput = document.querySelector('input[]:checked')if (ratingInput && ratingInput.value) {const score = Number(ratingInput.value)const jokeId = jokes[currentJokeIndex].idconst postData = { jokeId, score }fetch('/ratings', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify(postData),}).then(response => response.json()).then(responseData => {const jokeToUpdate = jokes.find(joke => joke.id === responseData.jokeId)jokeToUpdate && jokeToUpdate.ratings.push(responseData)}).finally(() => {ratingInput.checked = falsedisplayNextJoke()})} else {displayNextJoke()}
}nextJokeButton.addEventListener('click', submitJokeRating)fetch('/jokes?_embed=ratings').then(response => response.json()).then(data => {jokes.push(...data)displayNextJoke()})复制代码

安装并使用 Apollo Server

这样,我们已经完成了项目的架构:它有一个简单的页面,该页面通过 REST API 跟数据库通信。那么,我们如何使用 GraphQL?使用 GraphQL 之前需要哪些准备工作呢?第一步,我们安装 [apollo-server-express](https://www.npmjs.com/package/apollo-server-express),它是一个程序包,用于实现 Apollo Server 和 Express 的集成。也需要安装 [apollo-datasource-rest](https://www.npmjs.com/package/apollo-datasource-rest) 包,用于 REST API 和 Apollo Server 的集成。然后,我们来配置服务器,需要编写以下代码:

const express = require('express')
const path = require('path')
const { ApolloServer } = require('apollo-server-express')
const JokesAPI = require('./jokesAPI')
const RatingsAPI = require('./ratingsAPI')
const typeDefs = require('./typeDefs')
const resolvers = require('./resolvers')const app = express()
const server = new ApolloServer({typeDefs,resolvers,dataSources: () => ({jokesAPI: new JokesAPI(),ratingsAPI: new RatingsAPI(),}),
})server.applyMiddleware({ app })app.use(express.static(path.join(__dirname, 'public'))).get('/', (req, res) => {res.sendFile('index.html', { root: 'public' })}).get('/script.js', (req, res) => {res.sendFile('script.js', { root: 'public' })}).get('/style.css', (req, res) => {res.sendFile('style.css', { root: 'public' })})app.listen({ port: process.env.PORT || 4000 }, () => {console.log(`???? Server ready at port ${process.env.PORT || 4000}`)
})复制代码

你可以看到,我们配置了 Apollo Server 的三个属性:typeDefs, resolversdataSources。其中,typeDefs 属性包含了与我们的 GraphQL API 相关的 schema,我们在相应的包中定义笑话和评分的数据类型,以及如何查询和更新数据;resolvers 告诉服务器如何处理各种各样的查询和更新需求,以及如何连接数据源;最后,dataSources 大致描述了 GraphQL API 与 REST API 的关联关系。

下面的代码定义了 JokeRating 数据类型,以及如何查询和更新数据。

const { gql } = require('apollo-server-express')const typeDefs = gql`type Joke {id: Int!content: String!ratings: [Rating]}type Rating {id: Int!jokeId: Int!score: Int!}type Query {joke(id: Int!): Jokejokes: [Joke]rating(id: Int!): Ratingratings: [Rating]}type Mutation {rating(jokeId: Int!, score: Int!): Rating}
`module.exports = typeDefs
复制代码

下面是 JokesAPI 类的代码,主要定义了笑话数据创建、查询、更新、删除的方法,这些方法分别调用相应的 REST API 实施相关的数据操作。

const { RESTDataSource } = require('apollo-datasource-rest')class JokesAPI extends RESTDataSource {constructor() {super()this.baseURL = 'https://dad-joke-dadabase-rest-api.herokuapp.com/'}async getJoke(id) {return this.get(`jokes/${id}?_embed=ratings`)}async getJokes() {return this.get('jokes?_embed=ratings')}async postJoke(jokeContent) {return this.post('jokes', jokeContent)}async replaceJoke(joke) {return this.put('jokes', joke)}async updateJoke(joke) {return this.patch('jokes', { id: joke.id, joke })}async deleteJoke(id) {return this.delete(`jokes/${id}`)}
}module.exports = JokesAPI
复制代码

评分数据跟笑话相似,只是在每个实例中把 “joke” 变为 “rating”。欲获取这部分代码,可以参考 GitHub 上的代码仓库。

最后,我们设置解析器,在其中定义如何使用数据源。

const resolvers = {Query: {joke: async (_source, { id }, { dataSources }) =>dataSources.jokesAPI.getJoke(id),jokes: async (_source, _args, { dataSources }) =>dataSources.jokesAPI.getJokes(),rating: async (_source, { id }, { dataSources }) =>dataSources.ratingsAPI.getRating(id),ratings: async (_source, _args, { dataSources }) =>dataSources.ratingsAPI.getRatings(),},Mutation: {rating: async (_source, { jokeId, score }, { dataSources }) => {const rating = await dataSources.ratingsAPI.postRating({ jokeId, score })return rating},},
}module.exports = resolvers
复制代码

完成这些步骤,我们一切准备就绪,可以通过 Apollo Server 调用 GraphQL API 了。为了把新的前端页面和 GraphQL API 托管在 Heroku 上,我们需要创建并部署第二个应用程序:

# 创建 Heroku 应用程序
heroku create dad-joke-dadabase# 把代码部署在 Heroku 上
git push heroku master# 在本地打开 Heroku 应用程序
heroku open
复制代码

把 API 端点功能改为获取笑话的代码

你应当回忆下,我们有两个 API 端点供前端页面调用:它们的功能分别是获取笑话和提交评分。现在我们把 REST API 中获取笑话的代码改为 GraphQL API 形式:

fetch('/jokes?_embed=ratings').then(response => response.json()).then(data => {jokes.push(...data)displayNextJoke()})
复制代码

我们把上述代码改为:

fetch('/graphql', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({query: `query GetAllJokesWithRatings {jokes {idcontentratings {scoreidjokeId}}}`,}),
}).then(res => res.json()).then(res => {jokes.push(...res.data.jokes)displayNextJoke()})
复制代码

现在,我们可以在本地运行应用程序了。实际上,从用户的角度来说,没有发生任何变化。但假如你在浏览器的开发者工具中查看网络请求,你会发现,现在获取笑话是通过访问 /graphql 端点来实现的了。真棒!

把 API 端点功能改为提交评分的代码

一个 API 请求已完成,还有一个!我们现在对评分功能的代码进行修改。提交评分的代码原来类似于:

fetch('/ratings', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify(postData),
}).then(response => response.json()).then(responseData => {const jokeToUpdate = jokes.find(joke => joke.id === responseData.jokeId)jokeToUpdate && jokeToUpdate.ratings.push(responseData)}).finally(() => {ratingInput.checked = falsedisplayNextJoke()})
复制代码

现在我们作如下的改动,让它使用我们的 GraphQL API:

fetch('/graphql', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({query: `mutation CreateRating {rating(jokeId: ${jokeId}, score: ${score}) {idscorejokeId}}`,}),
}).then(res => res.json()).then(res => {const rating = res.data.ratingconst jokeToUpdate = jokes.find(joke => joke.id === rating.jokeId)jokeToUpdate && jokeToUpdate.ratings.push(rating)}).finally(() => {ratingInput.checked = falsedisplayNextJoke()})
复制代码

经过快速测试,这段代码符合需求。再次说明,用户体验没有变,但现在我们请求数据使用的都是 /graphql 端点。

结论

我们做到了。我们以已有的 REST API 为基础,成功地实现了一个 GraphQL API 端点。因此,我们也能使用 GraphQL 来实现一些核心功能,而且已有的功能和原来的 REST API 都不需要修改。如今我们可以弃用 REST API,它将来也可能会退出历史舞台。

虽然 dad joke 数据库是完全虚拟的项目,但几乎所有的在 2015 年 GraphQL 发布会之前成立的科技公司都发现:如果他们改变技术路线,使用 GraphQL,他们自身的情况跟 dad jokes 一样,也是可行的。另外,还有个好消息,Apollo Server 属于较灵活的产品,它也可以从包括 REST API 端点在内的各种数据源获取数据。

End

「进击的Coder」专属学习群已正式成立,搜索「CQCcqc4」添加崔庆才的个人微信或者扫描下方二维码拉您入群交流学习。

看完记得关注@进击的Coder

及时收看更多好文

↓↓↓

点个在看你最好看

如何基于已有的 REST API 实现 GraphQL API相关推荐

  1. laravel api_如何在现有的Laravel应用中获取即时GraphQL API

    laravel api by Karthikeya Viswanath 通过Karthikeya Viswanath 如何在现有的Laravel应用中获取即时GraphQL API (How to g ...

  2. 给它一个REST:对您的API使用GraphQL

    by David Celis 大卫·塞利斯(David Celis) 给它一个REST:对您的API使用GraphQL (Give it a REST: use GraphQL for your AP ...

  3. 人人都是 API 设计者:我对 RESTful API、GraphQL、RPC API 的思考

    有一段时间没怎么写文章了,今天提笔写一篇自己对 API 设计的思考.首先,为什么写这个话题呢?其一,我阅读了<阿里研究员谷朴:API 设计最佳实践的思考>一文后受益良多,前两天并转载了这篇 ...

  4. 基本操作:Go创建GraphQL API

    女主宣言 越来越多的项目中都能看到GraphQL的身影,不知道大家在项目中有没有使用过GraphQL呢?今天给大家分享一下使用Go,来创建基础GraphQL API,供大家参考学习. PS:丰富的一线 ...

  5. 区块链/以太坊/DEX-在以太坊上构建 GraphQL API

    摘要 在做dex项目中,没有指导,只能自己摸索,本片文章的转载自https://learnblockchain.cn/article/2566 在操作过程中,遇到的最大难题应该是环境问题,会有各种依赖 ...

  6. 【程序】在STM32单片机上用1700行代码实现基于LwIP 2.1.2协议栈raw API和FatFs文件系统的FTP服务器(20230315版)

    [更新记录] 本程序基于20200703版的程序,作出了如下更新: 1. 解决了当accept函数的参数err!=ERR_OK时,程序出现HardFault错误的bug. 2. 当lwip MEM_S ...

  7. 基于 QT5 百度语音API 图灵机器人API 的智能语音聊天机器人

    基于 QT5 百度语音API 图灵机器人API 的智能语音聊天机器人 程序简介 代码一共分为以下几个模块 伪代码形式为 部分代码 源代码下载地址 程序简介 程序界面包含录音和发送两个按钮 点录音将开始 ...

  8. json graphql_使用json-graphql-server模拟GraphQL API

    json graphql You may find that you need to set up a fast GraphQL server to test your frontend app's ...

  9. aws api_带有AWS的GraphQL API和与React一起使用

    aws api GraphQL has become a go-to API implementation for developers looking to take advantage of fe ...

最新文章

  1. 用批处理查询电脑信息
  2. JVM 调优 —— GC 长时间停顿问题及解决方法
  3. dot全称_dot是什么币
  4. promise 浏览器实现的源码_【大前端01-01】函数式编程与JS异步编程、手写Promise...
  5. vb访问mysql容易死机_VB访问MySQL
  6. eclipse指定JDK版本启动,解决version XXX of the JVM is not suitable for this product.Version:XXX 问题
  7. 【Hadoop篇】--Hadoop常用命令总结
  8. 深度学习技术驱动下的人工智能时代!
  9. MFC 教程【6_应用程序的退出 】
  10. mqtt安装使用教程。(基于rabbitmq插件,docker部署,k8s部署,python教程)
  11. mysql查询优化~group by知多少
  12. python实例方法不可以用类调用_为什么python静态/类方法不可调用?
  13. GMP法规附录《计算机化系统》那些事儿
  14. Implementing Infinite Scroll Into a React Component
  15. 图形界限命令在命令行输入_设置图形界限的命令为在命令行输入
  16. SQL Server求解最近多少销售记录的销售额占比总销售额的指定比例
  17. apn描述文件下载_iOS 11.3 beta 6描述文件下载|苹果iOS 11.3 Beta 6描述文件官方版_ - 极光下载站...
  18. 达梦数据库 find_in_set 函数适配
  19. mysql没有及时启动1053_mysql 启动 错误1053:服务没有及时响应启动或者控制请求
  20. 前端工作随笔日记 Day02

热门文章

  1. Python动物图像分割API简单调用实例演示,阿里达摩院视觉智能开放平台使用步骤
  2. LuceneSolrElasticSearch-面试题
  3. 股票数据的获取以及下载保存
  4. h5将数字翻译为大写汉字_JS将数字转换为大写汉字人民币
  5. Matlab quiver函数用法 - 画矢量箭头图
  6. 微信小程序如何上拉加载下一页
  7. 测试用例设计方法(2)
  8. 四舍五入 java_Java中四舍五入
  9. 【机器学习】树模型遇上类别型特征(Python)
  10. java定义一个eat方法_小黄鸭系列java基础知识 | java中的方法