如何基于已有的 REST API 實現(xiàn) GraphQL API
本文已獲得原作者的獨家授權,有想轉(zhuǎn)載的朋友們可以在后臺聯(lián)系我申請開白哦! PS:歡迎掘友們向我投稿哦,被采用的文章還可以送你掘金精美周邊!
原文地址:How to Implement a GraphQL API on Top of an Existing REST API 原文作者:Tyler Hawkins 譯文出自:掘金翻譯計劃 本文永久鏈接:github.com/xitu/gold-m… 譯者:samyu2000 校對者:PassionPenguin, k8scat

你的 dad jokes 放在哪兒?當然是在 dadabase 里。我們來想象一下,你是全世界最受歡迎的 dad jokes 數(shù)據(jù)庫的管理員。項目的技術概況是:使用 REST API 與數(shù)據(jù)庫通信,這種 REST API 具有搜索笑話和對笑話進行評分的功能;網(wǎng)站的訪問者可以通過一個簡單的用戶界面對每條笑話進行評分。
最近你了解到一種新技術,它叫做 GraphQL,它具有一定的靈活性,可以精準獲取你需要的數(shù)據(jù),而且是使用單一的 API 結點。這聽上去很不錯,于是你打算在應用程序中使用這種技術。但是,你不希望對原有的 REST API 作過多的改動。能否讓你的項目同時支持 REST API 和 GraphQL API?
在本文中,我們會討論如何基于已有的 REST API 來實現(xiàn) GraphQL API。你使用這種方法,不需要對基于原有的 REST API 框架進行調(diào)整,就可以在項目的未完成的模塊中使用 GraphQL。
如果你想看到最終的結果,可以訪問 REST API 代碼 和 前端和 GraphQL API 代碼。還要記得瀏覽一下網(wǎng)站,那些笑話很值得看哦。
初始架構
項目的后臺原先是使用 Node 和 JSON Server 開發(fā)的。JSON Server 利用 Express 為一個模擬的數(shù)據(jù)庫提供了完整的 REST API,并且這個數(shù)據(jù)庫是由一個簡單的 JSON 文件生成的。前端是使用 Vanilla JS 實現(xiàn)的,并使用瀏覽器內(nèi)嵌的 Fetch API 發(fā)出 API 請求。該應用程序托管在 Heroku 上,可以方便地對它進行部署和監(jiān)控。
我們使用的 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 系統(tǒng)把這個文件中的數(shù)據(jù)作為數(shù)據(jù)庫的初始數(shù)據(jù),接著實現(xiàn)一套 REST API,其中包括對 GET, POST, PUT, PATCH 和 DELETE 請求的支持。JSON Server 的神奇之處在于,使用這套 API 就能實現(xiàn)對 JSON 文件的修改,因此數(shù)據(jù)庫就是完全交互式的。JSON Server 不經(jīng)安裝就可以直接由 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}`)
})
復制代碼
欲對這個模擬的數(shù)據(jù)庫進行測試,你可以把 API 有關的倉庫克隆到本地,并運行 npm install 和 npm start。在瀏覽器中訪問 http://localhost:3000/jokes ,頁面會顯示所有的笑話。訪問 http://localhost:3000/ratings ,頁面會顯示所有的評分信息。

太棒了。我們可以在瀏覽器上運行應用程序的后臺?,F(xiàn)在我們把 API 托管在 Heroku 中。首先需要安裝 Heroku 命令行工具。然后,我們可以進行這些操作:登錄,創(chuàng)建項目,推送到 Heroku 服務端,在瀏覽器中打開項目的操作界面。
#?登錄你的?Heroku?賬戶
heroku?login
#?創(chuàng)建項目
heroku?create?dad-joke-dadabase-rest-api
#?將代碼部署到?Heroku?服務端
git?push?heroku?master
#?打開項目的后臺頁面
heroku?open
復制代碼
看,現(xiàn)在我們把 API 發(fā)布到公網(wǎng)上了!

構建用戶界面
既然我們已經(jīng)部署了一個運行中的 REST API,就可以制作前端頁面,并使用 API 把這些笑話數(shù)據(jù)呈現(xiàn)在頁面上,還可以對這些笑話進行評分。下面的 HTML 頁面代碼實現(xiàn)了一個顯示笑話內(nèi)容的容器,笑話內(nèi)容由 JavaScript 代碼加載進來。
"en">
??"utf-8">
??
??Dad?Joke?Dadabase
??do?you?keep?your?dad?jokes??In?a?dadabase?of?course!">
??
??stylesheet"?href="./style.css">
??Dad?Joke?Dadabase
??project">
????jokeContent">
????rateThisJokeContainer">
??????Rate?this?joke:
??????rateThisJokeOptions">
????????">radio"?id="score-1"?>1
????????">radio"?id="score-2"?>2
????????">radio"?id="score-3"?>3
????????">radio"?id="score-4"?>4
????????">radio"?id="score-5"?>5
????????">radio"?id="score-6"?>6
????????">radio"?id="score-7"?>7
????????">radio"?id="score-8"?>8
????????">radio"?id="score-9"?>9
????????">radio"?id="score-10"?>10
??????
????
????averageRating">Average?Rating:?">7.8
????
??
復制代碼
JavaScript 代碼如下。跟 REST API 交互的關鍵代碼在于兩個獲取數(shù)據(jù)的請求。第一個請求通過訪問 /jokes?_embed=ratings 獲取數(shù)據(jù)庫中所有的笑話,第二個請求是 POST 類型的,它通過訪問 /ratings 提交對某個笑話的評分。
const?jokeContent?=?document.querySelector('.jokeContent')
const?jokeRatingValue?=?document.querySelector('.jokeRatingValue')
const?nextJokeButton?=?document.querySelector('#nextJoke')
const?jokes?=?[]
let?currentJokeIndex?=?-1
const?displayNextJoke?=?()?=>?{
??currentJokeIndex++
??if?(currentJokeIndex?>=?jokes.length)?{
????currentJokeIndex?=?0
??}
??const?joke?=?jokes[currentJokeIndex]
??jokeContent.textContent?=?joke.content
??const?totalScore?=?joke.ratings.reduce(
????(total,?rating)?=>?(total?+=?rating.score),
??)
??const?numberOfRatings?=?joke.ratings.length
??const?averageRating?=?totalScore?/?numberOfRatings
??jokeRatingValue.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].id
????const?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?=?false
????????displayNextJoke()
??????})
??}?else?{
????displayNextJoke()
??}
}
nextJokeButton.addEventListener('click',?submitJokeRating)
fetch('/jokes?_embed=ratings')
??.then(response?=>?response.json())
??.then(data?=>?{
????jokes.push(...data)
????displayNextJoke()
??})
復制代碼

安裝并使用 Apollo Server
這樣,我們已經(jīng)完成了項目的架構:它有一個簡單的頁面,該頁面通過 REST API 跟數(shù)據(jù)庫通信。那么,我們?nèi)绾问褂?GraphQL?使用 GraphQL 之前需要哪些準備工作呢?第一步,我們安裝 [apollo-server-express](https://www.npmjs.com/package/apollo-server-express),它是一個程序包,用于實現(xiàn) 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, resolvers 和 dataSources。其中,typeDefs 屬性包含了與我們的 GraphQL API 相關的 schema,我們在相應的包中定義笑話和評分的數(shù)據(jù)類型,以及如何查詢和更新數(shù)據(jù);resolvers 告訴服務器如何處理各種各樣的查詢和更新需求,以及如何連接數(shù)據(jù)源;最后,dataSources 大致描述了 GraphQL API 與 REST API 的關聯(lián)關系。
下面的代碼定義了 Joke 和 Rating 數(shù)據(jù)類型,以及如何查詢和更新數(shù)據(jù)。
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!):?Joke
????jokes:?[Joke]
????rating(id:?Int!):?Rating
????ratings:?[Rating]
??}
??type?Mutation?{
????rating(jokeId:?Int!,?score:?Int!):?Rating
??}
`
module.exports?=?typeDefs
復制代碼
下面是 JokesAPI 類的代碼,主要定義了笑話數(shù)據(jù)創(chuàng)建、查詢、更新、刪除的方法,這些方法分別調(diào)用相應的 REST API 實施相關的數(shù)據(jù)操作。
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
復制代碼
評分數(shù)據(jù)跟笑話相似,只是在每個實例中把 “joke” 變?yōu)?“rating”。欲獲取這部分代碼,可以參考 GitHub 上的代碼倉庫。
最后,我們設置解析器,在其中定義如何使用數(shù)據(jù)源。
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 調(diào)用 GraphQL API 了。為了把新的前端頁面和 GraphQL API 托管在 Heroku 上,我們需要創(chuàng)建并部署第二個應用程序:
#?創(chuàng)建?Heroku?應用程序
heroku?create?dad-joke-dadabase
#?把代碼部署在?Heroku?上
git?push?heroku?master
#?在本地打開?Heroku?應用程序
heroku?open
復制代碼
把 API 端點功能改為獲取笑話的代碼
你應當回憶下,我們有兩個 API 端點供前端頁面調(diào)用:它們的功能分別是獲取笑話和提交評分。現(xiàn)在我們把 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?{
????????id
????????content
????????ratings?{
??????????score
??????????id
??????????jokeId
????????}
??????}
????}
??`,
??}),
})
??.then(res?=>?res.json())
??.then(res?=>?{
????jokes.push(...res.data.jokes)
????displayNextJoke()
??})
復制代碼
現(xiàn)在,我們可以在本地運行應用程序了。實際上,從用戶的角度來說,沒有發(fā)生任何變化。但假如你在瀏覽器的開發(fā)者工具中查看網(wǎng)絡請求,你會發(fā)現(xiàn),現(xiàn)在獲取笑話是通過訪問 /graphql 端點來實現(xiàn)的了。真棒!

把 API 端點功能改為提交評分的代碼
一個 API 請求已完成,還有一個!我們現(xiàn)在對評分功能的代碼進行修改。提交評分的代碼原來類似于:
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?=?false
????displayNextJoke()
??})
復制代碼
現(xiàn)在我們作如下的改動,讓它使用我們的 GraphQL API:
fetch('/graphql',?{
??method:?'POST',
??headers:?{?'Content-Type':?'application/json'?},
??body:?JSON.stringify({
????query:?`
????mutation?CreateRating?{
??????rating(jokeId:?${jokeId},?score:?${score})?{
????????id
????????score
????????jokeId
??????}
????}
??`,
??}),
})
??.then(res?=>?res.json())
??.then(res?=>?{
????const?rating?=?res.data.rating
????const?jokeToUpdate?=?jokes.find(joke?=>?joke.id?===?rating.jokeId)
????jokeToUpdate?&&?jokeToUpdate.ratings.push(rating)
??})
??.finally(()?=>?{
????ratingInput.checked?=?false
????displayNextJoke()
??})
復制代碼
經(jīng)過快速測試,這段代碼符合需求。再次說明,用戶體驗沒有變,但現(xiàn)在我們請求數(shù)據(jù)使用的都是 /graphql 端點。
結論
我們做到了。我們以已有的 REST API 為基礎,成功地實現(xiàn)了一個 GraphQL API 端點。因此,我們也能使用 GraphQL 來實現(xiàn)一些核心功能,而且已有的功能和原來的 REST API 都不需要修改。如今我們可以棄用 REST API,它將來也可能會退出歷史舞臺。
雖然 dad joke 數(shù)據(jù)庫是完全虛擬的項目,但幾乎所有的在 2015 年 GraphQL 發(fā)布會之前成立的科技公司都發(fā)現(xiàn):如果他們改變技術路線,使用 GraphQL,他們自身的情況跟 dad jokes 一樣,也是可行的。另外,還有個好消息,Apollo Server 屬于較靈活的產(chǎn)品,它也可以從包括 REST API 端點在內(nèi)的各種數(shù)據(jù)源獲取數(shù)據(jù)。
如果發(fā)現(xiàn)譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改并 PR,也可獲得相應獎勵積分。文章開頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。
最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「huab119」拉你進技術群,長期交流學習...
關注公眾號「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。

